🚀 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?
- Clona el repositorio
uses: actions/checkout@v4
- Inicia sesión en GitHub Container Registry
uses: docker/login-action@v3
- Construye y sube la imagen Docker
docker build ...
docker push ...
- Login Azure
uses: azure/login@v2
Más info: Documentación de Azure Login
- 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.
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:
- Crea un App Service asociado a un App Service plan Premium.
- Ve a "Deployment slots" y crea uno nuevo, por ejemplo:
staging-my-site
.
- Ejecuta un
Swap
manual.
“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.
Modifica el workflow de GitHub Actions para desplegar en el slot de staging-my-site (en lugar de producción).
Añade un texto o cambio visual para saber que esa versión es la nueva.
Lanza el workflow.
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?
- En tu sitio añade un endpoint
/api/health
que devuelva200 OK
cuando el sitio esté listo. - En el workflow de GitHub Actions, antes del swap:
- Hace una petición a
/api/health
al slot destaging-my-site
. - Espera hasta recibir un
200 OK
. - Si falla, espera unos segundos y vuelve a reintentarlo.
- Si todo OK, hace el swap.
- Hace una petición a
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 destaging-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:
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
💰 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.