Skip to main content
友田 陽大
Azure Container Apps in production
Azure
Container Apps
CI/CD
GitHub Actions
Bicep
DevOps
インフラ
セキュリティ

Azure Container Apps CI/CD guide: deploy safely and automatically with GitHub Actions, OIDC keyless, Bicep, and Blue/Green revisions

An implementation guide for building Azure Container Apps CI/CD at production quality. It explains, with YAML/Bicep/az CLI faithful to Microsoft Learn official docs: azure/container-apps-deploy-action, keyless authentication via OIDC (federated credentials), a managed identity for ACR pull, declarative deployment with Bicep, Blue/Green and canary via revision traffic splitting, and automatic rollback.

Published
Reading time
7 min read
Author
友田 陽大
Share

Deployment isn't "as long as it works"; a mechanism that runs safely, repeatably, and without leaking keys determines production quality. Placing long-lived secrets on GitHub, overwriting with the latest tag, not being able to revert on failure — all are breeding grounds for accidents.

This article assembles production CI/CD for Azure Container Apps (ACA), faithful to Microsoft Learn's GitHub Actions documentation. I've run keyless CI/CD with OIDC and Blue/Green deployment on Fargate in production on AWS. The deployment principles of "trust a short-lived token" and "don't switch over if it fails" are the same shape on Azure too. For ACA overall, see the Azure Container Apps production-operations guide.


The big picture: commit → new revision

Azure Container Apps allows you to use GitHub Actions to publish revisions to your container app. As commits are pushed to your GitHub repository, a workflow is triggered which updates the container image in the container registry. Azure Container Apps creates a new revision based on the updated container image. (— Publish revisions with GitHub Actions)

The flow is simple: commit → workflow triggered → build the image and push to the registry → ACA creates a new revision. ACA's revisions are immutable snapshots, so a deployment = a switch to a new revision.


The official action: azure/container-apps-deploy-action

To build and deploy your container app, you add the azure/container-apps-deploy-action action to your GitHub Actions workflow.

This action supports three scenarios.

  • Build from a Dockerfile and deploy
  • Build from source without a Dockerfile and deploy (.NET, Java, Node.js, PHP, Python)
  • Deploy an existing container image

Deploy an existing image (the most controllable)

Doing the build in a separate step and deploying an image with a unique tag (commit SHA) is the most traceable in production:

- name: Build and deploy Container App
  uses: azure/container-apps-deploy-action@v1
  with:
    acrName: myregistry
    containerAppName: my-container-app
    resourceGroup: my-rg
    imageToDeploy: myregistry.azurecr.io/app:${{ github.sha }}

The official docs also warn.

If you're building a container image in a separate step, make sure you use a unique tag such as the commit SHA instead of a stable tag like latest.

Don't use latest; make it unique with ${{ github.sha }} etc. This prevents caching issues and the accident of "not knowing which revision is which code."


Authentication: service principal vs. OIDC (keyless)

Shortest but can become debt: service principal

The official starter places the service principal's JSON credentials in the AZURE_CREDENTIALS secret.

az ad sp create-for-rbac --name my-app-credentials \
  --role contributor \
  --scopes /subscriptions/<SUBSCRIPTION_ID>/resourceGroups/my-container-app-rg \
  --json-auth

It works, but it stores long-lived credentials (a password) on GitHub. Leakage risk and rotation burden remain. It's fine for learning or PoC, but in production use the OIDC below.

The production answer: OIDC (federated credentials)

Trust GitHub Actions' OIDC token with a Microsoft Entra federated credential, and place not a single long-lived secret. azure/login authenticates with only client-id/tenant-id/subscription-id.

name: deploy-aca
on:
  push: { branches: [main] }
permissions:
  id-token: write      # OIDCトークン発行に必須
  contents: read
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Log in to Azure (OIDC, keyless)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Build and deploy Container App
        uses: azure/container-apps-deploy-action@v1
        with:
          acrName: myregistry
          containerAppName: my-container-app
          resourceGroup: my-rg
          imageToDeploy: myregistry.azurecr.io/app:${{ github.sha }}

A federated credential just registers, on the target Entra app / managed identity, a condition (subject) that "trusts a token from this repository's main branch." Key storage and rotation disappear. The thinking is completely the same shape as AWS's IAM OIDC provider (throw away keys with GitHub Actions OIDC).


Registry authentication: separate push and pull

CI (push) and the app (pull) each authenticate with a separate least-privilege identity.

To pull images, Azure Container Apps uses either managed identity (recommended) or admin credentials to authenticate with the Azure Container Registry. (— github-actions)

The app's image pull is via a managed identity (recommended). Grant the app's managed identity the AcrPull role and specify --identity in the registry setting.

# アプリにシステム割当IDを付与
az containerapp identity assign \
  --name my-container-app --resource-group my-rg --system-assigned

# そのIDにACRのAcrPullロールを付与(pull専用の最小権限)
az role assignment create \
  --assignee <MANAGED_IDENTITY_PRINCIPAL_ID> \
  --role AcrPull --scope <ACR_RESOURCE_ID>

# レジストリをマネージドID認証に設定
az containerapp registry set \
  --name my-container-app --resource-group my-rg \
  --server myregistry.azurecr.io --identity system

