Skip to content

06 - CI/CD with GitHub Actions

Automate the build and deployment of your .NET application so every commit produces a new Container App revision. This tutorial uses GitHub Actions, Azure Container Registry (ACR), and the Azure Container Apps deploy action.

Infrastructure Context

Service: Container Apps (Consumption) | Network: VNet integrated | VNet: ✅

This tutorial assumes a production-ready Container Apps deployment with a custom VNet, ACR with managed identity pull, and private endpoints for backend services.

flowchart TD
    INET[Internet] -->|HTTPS| CA["Container App\nConsumption\nLinux .NET 8"]

    subgraph VNET["VNet 10.0.0.0/16"]
        subgraph ENV_SUB["Environment Subnet 10.0.0.0/23\nDelegation: Microsoft.App/environments"]
            CAE[Container Apps Environment]
            CA
        end
        subgraph PE_SUB["Private Endpoint Subnet 10.0.2.0/24"]
            PE_ACR[PE: ACR]
            PE_KV[PE: Key Vault]
            PE_ST[PE: Storage]
        end
    end

    PE_ACR --> ACR[Azure Container Registry]
    PE_KV --> KV[Key Vault]
    PE_ST --> ST[Storage Account]

    subgraph DNS[Private DNS Zones]
        DNS_ACR[privatelink.azurecr.io]
        DNS_KV[privatelink.vaultcore.azure.net]
        DNS_ST[privatelink.blob.core.windows.net]
    end

    PE_ACR -.-> DNS_ACR
    PE_KV -.-> DNS_KV
    PE_ST -.-> DNS_ST

    CA -.->|System-Assigned MI| ENTRA[Microsoft Entra ID]
    CAE --> LOG[Log Analytics]
    CA --> AI[Application Insights]

    style CA fill:#107c10,color:#fff
    style VNET fill:#E8F5E9,stroke:#4CAF50
    style DNS fill:#E3F2FD

CI/CD Pipeline Flow

graph LR
    PUSH[Push to main] --> GHA[GitHub Actions]
    GHA --> DOTNET[dotnet publish]
    DOTNET --> ACR[ACR Build]
    ACR --> DEPLOY[Deploy Revision]
    DEPLOY --> HEALTH[Health Check]

Prerequisites

Never expose credentials in workflow logs

Keep all credential material in GitHub Secrets and avoid printing secret-derived values in shell steps. Use masked placeholders in documentation and workflow examples.

Step-by-step

  1. Configure repository variables and secrets

  2. Variables: RESOURCE_GROUP, APP_NAME, ACR_NAME

  3. Secrets: AZURE_CREDENTIALS, REGISTRY_USERNAME, REGISTRY_PASSWORD

Example AZURE_CREDENTIALS JSON (masked):

{
  "clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "clientSecret": "<client-secret>",
  "subscriptionId": "<subscription-id>",
  "tenantId": "<tenant-id>"
}
  1. Create the GitHub Actions workflow file

Save this as .github/workflows/deploy.yml:

name: Deploy .NET App to ACA

on:
  push:
    branches: [ main ]
  workflow_dispatch:

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'

      - name: Azure Login
        uses: azure/login@v2
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Build and Push to ACR
        uses: azure/docker-login@v2
        with:
          login-server: ${{ vars.ACR_NAME }}.azurecr.io
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}

      - name: Build container image
        run: |
          docker build -t ${{ vars.ACR_NAME }}.azurecr.io/${{ vars.APP_NAME }}:${{ github.sha }} ./apps/dotnet-aspnetcore
          docker push ${{ vars.ACR_NAME }}.azurecr.io/${{ vars.APP_NAME }}:${{ github.sha }}

      - name: Deploy to Container App
        uses: azure/container-apps-deploy-action@v1
        with:
          imageToDeploy: ${{ vars.ACR_NAME }}.azurecr.io/${{ vars.APP_NAME }}:${{ github.sha }}
          resourceGroup: ${{ vars.RESOURCE_GROUP }}
          containerAppName: ${{ vars.APP_NAME }}
  1. Add unit tests to the pipeline (Recommended)

Before building the container, ensure your .NET tests pass:

      - name: Run Tests
        run: dotnet test ./apps/dotnet-aspnetcore/AzureContainerApps.csproj --configuration Release
  1. Validate rollout behavior

    • Trigger the workflow by pushing a change to main.
    • Confirm a new revision was created in the Azure Portal or via CLI.
    • Confirm traffic moved to the healthy new revision.
    az containerapp revision list \
      --name "$APP_NAME" \
      --resource-group "$RESOURCE_GROUP" \
      --query "[].{name:name,active:properties.active,trafficWeight:properties.trafficWeight,healthState:properties.healthState}"
    

    ???+ example "Expected output"

    [
      {
        "name": "<your-app-name>--<sha>",
        "active": true,
        "trafficWeight": 100,
        "healthState": "Healthy"
      }
    ]
    

Advanced Topics

  • Environment-based deployments: Use GitHub Environments with protection rules for manual approval before deploying to production.
  • Bicep integration: Run az deployment group create within the workflow to ensure infrastructure stays in sync with code.
  • Canary releases: Use the azure/container-apps-deploy-action to deploy with 0% traffic initially, then gradually ramp up after verification.

Use immutable image tags

Always tag your images with ${{ github.sha }} or a semantic version. This ensures that every revision in Container Apps can be traced back to a specific commit in your repository.

See Also

Sources