azurecontainer-appscontainer-registrygithub-actionsdockerdevopscd

Deploy sin guardar secretos: WIF, Container Apps y rollback automático en GitHub Actions

Azure Container Apps es la plataforma de contenedores serverless de Azure. Abstracts Kubernetes, maneja escalado automático, gestiona revisiones para rollback sin downtime. Para la mayoría de workloads que no necesitan control total sobre el cluster, es la opción correcta.

Este post es el pipeline completo: desde el push a main hasta el contenedor corriendo en producción, con todas las partes que los tutoriales oficiales suelen omitir.

La arquitectura del pipeline

Push a main
  → Build Docker image
  → Tag con commit SHA + :latest
  → Push a Azure Container Registry
  → Deploy nueva revisión en Container Apps
  → Smoke test (esperar healthy)
  → Si falla: rollback automático a revisión anterior

Cada paso tiene un propósito. El tag con commit SHA permite trazar exactamente qué código está corriendo. El smoke test evita que el deployment se considere exitoso si el contenedor no arranca.

Autenticación con Workload Identity Federation

El antipatrón: guardar AZURE_CLIENT_SECRET en GitHub Secrets y rotar manualmente. El patrón correcto: Workload Identity Federation (WIF). GitHub Actions autentica con Azure sin secretos de larga duración.

Configuración (una vez, con az CLI):

# Crear service principal para GitHub Actions
az ad sp create-for-rbac --name "github-actions-deploy" \
  --role contributor \
  --scopes /subscriptions/<SUB_ID>/resourceGroups/<RG_NAME> \
  --sdk-auth

# Crear federated credential para el repo
az ad app federated-credential create \
  --id <APP_ID> \
  --parameters '{
    "name": "github-main",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:mi-org/mi-repo:ref:refs/heads/main",
    "audiences": ["api://AzureADTokenExchange"]
  }'

En GitHub Secrets: AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID. No hay CLIENT_SECRET que rotar.

El workflow completo

name: Deploy to Azure Container Apps

on:
  push:
    branches: [main]

permissions:
  id-token: write   # Requerido para WIF
  contents: read

env:
  REGISTRY: miorg.azurecr.io
  IMAGE_NAME: mi-servicio
  RESOURCE_GROUP: mi-rg
  APP_NAME: mi-container-app
  CONTAINER_APP_ENV: mi-environment

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Azure Login (WIF)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Get current revision (for rollback)
        id: current-revision
        run: |
          CURRENT=$(az containerapp revision list \
            --name $APP_NAME \
            --resource-group $RESOURCE_GROUP \
            --query "[?properties.active==\`true\`] | [0].name" \
            -o tsv)
          echo "revision=$CURRENT" >> $GITHUB_OUTPUT

      - name: Build and push Docker image
        run: |
          SHA=${{ github.sha }}
          SHORT_SHA=${SHA::7}

          az acr build \
            --registry ${{ env.REGISTRY }} \
            --image ${{ env.IMAGE_NAME }}:${SHORT_SHA} \
            --image ${{ env.IMAGE_NAME }}:latest \
            .

      - name: Deploy to Container Apps
        id: deploy
        run: |
          SHORT_SHA=${{ github.sha }}
          SHORT_SHA=${SHORT_SHA::7}

          az containerapp update \
            --name ${{ env.APP_NAME }} \
            --resource-group ${{ env.RESOURCE_GROUP }} \
            --image ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${SHORT_SHA} \
            --revision-suffix ${SHORT_SHA}

      - name: Wait for healthy revision
        id: smoke-test
        run: |
          SHORT_SHA=${{ github.sha }}
          SHORT_SHA=${SHORT_SHA::7}
          REVISION="${{ env.APP_NAME }}--${SHORT_SHA}"

          for i in {1..12}; do
            STATUS=$(az containerapp revision show \
              --name ${{ env.APP_NAME }} \
              --resource-group ${{ env.RESOURCE_GROUP }} \
              --revision $REVISION \
              --query "properties.healthState" -o tsv 2>/dev/null || echo "Unknown")

            echo "Attempt $i: $STATUS"

            if [ "$STATUS" = "Healthy" ]; then
              echo "Revision is healthy"
              exit 0
            fi

            sleep 10
          done

          echo "Revision did not become healthy in time"
          exit 1

      - name: Rollback on failure
        if: failure() && steps.smoke-test.outcome == 'failure'
        run: |
          PREV_REVISION="${{ steps.current-revision.outputs.revision }}"
          echo "Rolling back to $PREV_REVISION"

          az containerapp ingress traffic set \
            --name ${{ env.APP_NAME }} \
            --resource-group ${{ env.RESOURCE_GROUP }} \
            --revision-weight ${PREV_REVISION}=100

