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.