The last part of the series about Azure Container Apps. We covered why, configuration and scaling, and Dapr. Here we tie everything together with a complete automated deployment pipeline.
Secret-free Authentication: OIDC
Consistent with everything we built in the series, the CI/CD pipeline must not store secrets either. Instead of a service principal with a client secret stored in GitHub Secrets, we use OIDC (OpenID Connect) with federated credentials: GitHub Actions receives a temporary token from Azure on each run, without any stored credential.
Configuring the federated credential
# Create an app registration (or use an existing one)
APP_ID=$(az ad app create --display-name "github-actions-my-api" --query appId -o tsv)
az ad sp create --id $APP_ID
# Federated credential linked to repo and branch
az ad app federated-credential create \
--id $APP_ID \
--parameters '{
"name": "github-main",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:myorg/my-repo:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"]
}'
The subject field restricts who can request tokens: only workflows from the main branch of your repo. A pull request from another repo cannot gain access.
Roles for the service principal
PRINCIPAL_ID=$(az ad sp show --id $APP_ID --query id -o tsv)
ACR_ID=$(az acr show --name myregistry --query id -o tsv)
RG_ID=$(az group show --name my-rg --query id -o tsv)
# Push to Container Registry
az role assignment create --assignee $PRINCIPAL_ID \
--role "AcrPush" --scope $ACR_ID
# Deploy to Container Apps (Contributor on resource group or more granular)
az role assignment create --assignee $PRINCIPAL_ID \
--role "Contributor" --scope $RG_ID
Secrets in GitHub
With OIDC, you don't put any sensitive secret in GitHub — only public identifiers:
# These are not real secrets, just identifiers
AZURE_CLIENT_ID =
AZURE_TENANT_ID =
AZURE_SUBSCRIPTION_ID =
The complete workflow
# .github/workflows/deploy.yml
name: Build and Deploy to Container Apps
on:
push:
branches: [ main ]
permissions:
id-token: write # required for OIDC
contents: read
env:
REGISTRY: myregistry.azurecr.io
IMAGE_NAME: my-api
CONTAINER_APP: my-api
RESOURCE_GROUP: my-rg
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Azure login via OIDC -- no secrets
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# Login to ACR using Azure token
- name: ACR Login
run: az acr login --name ${{ env.REGISTRY }}
# Build and push image, tagged with commit SHA
- name: Build and push
run: |
IMAGE_TAG=${{ github.sha }}
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$IMAGE_TAG .
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$IMAGE_TAG
# Deploy the new image to Container Apps
- name: Deploy to Container Apps
run: |
az containerapp update \
--name ${{ env.CONTAINER_APP }} \
--resource-group ${{ env.RESOURCE_GROUP }} \
--image ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
The key point: the image is tagged with github.sha (the commit SHA), not with latest. Each deploy is traceable to the exact commit, and rollback means simply redeploying a previous SHA.
Revisions and traffic splitting
Each containerapp update creates a new revision. By default, ACA routes 100% of traffic to the latest revision. But you can do real canary releases by splitting traffic between revisions.
Enabling multiple revisions mode
az containerapp revision set-mode \
--name my-api --resource-group my-rg \
--mode multiple
Traffic split: 90% stable, 10% canary
# Send 10% of traffic to the new revision, 90% to the stable one
az containerapp ingress traffic set \
--name my-api --resource-group my-rg \
--revision-weight \
my-api--stable-rev=90 \
my-api--new-rev=10
You monitor the new revision in Application Insights for a few hours. If error and latency metrics are OK, you move traffic to 100%. If not, you revert to 0% instantly — no redeploy, just a routing change.
# Promote the new revision to 100%
az containerapp ingress traffic set \
--name my-api --resource-group my-rg \
--revision-weight my-api--new-rev=100
Container Apps pipeline checklist
- OIDC instead of client secret — federated credentials, zero secrets in GitHub
- Restricted subject — only your branch and repo can request tokens
- Managed Identity for pull — Container App pulls the image from ACR without credentials
- Images tagged with SHA — traceability to commit, simple rollback
- Configured health probes — ACA does not route traffic to unhealthy revisions
- Traffic splitting for canary — gradual validation before 100%
Conclusion of the Container Apps series
With this, the mini-series about Azure Container Apps is complete: we established when migration is worthwhile, configured elastic scaling with KEDA, integrated Dapr for microservices, and automated deployment with GitHub Actions — all built on the secret-free security foundation from previous articles.
The continuation of the Cloud-native with Azure and .NET series will cover observability with Application Insights: distributed tracing, custom metrics, and alerts.
If you have questions or want to discuss how to structure the pipeline for your project, write to me at contact@ludoprogramming.com.