Skip to content

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-x64 only — there is no linux-musl-arm64 asset in any current release line (7.4 LTS, 7.5, 7.6). The linux-arm64 assets are glibc builds; a glibc .NET runtime does not run on musl, and gcompat cannot shim an entire .NET runtime (it works for a small glibc CLI like op, 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/arm64 image, 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 vscode user (ADR 0007), so modules must be readable by that user.
  • The full Microsoft.Graph meta-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.gz from the PowerShell/PowerShell GitHub release and verifies it against the release's hashes.sha256. That file is UTF-16LE with a BOM and CRLF line endings, which GNU sha256sum -c cannot 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 on x86_64; on aarch64 the staging directory is created empty so the runtime-stage COPY --from=builder /opt/microsoft still 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 pwsh onto PATH and installs the Graph submodules pinned to one SDK version at AllUsers scope (/usr/local/share/powershell/Modules, on the default PSModulePath for every user). The submodules release in lockstep, so a single pin keeps them mutually compatible. Installation uses the bundled Microsoft.PowerShell.PSResourceGet (Install-PSResource) rather than PowerShellGet's Install-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), and Microsoft.Graph.Authentication is installed first so that when Users and Groups resolve their shared Authentication dependency the pinned version is already present and a newer one is not pulled. The whole layer is a logged no-op when pwsh is absent (arm64).
  • Telemetry off: POWERSHELL_TELEMETRY_OPTOUT=1, POWERSHELL_UPDATECHECK=Off, DOTNET_CLI_TELEMETRY_OPTOUT=1 keep pwsh quiet behind the egress firewall (mirrors the browser posture in ADR 0016).
  • Renovate tracks the pwsh binary via datasource=github-releases depName=PowerShell/PowerShell and the Graph module pin via datasource=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 as powershell in renovate.json.

Rejected:

  • The full Microsoft.Graph meta-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-arm64 tarball under gcompat — a glibc .NET runtime segfaults on musl; gcompat cannot shim CoreCLR. Same lesson as the bundled glibc browsers in ADR 0016.
  • An apk PowerShell package — Alpine's repos do not ship one.
  • The dotnet tool install path — pulls the full .NET SDK, far heavier than the self-contained musl tarball.

Consequences

Accepted:

  • pwsh, Connect-MgGraph, and the Users/Groups cmdlets 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 — pwsh is intentionally absent; the build logs a skip and succeeds. The BATS suite skips its PowerShell assertions on aarch64. This asymmetry is the significant caveat: a project that depends on PowerShell must run the amd64 image.
  • Using the modules at runtime (Connect-MgGraph) reaches login.microsoftonline.com and graph.microsoft.com, which a consuming project must add to its egress allowlist (and proxy.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 in examples/egress-allowlist.txt document 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 Authentication dependency matched.
  • The powershell Renovate group couples two pins. Graph 2.41.0 and 2.41.1 needed a runtime that ships System.Text.Json 10 (PowerShell 7.6+, .NET 10), although the module manifest declares PowerShellVersion = '5.1': on PowerShell 7.5 and earlier, Connect-MgGraph failed to load it (msgraph-sdk-powershell#3810). Graph 2.41.2 makes the module's load context prefer its own copy of a shared-framework assembly such as System.Text.Json when 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 moving pwsh below 7.6, check that Connect-MgGraph loads on the target runtime; if it does not, move Graph back to 2.40.0 or earlier.