With this, you hold the registry password nowhere. CI has push permission and the app has only pull permission — responsibility and privilege are separated (the principle of least privilege).

Non-ACR registries like GHCR can also be used. In that case, set the credentials (a PAT's read:packages) with az containerapp registry set --server ghcr.io. Even for a public image, the authentication setting is needed.


Deployment strategy: Blue/Green, canary, rollback

Single-revision mode: zero downtime (default)

If you set nothing, ACA automatically switches over fully after the new revision is ready (all probes pass + scaled to the old count). If it fails, it stays on the old revision. This is enough for many services. It's the same safe-side behavior as AWS Fargate's "rolling + deployment circuit breaker."

Multiple-revision mode: traffic splitting

To ship carefully, switch to multiple-revision mode and allocate traffic by %.

# 複数リビジョンモードに(一度だけ)
az containerapp revision set-mode --name my-api --resource-group my-rg --mode multiple

# カナリア:新版に10%だけ
az containerapp ingress traffic set --name my-api --resource-group my-rg \
  --revision-weight my-api--green=10 my-api--blue=90

# 問題なければ昇格(Blue/Green切替)
az containerapp ingress traffic set --name my-api --resource-group my-rg \
  --revision-weight my-api--green=100

# 異常を検知したら即ロールバック(旧版へ100%戻す)
az containerapp ingress traffic set --name my-api --resource-group my-rg \
  --revision-weight my-api--blue=100

The strength of the immutable-revision model is that rollback gets by with just "send 100% back to the old revision." Since the old version remains active, no rebuild or redeploy is needed, and you can revert in seconds.

To build it into a pipeline, automate the stages of smoke test after deployment → metric monitoring → promote with traffic set. Adding a guard that automatically rolls back when it detects an SLO violation (symptom-based alert design) raises production quality a level.


IaC: declaratively with Bicep / Terraform

Click operations and hand-typed az aren't reproducible and become technical debt. In production, use declarative code.

Bicep (Azure-native)

param image string   // CIから一意タグを渡す(例: myregistry.azurecr.io/app:<sha>)

resource app 'Microsoft.App/containerApps@2025-02-02-preview' = {
  name: 'my-api'
  location: location
  identity: { type: 'SystemAssigned' }
  properties: {
    managedEnvironmentId: environmentId
    configuration: {
      activeRevisionsMode: 'single'
      ingress: { external: true, targetPort: 8080, transport: 'auto' }
      registries: [
        { server: 'myregistry.azurecr.io', identity: 'system' }  // マネージドIDでpull
      ]
    }
    template: {
      containers: [
        { name: 'api', image: image, resources: { cpu: json('0.5'), memory: '1.0Gi' } }
      ]
      scale: { minReplicas: 1, maxReplicas: 20 }
    }
  }
}

From CI, run this Bicep with az deployment group create --parameters image=...:${{ github.sha }}. You can deploy both infrastructure and the app declaratively in the same pipeline. If you're a Terraform (azurerm_container_app) person, you can write the same configuration in HCL (the Terraform example in the production-operations guide).

Auto-generating a starter workflow

If writing from scratch is a chore, you can generate a starter workflow with the az containerapp github-action add family of CLI. First get it running, then grow it to production requirements (OIDC, IaC, traffic splitting) from there — the KISS/YAGNI order.

az containerapp up --source . --ingress external is the shortest learning command that does resource creation, image build, registry storage, and deployment in one go. Use up for PoC and IaC for production.


CI/CD checklist

  • Authentication is OIDC (federated credentials), keyless. Don't use AZURE_CREDENTIALS long-lived secrets in production.
  • The image tag is unique with the commit SHA. No latest.
  • ACR pull is via a managed identity (AcrPull). CI's push and the app's pull with separate identities, least-privilege.
  • Deploy with zero downtime in single mode, and to ship carefully, canary → promote in multiple mode.
  • Rollback is to the old revision with traffic set (reverts in seconds).
  • Infrastructure is declarative with Bicep/Terraform. Eliminate hand-typing and clicking.
  • Smoke test + SLO monitoring after deployment, auto-rollback on violation.

Conclusion

ACA's CI/CD is a simple model that builds & deploys with azure/container-apps-deploy-action and creates an immutable revision per commit. There are four keys to production quality — OIDC keyless authentication, a unique tag of the commit SHA, least-privilege pull via a managed identity, and Blue/Green via traffic splitting + rollback that reverts in seconds. These are the same shape, with only different vocabulary, as the principles I've run with keyless CI/CD and Blue/Green on AWS.

For consultations on keyless CI/CD, Blue/Green deployment, and IaC-ization, go to contact. For production operation overall, see the Azure Container Apps production-operations guide.

友田

友田 陽大

Developer of a METI Minister's Award–winning product. With TypeScript + Python + AWS, I deliver SaaS, industry DX, and production-grade generative AI (RAG) end to end — from requirements to infrastructure and operations — single-handedly.

Got a challenge?

From design to implementation and operations — solo × generative AI

Implementation like this article's, end to end from requirements to production. Start with a free 30-minute technical consult and tell me about your situation.

Available for both project-based (contract) and advisory engagements. Start with a free 30-minute consult.

Also worth reading