4.3 Automate Resource Provisioning with GitHub Actions
Key Takeaways
- Microsoft’s recommended GitHub Actions authentication to Azure is OpenID Connect (OIDC) federated credentials on a Microsoft Entra application (service principal without a secret) or a user-assigned managed identity — not a long-lived client secret in GitHub secrets.
- The workflow job needs permissions.id-token: write, then azure/login@v2 with client-id, tenant-id, and subscription-id (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID). Do not pass creds: ${{ secrets.AZURE_CREDENTIALS }} for new work.
- az ad sp create-for-rbac --json-auth is deprecated as the happy path. It still emits a client secret that you would store as AZURE_CREDENTIALS; use it only for legacy pipelines and plan to migrate.
- After login, install CLI v2 (az extension add -n ml) and deploy with az deployment group create (Bicep) and/or az ml workspace create / az ml compute create. Path-filter workflows so infra changes do not train models.
- Scope the identity with least-privilege Azure RBAC (resource-group Contributor or a custom role, not subscription Owner). Use GitHub Environments with required reviewers so production Bicep cannot apply from a feature branch without approval.
Automate Resource Provisioning with GitHub Actions
Quick Answer: Authenticate GitHub Actions to Azure with OpenID Connect (OIDC) federated credentials on a user-assigned managed identity or Microsoft Entra app — not a client secret. Grant the job
id-token: write, runazure/login@v2, thenaz deployment group create(Bicep) andaz mlCLI v2. Use GitHub Environments as approval gates and least-privilege RBAC on the identity.
Domain 1’s third IaC bullet is automate resource provisioning by using GitHub Actions workflows. Section 4.1 was clone-and-commit. Section 4.2 was the template. This section is the pipeline that applies the template so humans are not running az deployment group create from a laptop with Owner rights.
Microsoft’s Azure Machine Learning GitHub Actions article (updated 2026) and the OIDC connect-from-Azure article agree: OIDC is the recommended, more secure option. The MLOps v2 demo still shows a service-principal JSON secret because the sample predates the push; the same page now warns that --json-auth is deprecated and tells you to follow the OIDC article for new work.
OIDC instead of a client secret
GitHub Actions can mint a short-lived OIDC id-token for the workflow. Azure federated identity credentials trust that token when the issuer, subject, and audience match. No password is stored in GitHub.
Option 1 — Microsoft Entra application (service principal without a secret)
- Register an app; note Application (client) ID, Directory (tenant) ID, subscription ID.
- Assign Azure RBAC at the resource group (or narrower) that owns the workspace.
- Add a federated credential that trusts GitHub: organization, repository, and a subject such as
repo:contoso/claims:environment:prodorrepo:contoso/claims:ref:refs/heads/main.
Option 2 — User-assigned managed identity
- Create the identity in Azure.
- Assign RBAC to that identity.
- Configure the federated credential on the user-assigned managed identity (same GitHub subject matching).
Copy three values into GitHub Actions secrets (or environment secrets):
| GitHub secret | Source |
|---|---|
AZURE_CLIENT_ID | App or managed identity client ID |
AZURE_TENANT_ID | Directory (tenant) ID |
AZURE_SUBSCRIPTION_ID | Subscription ID |
These are identifiers, not passwords. Still prefer environment secrets on public repositories so a fork pull request cannot read them. GitHub Environments can require reviewers: the job cannot start — and cannot read those secrets — until a human approves.
The legacy path stores the entire az ad sp create-for-rbac --json-auth blob as AZURE_CREDENTIALS and logs in with creds: ${{ secrets.AZURE_CREDENTIALS }}. That blob contains clientSecret. It works and Microsoft still documents it, but it is less secure and the CLI flag is deprecated. Exam answers that pick “paste the client secret into GitHub” as the recommended design are wrong.
Workflow anatomy for provisioning
A provisioning workflow lives in .github/workflows/deploy-infra.yml. Core pieces:
name: deploy-aml-infra
on:
workflow_dispatch:
push:
branches: [main]
paths:
- infra/**
- .github/workflows/deploy-infra.yml
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: prod
steps:
- uses: actions/checkout@v4
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy Bicep
run: |
az deployment group create \
--resource-group rg-claims-prod \
--template-file infra/main.bicep \
--parameters infra/prod.bicepparam
- name: Azure ML CLI
run: |
az extension add -n ml -y
az ml workspace update -f infra/workspace.yml --resource-group rg-claims-prod
az ml compute create -f infra/cpu-cluster.yml --resource-group rg-claims-prod --workspace-name mlw-claims-prod
Required pieces the exam will poke:
permissions.id-token: write— without it,azure/login@v2cannot request the OIDC token.contents: readis enough to checkout. Do not grantcontents: writeunless a later job must push a tag.actions/checkout@v4— the runner needsinfra/.azure/login@v2with the three IDs, notcreds:.- Bicep deploy then
az mlfor workspace-shaped leftovers (compute YAML,image_build_compute). - Path filters —
infra/**so a documentation-only commit does not redeploy prod. Training workflows (Microsoft’s NYC taxi sample) similarly filtercli/jobs/pipelines/...and are a different workflow from provisioning.
Microsoft’s sample training workflow also runs bash setup.sh (sets GROUP, LOCATION, WORKSPACE) and az ml job create against pipeline.yml. That is model CI, not landing-zone provisioning. You can keep both workflows in one repo; do not conflate them on the exam.
Audience for public Azure is api://AzureADTokenExchange (the login action default). Azure US Government uses a different environment and audience — only if the question names that cloud.
Least privilege and promotion gates
The identity GitHub federates to should not be subscription Owner. Prefer:
- Scope: one resource group per environment (
rg-claims-dev,rg-claims-prod). - Role: Contributor on that group is a common demo; production often uses a custom role that can create
Microsoft.MachineLearningServices/*, storage, Key Vault, network private endpoints, and ACR, but cannot attach new subscriptions or assign Owner. - Separate identities for dev and prod federated subjects so a leaked dev workflow cannot apply prod Bicep.
- GitHub Environment
prod: required reviewers, deployment branches limited tomain, environment secrets for the prod client ID.
Federated credential subjects are the other half of least privilege. A credential for repo:contoso/claims:ref:refs/heads/main will not mint Azure tokens for a feature branch. A credential for environment:prod requires the job to declare environment: prod (and therefore the approval gate).
If the workflow creates private endpoints in a managed virtual network, the identity also needs the Machine Learning private-endpoint connection actions (Microsoft.MachineLearningServices/workspaces/privateEndpointConnections/read and write) described in section 4.4.
What not to put in the workflow
- Do not echo secrets.
azdebug logs can leak tokens if you turn on too much verbosity. - Do not check in
setup.shwith prod workspace names only — parameterize like Bicep. - Do not use
continue-on-error: trueon the login or deploy step in production (the azureml-examples training sample does it on setup for lab convenience). - Do not grant the GitHub App or PAT used for clone (section 4.1) the same rights as the Azure identity. They are different planes.
Exam scenario
An MLOps engineer stores AZURE_CREDENTIALS from az ad sp create-for-rbac --role Owner --scopes /subscriptions/<id> --json-auth. Any contributor who can change a workflow on any branch can create resources in the subscription. A contractor forks the public repo and dumps the secret. The fix: delete the client secret, create a user-assigned managed identity with Contributor on rg-claims-prod only, add a federated credential for repo:contoso/claims:environment:prod, put the three IDs in environment secrets, set id-token: write, and require two reviewers on the prod GitHub Environment.
Common trap
Do not choose “GitHub Actions cannot deploy Azure Machine Learning; you must use Azure DevOps” — Microsoft documents GitHub Actions as a first-class path. The sibling trap is omitting id-token: write, which fails OIDC login with a token-permission error. A third trap is treating the NYC taxi training workflow as sufficient infrastructure provisioning — it assumes the workspace already exists.
What is Microsoft’s recommended way for a GitHub Actions workflow to authenticate to Azure when it deploys an Azure Machine Learning workspace with Bicep?
An OIDC deploy job fails during azure/login@v2 before any Bicep runs. The secrets AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_SUBSCRIPTION_ID are present. Which missing workflow setting is the usual cause?
Production Bicep must not apply from a feature branch, and a human must approve each prod deploy. Which combination implements that?