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:
- El runner no necesita acceso a internet para el registry — el build ocurre en ACR
- La imagen nunca pasa por la red del runner (más rápido para imágenes grandes)
- Sin autenticación Docker adicional —
az loginya autenticó
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.