Skip to content

Deploy to Azure with GitHub Actions

Lesson 2 of 7Intermediate → Advanced5 min readGitHub Actions & CI/CD · Continuous DeliveryVerified: azure/login v3, azure/webapps-deploy v2, azure/functions-action v1, August 2026

Azure’s older GitHub Actions guidance told you to run az ad sp create-for-rbac --sdk-auth, take the JSON blob it printed, and paste it into a repository secret called AZURE_CREDENTIALS. That blob contains a client secret with a multi-year lifetime. It is the Azure equivalent of a long-lived access key, and the same argument applies: replace it with a federated credential.

permissions:
contents: read
id-token: write
steps:
- uses: azure/login@v3
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

What it doesSigns in to Azure using the workflow's OIDC token, exchanged for an Entra ID access token.

Why we run itNo client secret exists to leak, expire, or rotate. Azure validates the token's issuer and subject against a federated credential you configured on the app registration.

Expected resultA signed-in Azure CLI context for subsequent steps. Nothing sensitive is stored in the repository.

Note that these are vars, not secrets. A client ID, tenant ID and subscription ID are identifiers, not credentials — they identify which application is trying to sign in, and they are useless without a federated credential that trusts your specific repository and branch. Storing them as secrets is harmless but misleading: it implies a leak would matter, and it makes debugging worse because the values are masked in logs.

Deploying straight to a production App Service replaces the running application in place. Deployment slots give you a safer sequence: deploy to a slot, verify it, then swap.

- name: Deploy to the staging slot
uses: azure/webapps-deploy@v2
with:
app-name: example-app
slot-name: staging
package: ./publish
- name: Smoke test the slot
run: |
for _ in $(seq 1 30); do
if curl -fsS "https://example-app-staging.azurewebsites.net/healthz"; then
exit 0
fi
sleep 10
done
echo "::error::staging slot did not become healthy"
exit 1
- name: Swap into production
run: az webapp deployment slot swap --name example-app --resource-group example-rg --slot staging

The swap is what makes this valuable. It is a routing change rather than a redeployment, so it takes seconds, and the previously-live application is still sitting in the staging slot afterwards. Rolling back is the same command again — swapping the slots back.

The retry loop matters more than it looks. A freshly deployed slot needs time to start, and a single curl immediately after deployment tests nothing but the load balancer. Polling until healthy, with a bounded number of attempts, is the difference between a smoke test and a coin flip.

- uses: azure/functions-action@v1
with:
app-name: example-functions
package: ./publish

Functions have their own version of the slot pattern, and the same rule applies: deploy to a slot, verify, swap. The consumption plan adds a wrinkle — a cold start after deployment means the first request can take seconds, so a smoke test needs the same retry loop rather than a single call.

For containerised workloads, deploy by digest for exactly the reasons covered in Docker CI:

- name: Deploy the revision
run: |
az containerapp update \
--name example-app \
--resource-group example-rg \
--image "ghcr.io/OWNER/REPO@${DIGEST}"
env:
DIGEST: ${{ needs.build.outputs.digest }}

Container Apps creates a new revision rather than mutating the old one, which gives a genuine rollback: revisions are retained, and traffic can be shifted back to a previous one without rebuilding anything. That also makes gradual rollout possible — split traffic across two revisions and increase the new one’s share as confidence grows.

Static Web Apps issue a deployment token rather than using the login above. This is one of the few places where a stored secret is still the supported mechanism, so treat it accordingly: scope it to the one resource, store it as an environment secret rather than a repository secret so only the deploy environment can read it, and rotate it on a schedule.

Its pull-request preview feature deploys a preview environment per pull request, which is genuinely useful and carries the usual caveat: a preview built from a fork’s code is running that fork’s code on your infrastructure under your domain. Restrict previews to branches in the repository unless you have thought that through.

DeploymentRollbackReliable?
App Service with slotsSwap backYes, seconds
App Service without slotsRedeploy the previous packageOnly if you kept the package
Functions with slotsSwap backYes
Container AppsShift traffic to the previous revisionYes — revisions are retained
Static Web AppsRedeploy the previous buildYes, if the artifact still exists
Database migrationNo.
Bicep/ARM infrastructure changeRe-deploy the previous templateSometimes — a delete is not undone by a redeploy

The pattern is the same as on any cloud: rollback is reliable exactly when the previous state still exists as an immutable object. Slots and revisions are that object; an in-place package deployment is not.

The federated identity should hold the narrowest Azure RBAC role that completes the deployment, at the narrowest scope:

  • Assign at the resource or resource group scope, not the subscription. Contributor on a subscription means the deploy identity can delete anything in it.
  • Use a purpose-built role where the built-in ones are too broad. “Website Contributor” is a much smaller grant than “Contributor”.
  • Separate app registrations for staging and production, each with its own federated credential restricted to the corresponding environment.

Restricting the federated credential’s subject is the control that stops a branch from deploying to production — the Azure equivalent of the sub claim condition in AWS OIDC, and it is covered in Azure OIDC.

  1. Follow Azure OIDC to add a federated credential to an app registration, scoped to your repository’s production environment.

  2. Store the client, tenant and subscription IDs as repository variables, not secrets.

  3. Write a workflow with id-token: write that logs in and runs az account show. Confirm it succeeds with no secret in the repository.

  4. Add an App Service with a staging slot. Deploy to the slot, poll /healthz until it responds, then swap.

  5. Swap back and confirm the previous version returns. That is your rollback, and having run it once before you need it is the entire point.

GitHub Actions Security ChecklistAudit your workflows against the failure modes that actually cause incidents. Free and complete.

Want production-ready workflow templates? The Professional Toolkit has five, with permissions set correctly.