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-actionaction 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) withaz 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 externalis the shortest learning command that does resource creation, image build, registry storage, and deployment in one go. Useupfor PoC and IaC for production.
CI/CD checklist
- Authentication is OIDC (federated credentials), keyless. Don't use
AZURE_CREDENTIALSlong-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.