# 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: 2026-06-26
- Author: 友田 陽大
- Tags: Azure, Container Apps, CI/CD, GitHub Actions, Bicep, DevOps, インフラ, セキュリティ
- URL: https://tomodahinata.com/en/blog/azure-container-apps-cicd-github-actions-oidc-bicep-blue-green-guide
- Category: Azure Container Apps in production
- Pillar guide: https://tomodahinata.com/en/blog/azure-container-apps-production-guide

## Key points

- With the official azure/container-apps-deploy-action, you can build & deploy from a Dockerfile, source, or an existing image. A new revision is created per commit.
- Authentication can be built with a service principal (AZURE_CREDENTIALS), but in production make it keyless CI/CD with OIDC (federated credentials) that places not a single long-lived secret.
- Image pull from ACR is recommended via a managed identity (AcrPull role). CI pushes and the app pulls, each with a least-privilege identity.
- Make the image tag unique with the Git commit SHA (no latch). Zero downtime in single-revision mode, and Blue/Green, canary, and instant rollback via traffic splitting in multiple mode.
- In production, declaratively with Bicep/Terraform. Use az containerapp up for learning and PoC, and IaC for production.

---

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](https://learn.microsoft.com/en-us/azure/container-apps/github-actions). I've run [keyless CI/CD with OIDC](/blog/github-actions-oidc-keyless-cicd-aws-gcp-guide) and [Blue/Green deployment on Fargate](/blog/aws-ecs-fargate-cicd-blue-green-codedeploy-github-actions-guide) 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](/blog/azure-container-apps-production-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](https://learn.microsoft.com/en-us/azure/container-apps/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](/blog/azure-container-apps-production-guide) 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:

```yaml
- 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.

```azurecli
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`.

```yaml
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](/blog/github-actions-oidc-keyless-cicd-aws-gcp-guide)).

---

## 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](https://learn.microsoft.com/en-us/azure/container-apps/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.

```azurecli
# アプリにシステム割当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 %.**

```bash
# 複数リビジョンモードに（一度だけ）
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](/blog/opentelemetry-observability-production-tracing-metrics-logs)) 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)

```bicep
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](/blog/azure-container-apps-production-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](/contact). For production operation overall, see the [Azure Container Apps production-operations guide](/blog/azure-container-apps-production-guide).
