CI/CD: GitHub Actions with OIDC & Azure DevOps
By the end of this lesson you will be able to explain CI/CD, set up a GitHub Actions workflow that signs in to Azure with OpenID Connect instead of a stored password, deploy Bicep and a web app from it, protect production with an approval step, and recognise the same ideas in Azure DevOps.
CI/CD in plain words
Continuous integration (CI) means every change pushed to Git is built and tested automatically. Continuous delivery or deployment (CD) means tested changes are released to an environment automatically, sometimes after an approval. Together they are CI/CD, and the automated steps form a pipeline.
Think of a railway coach factory in Kapurthala. Every coach passes the same inspection stations in the same order, and none leaves the factory without passing all of them. A pipeline is that inspection line for your code.
The old way: a secret in GitHub
For years, tutorials created a service principal with a client secret and pasted the whole credential JSON into a GitHub secret called AZURE_CREDENTIALS. It worked, but the secret could leak through logs or forks, it expired at awkward times, and someone had to rotate it.
The modern way: OpenID Connect
OpenID Connect (OIDC) is a sign-in standard. GitHub can issue a short-lived, signed token for each workflow run that says, for example, "this is the main branch of repository priya-dev/tiffin-app". You tell Microsoft Entra ID to trust exactly that statement through a federated identity credential. The workflow swaps GitHub's token for an Azure token. There is no stored secret at all.
It is like a school gate pass signed by the principal for one student on one date. The guard trusts the signature and the details, and the pass is useless tomorrow.
Step by step: set up the trust
You can use an Entra app registration or a user-assigned managed identity. The identity route is simple and needs no app registration.
Step 1: create the identity and give it Contributor on the target resource group only.
az group create --name rg-tiffin-prod --location centralindia --output none
az identity create --resource-group rg-tiffin-prod --name id-github-deploy --output none
principalId=$(az identity show --resource-group rg-tiffin-prod --name id-github-deploy --query principalId --output tsv)
rgId=$(az group show --name rg-tiffin-prod --query id --output tsv)
az role assignment create --assignee-object-id "$principalId" --assignee-principal-type ServicePrincipal \
--role Contributor --scope "$rgId" --output noneStep 2: add a federated credential that trusts only the production environment of one repository.
az identity federated-credential create --resource-group rg-tiffin-prod \
--identity-name id-github-deploy --name github-production \
--issuer https://token.actions.githubusercontent.com \
--subject repo:priya-dev/tiffin-app:environment:production \
--audiences api://AzureADTokenExchange --output noneThe subject is the exact claim GitHub must present. Other common subjects are repo:OWNER/REPO:ref:refs/heads/main for a branch and repo:OWNER/REPO:pull_request for pull requests. Using an environment lets you add approvals in GitHub.
Step 3: collect the three IDs the workflow needs. These are identifiers, not secrets, but keeping them in GitHub repository secrets or variables avoids hard-coding them.
az identity show --resource-group rg-tiffin-prod --name id-github-deploy --query clientId --output tsv
az account show --query "{tenantId:tenantId, subscriptionId:id}"In GitHub, open Settings > Secrets and variables > Actions and add AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_SUBSCRIPTION_ID. Then create an environment called production under Settings > Environments and add yourself as a required reviewer.
Step by step: the workflow
Save this as .github/workflows/deploy.yml:
name: Deploy tiffin app
on:
push:
branches: [main]
permissions:
id-token: write # allow the job to request a GitHub OIDC token
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v7
- name: Sign in to Azure with OIDC
uses: azure/login@v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Preview infrastructure changes
run: az deployment group what-if --resource-group rg-tiffin-prod --template-file infra/main.bicep
- name: Deploy infrastructure
run: az deployment group create --resource-group rg-tiffin-prod --template-file infra/main.bicep --output none
- name: Deploy app code
uses: azure/webapps-deploy@v3
with:
app-name: app-tiffin-prod-cin
package: ./srcThe important parts:
permissions: id-token: writelets the job request the OIDC token. Without it, sign-in fails.environment: productionmakes the job match the federated credential's subject and pauses for the reviewer's approval.azure/loginexchanges the token. Microsoft's OIDC article showsazure/login@v2; the action's releases page listed v3 as the latest major version in November 2026. Pin a major version and update it deliberately.- After login, plain
azcommands work in later steps.
When you push to main, the run waits for approval, then signs in, shows the what-if, deploys and publishes the app. In the log you should see a "Login successful" message and no secret anywhere.
Testing before deploying
A real pipeline usually has a CI job that runs tests and lints the Bicep before the deploy job:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- run: pip install -r src/requirements.txt pytest && pytest
- run: az bicep build --file infra/main.bicepAdd needs: test to the deploy job so it runs only when tests pass. This snippet goes under jobs: alongside deploy.
Azure DevOps overview
Azure DevOps is Microsoft's older, all-in-one DevOps suite, still widely used in Indian IT services companies and banks. It has five parts:
| Part | Purpose |
|---|---|
| Azure Boards | Work items, sprints, Kanban boards |
| Azure Repos | Git repositories |
| Azure Pipelines | CI/CD with YAML pipelines |
| Azure Test Plans | Manual and exploratory testing |
| Azure Artifacts | Package feeds for npm, NuGet, Maven, Python |
Pipelines connect to Azure through a service connection. Create it with workload identity federation, the same secret-free idea as GitHub OIDC. A small YAML pipeline:
trigger:
branches:
include: [main]
pool:
vmImage: ubuntu-latest
steps:
- task: AzureCLI@2
inputs:
azureSubscription: sc-tiffin-prod # service connection name
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az deployment group create --resource-group rg-tiffin-prod --template-file infra/main.bicep --output noneGitHub Actions and Azure Pipelines both work well with Azure. Teams with code on GitHub usually choose Actions; teams already on Azure DevOps keep Pipelines.
Common mistakes
- Missing
id-token: write. The login step fails because no OIDC token can be requested. - Subject mismatch. The credential trusts
environment:production, but the job has noenvironment. The error mentions that no matching federated identity was found. - Owner role for the pipeline. Contributor on one resource group is usually enough; add User Access Administrator only if the pipeline must create role assignments, and scope it narrowly.
- Storing client secrets "temporarily". Temporary secrets become permanent. Start with OIDC.
Key points
- CI builds and tests every change; CD releases tested changes, often with approvals.
- OIDC with a federated identity credential lets GitHub Actions sign in to Azure without stored secrets.
- The federated credential's subject must match the repository and branch or environment exactly.
- Grant the deployment identity a narrow role at a narrow scope, and use environments for approvals.
- Azure DevOps offers Boards, Repos, Pipelines, Test Plans and Artifacts; its service connections support workload identity federation.
Exercise
Kabir wants his repository kabir-dev/marks-api to deploy to Azure only from the main branch, without any GitHub environment. Write the az identity federated-credential create command for an identity id-marks-deploy in rg-marks, and list the two lines in the workflow that must be present for login to work.
Show solution
az identity federated-credential create --resource-group rg-marks \
--identity-name id-marks-deploy --name github-main \
--issuer https://token.actions.githubusercontent.com \
--subject repo:kabir-dev/marks-api:ref:refs/heads/main \
--audiences api://AzureADTokenExchange --output nonepermissions:
id-token: writeThe workflow needs id-token: write permission and an azure/login step with client-id, tenant-id and subscription-id. Because the subject names the main branch, runs from other branches cannot sign in.
Quick Quiz
1. What does OIDC remove from a GitHub-to-Azure deployment?
GitHub's short-lived token is exchanged for an Azure token, so no secret is stored.
2. Which workflow permission is required for OIDC login?
The job must be allowed to request an OIDC ID token from GitHub.
3. In Azure DevOps, what connects a pipeline to an Azure subscription?
Service connections hold the trust (ideally workload identity federation) used by pipeline tasks.
Learn Microsoft Azure with a mentor
Free lessons get you started. Our Cloud & DevOps training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