az acr build vs docker build + docker push

az acr build construye la imagen directamente en Azure, sin necesidad de Docker instalado en el runner. Ventajas:

Para builds complejos con cache, usar el buildkit de ACR:

az acr build \
  --registry $REGISTRY \
  --image $IMAGE_NAME:$TAG \
  --cache-from $REGISTRY/$IMAGE_NAME:cache \
  --build-arg BUILDKIT_INLINE_CACHE=1 \
  .

Revisiones y traffic splitting

Container Apps maneja revisiones — cada deploy crea una nueva revisión. El rollback es redireccionar el tráfico a la revisión anterior:

# Ver todas las revisiones
az containerapp revision list \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --query "[].{name:name, active:properties.active, created:properties.createdTime}" \
  -o table

# Canary: 90% a revisión estable, 10% a nueva
az containerapp ingress traffic set \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --revision-weight stable-revision=90 new-revision=10

# Rollback total a revisión anterior
az containerapp ingress traffic set \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --revision-weight previous-revision=100

En producción, el flujo de rollback automático en el workflow es suficiente para la mayoría de casos. El canary manual es útil cuando estás probando un cambio de comportamiento.

Variables de entorno y secretos en Container Apps

Las variables de configuración van en el containerapp, no en la imagen:

# Actualizar variable de entorno sin nuevo deploy de imagen
az containerapp update \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --set-env-vars "APP_ENV=production" "LOG_LEVEL=info"

# Secreto desde Key Vault
az containerapp secret set \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --secrets "db-password=keyvaultref:/subscriptions/.../secrets/db-password,identityref:/subscriptions/.../managedidentities/app-identity"

Las referencias a Key Vault permiten que el secreto se rote en Key Vault sin necesidad de redeployar la aplicación.

Monitoreo del deploy

El deploy es exitoso cuando el contenedor responde. Para verificarlo desde GitHub Actions:

- name: Verify deployment endpoint
  run: |
    URL="https://${{ env.APP_NAME }}.azurecontainerapps.io/health"
    for i in {1..5}; do
      HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" $URL)
      if [ "$HTTP_STATUS" = "200" ]; then
        echo "Health check passed: $HTTP_STATUS"
        exit 0
      fi
      echo "Health check attempt $i: $HTTP_STATUS"
      sleep 5
    done
    echo "Health check failed after 5 attempts"
    exit 1

El /health endpoint es la interfaz mínima que toda aplicación debe exponer — retorna 200 si está listo, 503 si no.

Lo que aprendí

WIF es no-negociable para producción. Secretos de larga duración en GitHub Secrets son un riesgo que no vale cuando WIF hace el trabajo gratis.

El rollback automático vale la complejidad adicional. Un deploy que falla sin rollback puede dejar la aplicación en un estado inconsistente. El smoke test + rollback es la diferencia entre un deployment que falla silencioso y uno que falla recuperable.

az acr build sobre docker build en CI. Para pipelines en Azure, el build remoto en ACR es más rápido y no requiere Docker instalado en el runner.

Las revisiones de Container Apps son el historial de deploys. Con tags de imagen basados en el commit SHA, puedes trazar exactamente qué código está en cada revisión y qué cambio introdujo un problema.

Volver al blog