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.
Last updated: August 2026

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 appnot a client secret. Grant the job id-token: write, run azure/login@v2, then az deployment group create (Bicep) and az ml CLI 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)

  1. Register an app; note Application (client) ID, Directory (tenant) ID, subscription ID.
  2. Assign Azure RBAC at the resource group (or narrower) that owns the workspace.
  3. Add a federated credential that trusts GitHub: organization, repository, and a subject such as repo:contoso/claims:environment:prod or repo:contoso/claims:ref:refs/heads/main.

Option 2 — User-assigned managed identity

  1. Create the identity in Azure.
  2. Assign RBAC to that identity.
  3. 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 secretSource
AZURE_CLIENT_IDApp or managed identity client ID
AZURE_TENANT_IDDirectory (tenant) ID
AZURE_SUBSCRIPTION_IDSubscription 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:

  1. permissions.id-token: write — without it, azure/login@v2 cannot request the OIDC token. contents: read is enough to checkout. Do not grant contents: write unless a later job must push a tag.
  2. actions/checkout@v4 — the runner needs infra/.
  3. azure/login@v2 with the three IDs, not creds:.
  4. Bicep deploy then az ml for workspace-shaped leftovers (compute YAML, image_build_compute).
  5. Path filtersinfra/** so a documentation-only commit does not redeploy prod. Training workflows (Microsoft’s NYC taxi sample) similarly filter cli/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 to main, 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. az debug logs can leak tokens if you turn on too much verbosity.
  • Do not check in setup.sh with prod workspace names only — parameterize like Bicep.
  • Do not use continue-on-error: true on 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.

Loading diagram...
OIDC from GitHub Actions into Azure Machine Learning
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

Production Bicep must not apply from a feature branch, and a human must approve each prod deploy. Which combination implements that?

A
B
C
D