RO EN

Azure Container Apps (4) — CI/CD with GitHub Actions

Azure Container Apps (4) — CI/CD with GitHub Actions ✨ Imagine generată cu AI
Doru Bulubașa
06 July 2026
43 views

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.