🚀 Zero Downtime Deployment con Azure App Service

🚀 Zero Downtime Deployment con Azure App Service

Introducción

Con Astro podemos generar un sitio estático que se despliega en Azure App Service y vuela: rendimiento top, métricas de Lighthouse cercanas al 100, y con Content Island podemos modificar el contenido de forma amigable.

Además, podemos conectar un custom webhook de GitHub para desplegar automáticamente cada vez que hacemos push a la rama main.

Dentro de Azure App Service, podemos crear dos tipos diferentes de aplicaciones:

  • Static Web App: para sitios estáticos, ideal para Astro.
  • Web App: para aplicaciones más complejas, donde además de servir archivos estáticos, podemos ejecutar código del lado del servidor (como una REST API).

En este post nos centraremos en Web App donde se conteneriza la aplicación frontend y una REST API en un único contenedor Docker que se despliega en Azure App Service con un sistema operativo Linux.

Pero hay un problema:

👉 Cada vez que hacemos un cambio en el sitio, el despliegue puede causar downtime, porque Azure App Service, tiene que levantar un contenedor en la instancia de producción, y eso puede hacer que nuestro sitio esté caído durante 30 / 60 segundos, y esto da mala imagen de cara a nuestros clientes.

Entonces, ¿cómo evitamos este corte?

La respuesta: Azure App Service Slots.

En este post vamos a partir de un workflow de despliegue automático con GitHub Actions y veremos cómo implementar Slots en Azure para tener zero downtime deployment.

⚡ TL;DR

Los slots de Azure App Service permiten desplegar en un entorno de staging. Una vez que todo esté listo (incluso con warmup si hace falta), hacemos un swap con el entorno de producción. Resultado: despliegue sin downtime.

🧨 El problema

Tenemos un webhook que lanza un despliegue cada vez que se actualiza el contenido en Content Island. Este webhook ejecuta el siguiente workflow de GitHub Actions:

name: Deploy

on:
  push:
    branches:
      - main

env:
  IMAGE_NAME: ghcr.io/${{github.repository}}:${{github.run_number}}-${{github.run_attempt}}

permissions:
  contents: "read"
  packages: "write"
  id-token: "write"

jobs:
  cd:
    runs-on: ubuntu-latest
    environment:
      name: Production
      url: https://my-app.com
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Log in to GitHub container registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push docker image
        run: |
          docker build \
          -t ${{ env.IMAGE_NAME }} .
          docker push ${{env.IMAGE_NAME}}

      - name: Login Azure
        uses: azure/login@v2
        env:
          AZURE_LOGIN_PRE_CLEANUP: true
          AZURE_LOGIN_POST_CLEANUP: true
          AZURE_CORE_OUTPUT: none
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Deploy to Azure
        uses: azure/webapps-deploy@v3
        with:
          app-name: ${{ secrets.AZURE_APP_NAME }}
          images: ${{env.IMAGE_NAME}}

¿Qué hace este workflow?

  1. Clona el repositorio
uses: actions/checkout@v4
  1. Inicia sesión en GitHub Container Registry
uses: docker/login-action@v3
  1. Construye y sube la imagen Docker
docker build ...
docker push ...
  1. Login Azure
uses: azure/login@v2

Más info: Documentación de Azure Login

  1. Despliega en Azure App Service
uses: azure/webapps-deploy@v3

❌ ¿Dónde está el problema?

Azure App Service no tiene un mecanismo nativo para swaps sin interrupciones en una única instancia.
Cuando haces un nuevo despliegue:

  • Se detiene el contenedor anterior.
  • Se descarga la nueva imagen del registry al que estemos apuntando (en este caso, GitHub Container Registry).
  • Se inicia un nuevo contenedor con la nueva imagen.
  • Y durante ese proceso… el sitio esta caído unos segundos o incluso minutos.

Flujo slots: 1. clonar repo, 2. login a azure, 3. login github container registry, 4. build docker image y push a github registry, 5. Deploy to azure (production), 6. servidor caido, 7.  serivdor ok

Esto se traduce en mala experiencia de usuario.

✅ La solución: Azure App Service Slots

