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 submodulesAuthentication (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.