0017. PowerShell and Microsoft Graph modules for Entra ID / M365 automation¶
Status¶
Accepted
Context¶
Infrastructure and platform work increasingly spills into Microsoft Entra ID
(formerly Azure AD) and Microsoft 365 administration — creating users, managing
group membership, reading directory objects. The canonical tooling for this is
PowerShell plus the Microsoft Graph PowerShell SDK, and the SDK's
Connect-MgGraph / Get-MgUser / Get-MgGroup cmdlets are what every Microsoft
runbook and doc assumes. The image shipped no PowerShell at all, so none of that
was possible without the user hand-installing a shell and modules at runtime
(which the egress firewall would then block).
Constraints specific to this image:
- Alpine/musl base (ADR 0001). Microsoft publishes an
Alpine-native (musl) PowerShell build for
linux-musl-x64only — there is nolinux-musl-arm64asset in any current release line (7.4 LTS, 7.5, 7.6). Thelinux-arm64assets are glibc builds; a glibc .NET runtime does not run on musl, andgcompatcannot shim an entire .NET runtime (it works for a small glibc CLI likeop, not for CoreCLR). This is the same musl footgun that made bundled glibc browsers unusable in ADR 0016. - Native arm64 release builds (ADR 0012).
The release pipeline builds a real
linux/arm64image, and the maintainer develops on Apple Silicon (arm64). The build must not break on arm64. - Security-hardened, default-deny egress (ADR 0009). PowerShell phones home on startup (telemetry + update check), which would be blocked and would spam the firewall logs.
- Runs as the non-root
vscodeuser (ADR 0007), so modules must be readable by that user. - The full
Microsoft.Graphmeta-module is a ~38-submodule umbrella (hundreds of MB, thousands of cmdlets, slow to import). Microsoft's own guidance is to install only the submodules you need.
Decision¶
Install PowerShell from Microsoft's official musl-x64 tarball, amd64-only,
and pre-install the most common Microsoft Graph submodules — Authentication
(the mandatory shared dependency), Users, and Groups:
- Builder stage downloads
powershell-<ver>-linux-musl-x64.tar.gzfrom thePowerShell/PowerShellGitHub release and verifies it against the release'shashes.sha256. That file is UTF-16LE with a BOM and CRLF line endings, which GNUsha256sum -ccannot parse, so the build strips the NULs/BOM/CR, pulls just the 64-hex digest for our asset, and verifies it against a clean, freshly built checksum line (failing closed if no digest is found). It then extracts to/opt/microsoft/powershell/7. This is gated onx86_64; onaarch64the staging directory is created empty so the runtime-stageCOPY --from=builder /opt/microsoftstill succeeds and the arm64 image keeps building. - Runtime stage
apk adds the .NET runtime dependencies (icu-libs libintl krb5-libs userspace-rcu ncurses-terminfo-base libssl3 tzdata) unconditionally — they exist on both arches and are cheap, and listing them makes the dependency explicit rather than relying on transitive pulls. - A later layer (K2) symlinks
pwshontoPATHand installs the Graph submodules pinned to one SDK version at AllUsers scope (/usr/local/share/powershell/Modules, on the defaultPSModulePathfor every user). The submodules release in lockstep, so a single pin keeps them mutually compatible. Installation uses the bundledMicrosoft.PowerShell.PSResourceGet(Install-PSResource) rather than PowerShellGet'sInstall-Module: PSResourceGet talks to the PSGallery NuGet v3 API directly and needs no NuGet-provider bootstrap from the external OneGet CDN (onegetcdn.azureedge.net), which has a history of cert/availability outages — dropping it keeps the build deterministic. The pin is applied as the NuGet exact-version range[<ver>](a bare version would be a minimum), andMicrosoft.Graph.Authenticationis installed first so that when Users and Groups resolve their sharedAuthenticationdependency the pinned version is already present and a newer one is not pulled. The whole layer is a logged no-op whenpwshis absent (arm64). - Telemetry off:
POWERSHELL_TELEMETRY_OPTOUT=1,POWERSHELL_UPDATECHECK=Off,DOTNET_CLI_TELEMETRY_OPTOUT=1keeppwshquiet behind the egress firewall (mirrors the browser posture in ADR 0016). - Renovate tracks the
pwshbinary viadatasource=github-releases depName=PowerShell/PowerShelland the Graph module pin viadatasource=nuget depName=Microsoft.Graph.Authentication registryUrl=https://www.powershellgallery.com/api/v2/(PowerShell Gallery is a standard NuGet v2 OData feed, which Renovate's nuget datasource targets by default). The two pins are grouped aspowershellinrenovate.json.
Rejected:
- The full
Microsoft.Graphmeta-module — hundreds of MB and a slow import for ~38 submodules, most unused here. Installing only Authentication + Users + Groups keeps the image lean and matches Microsoft's guidance. Additional submodules (e.g.Identity.DirectoryManagement,Applications) install the same way if a project needs them. - The glibc
linux-arm64tarball undergcompat— a glibc .NET runtime segfaults on musl;gcompatcannot shim CoreCLR. Same lesson as the bundled glibc browsers in ADR 0016. - An
apkPowerShell package — Alpine's repos do not ship one. - The
dotnet toolinstall path — pulls the full .NET SDK, far heavier than the self-contained musl tarball.
Consequences¶
Accepted:
pwsh,Connect-MgGraph, and theUsers/Groupscmdlets are available out of the box on amd64. Entra ID / M365 automation works without any runtime install.- PowerShell is amd64-only. On
aarch64— the maintainer's Apple Silicon dev box and the native arm64 release leg —pwshis intentionally absent; the build logs a skip and succeeds. The BATS suite skips its PowerShell assertions onaarch64. This asymmetry is the significant caveat: a project that depends on PowerShell must run the amd64 image. - Using the modules at runtime (
Connect-MgGraph) reacheslogin.microsoftonline.comandgraph.microsoft.com, which a consuming project must add to its egress allowlist (andproxy.yaml). Installing the modules is baked in at build time — where the runtime firewall does not apply — so no allowlist change is needed just to have them present. The commented Microsoft Graph entries inexamples/egress-allowlist.txtdocument the runtime hosts. - Image grows ~200–300 MB on amd64 (PowerShell ~150 MB + the three Graph submodules). The arm64 image is unchanged. The CI/release free-disk step (added in ADR 0016's wake) already covers the headroom.
- The three Graph submodules share a single Renovate pin. Because they release in
lockstep, one bump moves all three together and keeps the
Authenticationdependency matched. - The
powershellRenovate group couples two pins. Graph2.41.0and2.41.1needed a runtime that shipsSystem.Text.Json10 (PowerShell 7.6+, .NET 10), although the module manifest declaresPowerShellVersion = '5.1': on PowerShell 7.5 and earlier,Connect-MgGraphfailed to load it (msgraph-sdk-powershell#3810). Graph2.41.2makes the module's load context prefer its own copy of a shared-framework assembly such asSystem.Text.Jsonwhen that copy is newer than the runtime's (msgraph-sdk-powershell#3813), which should lift that requirement. As of October 2026 we found no test or report on PowerShell below 7.6, and the pull request added no test, so this image still treats 7.6+ as the floor. Before movingpwshbelow 7.6, check thatConnect-MgGraphloads on the target runtime; if it does not, move Graph back to2.40.0or earlier.