Azure ofrece una funcionalidad llamada Deployment Slots: entornos adicionales donde puedes desplegar versiones previas de tu app sin afectar producción.

🔢 Algunos datos:

  • Disponible en planes Premium y superiores (approx.: a partir de 56 € mes), tabla comprativa.
  • Puedes tener hasta 20 slots por App Service en una Premium, en teoría puedes desplegar hasta 20 aplicaciones con su slot de staging en ese plan (claro aquí aplican limitaciones de recursos).
  • Slots están disponibles tanto en Windows como Linux.
  • En este ejemplo usaremos Linux (más económico, pero requiere un paso adicional).

🛠️ ¿Cómo lo hacemos?

Una primera aproximación con un deploy en staging y paso manual a producción:

  1. Crea un App Service asociado a un App Service plan Premium.

App Service con un App Service plan Premium

  1. Ve a "Deployment slots" y crea uno nuevo, por ejemplo: staging-my-site.

Nuevo deployment slot

  1. Ejecuta un Swap manual.

03-manual-swap.png

“Vale, esto está bien… pero no voy a hacer esto manualmente cada vez que haga un deploy.”

Y tienes toda la razón.

🧪 Automatizando el swap

Vamos a automatizar el swap con GitHub Actions.

  1. Modifica el workflow de GitHub Actions para desplegar en el slot de staging-my-site (en lugar de producción).

  2. Añade un texto o cambio visual para saber que esa versión es la nueva.

  3. Lanza el workflow.

  4. Comprueba que tienes dos versiones activas: una en producción y una en staging-my-site.

Resultado: zero downtime.

¿Cómo quedaría el workflow?

...

      - name: Deploy to Azure
        uses: azure/webapps-deploy@v3
        with:
          app-name: ${{ secrets.AZURE_APP_NAME }}
+         slot-name: staging-my-site
          images: ${{env.IMAGE_NAME}}

+     - name: Swap
+       run: |
+         az webapp deployment slot swap \
+         --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
+         --name ${{ secrets.AZURE_APP_NAME }} \
+         --slot staging-my-site \
+         --target-slot production

😬 El detalle

Si haces el swap justo después de desplegar en staging-my-site, puede que el contenedor aún no esté listo. Si swapéas demasiado pronto, vuelves a tener downtime.

¿Qué podemos hacer para evitar esto?

💰 Opción con coste: Windows

Si tiras del sistema operativo Windows en vez de Linux, hay un flag auto swap que hace que Azure espere a que el contenedor esté listo antes de hacer el swap.

Descartamos esto ya que implica un coste extra, y por otro lado dejamos de trabajar en Linux.

🕒 Opción simple: Espera fija

Una forma rápida (aunque no perfecta) es añadir una espera antes del swap:

...
      - name: Deploy to Azure
        uses: azure/webapps-deploy@v3
        with:
          app-name: ${{ secrets.AZURE_APP_NAME }}
          slot-name: staging-my-site
          images: ${{env.IMAGE_NAME}}

+      - name: Esperar antes del swap
+        run: sleep 120 # espera 2 minutos

      - name: Swap

✅ Fácil de implementar
❌ Puede fallar si el contenedor tarda más de lo esperado

🧠 Opción avanzada: Healthcheck + espera activa

Una opción más robusta es hacer un healthcheck antes del swap.

¿Cómo?

  1. En tu sitio añade un endpoint /api/health que devuelva 200 OK cuando el sitio esté listo.
  2. En el workflow de GitHub Actions, antes del swap:
    • Hace una petición a /api/health al slot de staging-my-site.
    • Espera hasta recibir un 200 OK.
    • Si falla, espera unos segundos y vuelve a reintentarlo.
    • Si todo OK, hace el swap.

Puedes hacerlo con un pequeño script en Bash o usar una acción de GitHub ya existente que lo hace por ti.

Sobre la acción de GitHub: al momento de escribir esto, no hay una acción oficial de Azure para hacer healthchecks, pero puedes usar una acción de terceros como jtalk/url-health-check-action@v1.2, en nuestro caso hemos preferido evitar utilizar acciones no oficiales, ya que a futuro pueden traer problemas, pero si hemos buceado en el código y hemos replicado la funcionalidad usando simplemente el comando curl.

...
      - name: Deploy to Azure
        uses: azure/webapps-deploy@v3
        with:
          app-name: ${{ secrets.AZURE_APP_NAME }}
          slot-name: staging-my-site
          images: ${{env.IMAGE_NAME}}

+     - name: Waiting for deployment to finish
+       run: |
+         sleep 30
+         curl \
+           --retry 20 \
+           --retry-delay 10 \
+           --retry-connrefused \
+           "${{ secrets.STAGING_HEALTH_CHECK_URL }}"

      - name: Swap

¿Qué hacemos aquí?

  • Primero esperamos 30 segundos para dar tiempo a que el contenedor se inicie (para evitar que haga una petición contra la versión antigua que está desplegada).
  • Luego hacemos una petición curl a la URL de healthcheck del slot de staging-my-site (por ejemplo: https://staging-my-site.com/api/health).
  • Si la petición falla, reintentamos hasta 20 veces con un retraso de 10 segundos entre intentos.
  • Si todo va bien, hacemos el swap.

🔄 Workflow completo con slots y espera

Visualmente sería:

Flujo slots: 1. clonar repo, 2. login a azure, 3. login github container registry, 4. build docker image y push a github registry, 5. Deploy to azure (staging), 6. esperar que te termine el deploy, 7.  hacer el swap de instancias

El detalle del workflow completo con slots y espera activa quedaría así:

name: Deploy

on:
  push:
    branches:
      - main

env:
  IMAGE_NAME: ghcr.io/${{github.repository}}:${{github.run_number}}-${{github.run_attempt}}

permissions:
  contents: "read"
  packages: "write"
  id-token: "write"

jobs:
  cd:
    runs-on: ubuntu-latest
    environment:
      name: Production
      url: https://my-app.com
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Log in to GitHub container registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push docker image
        run: |
          docker build \
          -t ${{ env.IMAGE_NAME }} .
          docker push ${{env.IMAGE_NAME}}

      - name: Login Azure
        uses: azure/login@v2
        env:
          AZURE_LOGIN_PRE_CLEANUP: true
          AZURE_LOGIN_POST_CLEANUP: true
          AZURE_CORE_OUTPUT: none
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Deploy to Azure
        uses: azure/webapps-deploy@v3
        with:
          app-name: ${{ secrets.AZURE_APP_NAME }}
          slot-name: staging-my-site
          images: ${{env.IMAGE_NAME}}

      - name: Waiting for deployment to finish
        run: |
          sleep 30
          curl \
            --retry 20 \
            --retry-delay 10 \
            --retry-connrefused \
            "${{ secrets.STAGING_HEALTH_CHECK_URL }}"

      - name: Swap
        run: |
          az webapp deployment slot swap \
          --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
          --name ${{ secrets.AZURE_APP_NAME }} \
          --slot staging-my-site \
          --target-slot production

Nota: Asegúrate de tener todos los secretos que hemos puesto con ${{ secrets.XXXX }} configurados en tu repositorio de GitHub:

  • AZURE_CLIENT_ID
  • AZURE_TENANT_ID
  • AZURE_SUBSCRIPTION_ID
  • AZURE_APP_NAME
  • AZURE_RESOURCE_GROUP
  • STAGING_HEALTH_CHECK_URL

Secretos de GitHub

💰 Costos y consideraciones

  • El plan Premium de Azure App Service parte desde approx. 56 €/mes puedes consultar los precios exactos en la calculadora de azure.
  • Incluye hasta 20 slots, por lo que puedes tener varias apps corriendo en paralelo.
  • Puedes:
    • Escalar verticalmente (más CPU, más RAM).
    • Escalar horizontalmente (más instancias).
  • Si haces un swap y algo sale mal, puedes hacer otro swap y volver a la versión anterior.

🧾 Conclusiones

El uso de Azure App Service Slots te permite:

✅ Desplegar sin downtime
✅ Tener entornos de staging listos para validar
✅ Volver atrás fácilmente en caso de error
✅ Mantener una experiencia profesional para tus usuarios

Todo esto sin complicarte con infraestructura de bajo nivel.