La lección anterior dejó el mapa dibujado: el artefacto reservalia/api:a3f9c21 tiene que recorrer devstagingprod sin que nadie abra la consola de AWS. Ahora vamos a escribir el fichero que lo hace. Al terminar esta lección, Reservalia tendrá su segundo workflow —.github/workflows/cd.yml— que se dispara solo cuando el CI publica un artefacto nuevo, despliega a dev de forma automática, comprueba con un smoke test que lo desplegado es exactamente lo que creíamos, y continúa hacia staging. Veremos cómo se encadenan dos workflows, qué son los entornos de GitHub y sus reglas de protección, cómo se obtiene acceso a AWS sin guardar ni una sola clave, y dónde vive la configuración de cada entorno, que es la pregunta que más despliegues rompe.

Contenido

  1. Qué es exactamente "desplegar" en ECS Fargate
  2. Encadenar CI y CD: workflow_run frente a workflow_call
  3. Entornos de GitHub: reglas de protección y revisores requeridos
  4. Autenticación con AWS por OIDC, sin claves de larga vida
  5. Desplegar la API: definición de tarea con el digest exacto
  6. Configuración por entorno: el artefacto es igual, la configuración cambia
  7. Comprobaciones posteriores al despliegue: estabilización y smoke tests
  8. Idempotencia y reintentos
  9. Desplegar la web: S3, CloudFront e invalidación de caché
  10. El cd.yml de Reservalia, de principio a fin
  11. Errores Comunes y Consejos
  12. Ejercicios
  13. Conclusión

  1. Qué es exactamente "desplegar" en ECS Fargate

En ECS Fargate, "desplegar" no es copiar ficheros a un servidor: es cambiar una declaración y dejar que el orquestador la haga realidad. Tres piezas:

Pieza Qué es Analogía
Task definition JSON versionado: imagen, CPU, memoria, variables, secretos, puerto La receta
Task Una ejecución concreta de esa receta: un contenedor corriendo El plato servido
Service Mantiene N tasks vivas, las registra en el ALB y las sustituye al cambiar la receta El cocinero

Desplegar son tres pasos: registrar una revisión nueva de la task definition con la imagen nueva; decirle al service que la use; y esperar a que haya sustituido las tasks viejas sin que el ALB deje de recibir tráfico. Cómo se sustituyen exactamente es la estrategia de despliegue, contenido de la 03-04; aquí usamos el comportamiento por defecto de ECS, que ya es un rolling update razonable.

  1. Encadenar CI y CD: workflow_run frente a workflow_call

El ci.yml de Reservalia acaba publicando en ECR. ¿Cómo arranca el despliegue a partir de ahí? GitHub Actions ofrece dos mecanismos y la elección tiene consecuencias.

workflow_run workflow_call
Cómo funciona El CD escucha "el workflow CI ha terminado" El CI llama al CD como una función
Visibilidad en la UI Dos ejecuciones separadas Una sola ejecución con todos los jobs
Paso de datos Vía outputs del run anterior o artefactos Con inputs: y secrets: explícitos
Reintento manual Se puede relanzar solo el CD Hay que relanzar todo
Trampa conocida Solo funciona si el workflow está en la rama por defecto Ninguna relevante

Reservalia elige workflow_run: quiere poder relanzar solo el despliegue cuando falla por un problema transitorio de AWS, sin repetir cuatro minutos de pruebas, y quiere un historial de despliegues legible en lugar de jobs perdidos dentro de ejecuciones de CI. (Los workflows reutilizables con workflow_call se tratan a fondo en la 04-05.)

# .github/workflows/cd.yml
name: CD

on:
  workflow_run:
    workflows: [CI]              # 1 · el name: del otro workflow, no su fichero
    types: [completed]
    branches: [main]             # 2 · solo los CI que corrieron sobre main

permissions: { id-token: write, contents: read }                # 3
concurrency: { group: cd-main, cancel-in-progress: false }      # 4 y 5
env: { TZ: Europe/Madrid, AWS_REGION: eu-west-1 }
  1. workflows: [CI] referencia el name: del otro workflow, no el nombre del fichero: fuente clásica de confusión.
  2. branches: [main] filtra por la rama sobre la que corrió el CI; sin esto, cualquier CI de un PR intentaría desplegar.
  3. permissions: id-token: write habilita el token OIDC del apartado 4 y contents: read basta para el checkout.
  4. concurrency con group fijo por rama impide que dos despliegues se pisen: si Diego fusiona dos PR seguidos, el segundo espera.
  5. cancel-in-progress: false es crítico y opuesto a lo que hacíamos en el CI. Cancelar un despliegue a medias deja el servicio con la mitad de las tasks en cada versión y nadie vigilando.

Y falta una comprobación esencial: workflow_run se dispara también cuando el CI ha fallado, así que cada job necesita if: github.event.workflow_run.conclusion == 'success'. Olvidarlo es el error número uno de este patrón: desplegar el resultado de un pipeline rojo.

  1. Entornos de GitHub: reglas de protección y revisores requeridos

Un environment de GitHub es más que una etiqueta: es donde viven los secretos y variables de un entorno concreto y donde se configuran las reglas que un despliegue debe cumplir. Reservalia crea tres en Settings → Environments:

Regla dev staging prod
Required reviewers Marta o Nuria
Deployment branches main main main
Variables propias ENTORNO=dev ENTORNO=staging ENTORNO=prod
Secretos propios ARN del rol dev ARN del rol staging ARN del rol prod
URL api-dev.reservalia.com api-staging.reservalia.com api.reservalia.com

Lo que los hace algo más que documentación:

  • Un job con environment: prod se queda pausado hasta que un revisor aprueba. Esa es, literalmente, la puerta manual de la lección anterior; quitarla el día que Reservalia pase a Despliegue Continuo será borrar la lista de revisores.
  • Los secretos son de ámbito de entorno. Un job con environment: dev no puede leer el ARN del rol de prod, aunque el secreto exista en el repositorio. Es la barrera contra el error de copiar y pegar.
  • deployment branches impide desplegar prod desde otra rama, y GitHub registra un historial por entorno con quién aprobó y qué commit se desplegó: auditoría gratis.

  1. Autenticación con AWS por OIDC, sin claves de larga vida

La forma tentadora de dar permisos al pipeline es crear un usuario IAM y guardar AWS_ACCESS_KEY_ID y AWS_SECRET_ACCESS_KEY como secretos. Es mala idea: son credenciales permanentes, hay que rotarlas a mano y, si se filtran en un log, siguen valiendo mañana. OIDC (OpenID Connect) invierte el modelo: GitHub emite en cada ejecución un token JWT de corta duración que dice "soy el workflow cd.yml del repositorio reservalia/reservalia, en la rama main, con el entorno prod", y AWS —que confía en GitHub como proveedor de identidad— entrega credenciales temporales de una hora si esas afirmaciones encajan con la política de confianza del rol.

{
  "Effect": "Allow",
  "Principal": { "Federated": "arn:aws:iam::…:oidc-provider/token.actions.githubusercontent.com" },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": { "StringEquals": {
      "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
      "token.actions.githubusercontent.com:sub": "repo:reservalia/reservalia:environment:prod" } }
}

La línea decisiva es la del sub: este rol solo lo puede asumir un job de ese repositorio que declare environment: prod. Un fork, otro repositorio o un job sin entorno reciben un AccessDenied. Reservalia tiene tres roles —reservalia-deploy-dev, reservalia-deploy-staging y reservalia-deploy-prod— cada uno con su condición y con permisos solo sobre los recursos de su entorno. Un error frecuente es usar comodines (repo:reservalia/reservalia:*): eso permite asumir el rol de producción desde cualquier rama y cualquier workflow, incluido uno que alguien añada en un pull request.

  1. Desplegar la API: definición de tarea con el digest exacto

  desplegar-dev:
    runs-on: ubuntu-22.04
    timeout-minutes: 20
    if: github.event.workflow_run.conclusion == 'success'
    environment: { name: dev, url: 'https://api-dev.reservalia.com' }   # 1
    steps:
      - uses: actions/checkout@v4
        with: { ref: '${{ github.event.workflow_run.head_sha }}' }      # 2
      - id: meta
        run: echo "sha=$(git rev-parse --short=7 HEAD)" >> $GITHUB_OUTPUT

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_DEPLOY }}                # 3
          aws-region: ${{ env.AWS_REGION }}

      - name: Resolver el digest de la imagen
        id: imagen
        run: |                                                          # 4
          DIGEST=$(aws ecr describe-images --repository-name reservalia/api \
            --image-ids imageTag=${{ steps.meta.outputs.sha }} \
            --query 'imageDetails[0].imageDigest' --output text)
          echo "uri=${{ vars.ECR_REGISTRY }}/reservalia/api@${DIGEST}" >> $GITHUB_OUTPUT
  1. url: muestra un enlace al entorno desplegado en la interfaz de GitHub.
  2. ref: head_sha es imprescindible con workflow_run: por defecto el checkout traería la rama por defecto en su estado actual, que puede ser ya otro commit. Queremos el commit exacto que produjo el artefacto.
  3. secrets.AWS_ROLE_DEPLOY se resuelve al secreto del entorno dev, así que el mismo texto YAML asume un rol distinto en cada job. Es el patrón que evita duplicar código por entorno.
  4. Resolvemos el digest, no usamos la etiqueta: a3f9c21 es inmutable por convención, sha256:… lo es por criptografía. Es "construir una vez, promocionar el mismo digest" aplicado literalmente.

Con el URI resuelto se registra la revisión nueva de la task definition y se actualiza el servicio:

      - uses: aws-actions/amazon-ecs-render-task-definition@v1
        id: taskdef
        with:
          task-definition: infra/ecs/taskdef-api.json     # 5 · plantilla versionada
          container-name: api
          image: ${{ steps.imagen.outputs.uri }}

      - uses: aws-actions/amazon-ecs-deploy-task-definition@v2
        with:
          task-definition: ${{ steps.taskdef.outputs.task-definition }}
          service: reservalia-api                          # 6
          cluster: reservalia-dev
          wait-for-service-stability: true                 # 7
          wait-for-minutes: 10
  1. La plantilla infra/ecs/taskdef-api.json está versionada en el repositorio: CPU, memoria, puertos, variables y referencias a secretos. La acción sustituye únicamente el campo image, así que el despliegue es reproducible y revisable en un pull request en lugar de depender del estado que hubiera en AWS.
  2. service y cluster: Reservalia usa el mismo nombre de servicio en los tres entornos y clústeres distintos (reservalia-dev, reservalia-staging, reservalia-prod).
  3. wait-for-service-stability convierte "he lanzado un despliegue" en "el despliegue ha terminado". Sin ella el job saldría verde en segundos mientras ECS todavía arranca contenedores que quizá fallan.

  1. Configuración por entorno: el artefacto es igual, la configuración cambia

Ya apareció en la 02-06 como corolario de la inmutabilidad: si el artefacto es el mismo en los tres entornos, no puede contener nada específico de un entorno. Construir una imagen reservalia/api:a3f9c21-prod distinta de la de staging destruye la cadena entera, porque lo que llegaría a producción sería un binario nunca probado. La configuración entra desde fuera, en el arranque, y es de dos clases:

Configuración no sensible Secretos
Ejemplos ENTORNO, LOG_NIVEL, PUERTO DATABASE_URL, clave de la pasarela de pago
Dónde vive Task definition, bloque environment Secrets Manager, referenciado desde secrets
Quién la ve Cualquiera con lectura sobre ECS Solo la task en ejecución, y queda auditado
Se rota Con un despliegue Sin desplegar

El fragmento relevante de infra/ecs/taskdef-api.json:

{
  "family": "reservalia-api",
  "containerDefinitions": [{
    "name": "api",
    "image": "SUSTITUIDO_POR_EL_PIPELINE",
    "environment": [
      { "name": "ENTORNO", "value": "dev" },
      { "name": "LOG_NIVEL", "value": "debug" },
      { "name": "TZ", "value": "Europe/Madrid" }
    ],
    "secrets": [
      { "name": "DATABASE_URL",
        "valueFrom": "arn:aws:secretsmanager:eu-west-1:…:secret:reservalia/dev/api:DATABASE_URL::" }
    ]
  }]
}

La diferencia entre ambos bloques es sustancial. environment es texto plano visible en la consola de AWS y en cualquier describe-task-definition. secrets son referencias: ECS resuelve el valor en el arranque contra Secrets Manager usando el rol de ejecución de la tarea, y el valor nunca aparece en la definición ni en los logs del pipeline. Rotar la contraseña de la base de datos es cambiarla en Secrets Manager y reiniciar el servicio: no hay que reconstruir ni redesplegar nada. Y una regla operativa que ahorra incidentes: la aplicación debe fallar al arrancar si falta una variable obligatoria, en lugar de tirar de un valor por defecto silencioso. Mejor que el despliegue se caiga en dev con un mensaje claro que arrancar en prod apuntando a la base de datos equivocada.

  1. Comprobaciones posteriores al despliegue: estabilización y smoke tests

wait-for-service-stability responde a "¿ha terminado ECS?", no a "¿funciona?": un contenedor puede estar corriendo y contestar 500 a todo. El smoke test cubre esa diferencia, y no se limita a comprobar que la API responde: verifica que responde la versión que acabamos de desplegar. Es el uso práctico del endpoint /version de la 02-06.

      - name: Smoke test
        env: { HOST: 'https://api-dev.reservalia.com', ESPERADO: '${{ steps.meta.outputs.sha }}' }
        run: |
          for intento in $(seq 1 10); do                       # 1
            DESPLEGADO=$(curl -sf --max-time 5 "$HOST/version" | jq -r '.commit' || echo "")
            if [ "$DESPLEGADO" = "$ESPERADO" ]; then
              echo "OK: $HOST está sirviendo $DESPLEGADO"; break
            fi
            echo "Intento $intento: veo '$DESPLEGADO', espero '$ESPERADO'"
            sleep 10
            [ "$intento" = "10" ] && { echo "::error::el despliegue no llegó"; exit 1; }
          done

          curl -sf --max-time 5 "$HOST/salud" > /dev/null      # 2
          curl -sf --max-time 5 "$HOST/api/negocios/demo/huecos?fecha=2026-03-10" > /dev/null   # 3
  1. Se reintenta, porque el DNS y el ALB tardan unos segundos en dirigir todo el tráfico a las tasks nuevas. Un smoke test sin reintentos es una prueba flaky que enseñará al equipo a ignorar el rojo. Diez intentos de diez segundos dan 100 s de margen, muy por debajo del timeout-minutes: 20 del job.
  2. /salud comprueba las dependencias, no solo el proceso; volveremos sobre la diferencia en la 03-04.
  3. Una ruta de negocio real que consulta la base de datos detecta el caso clásico de "la aplicación arranca pero no tiene permisos sobre RDS". En conjunto, un smoke test debe ser rápido, estable y cubrir el camino crítico: no es la suite de pruebas, es la comprobación de que el despliegue ha surtido efecto y de que lo esencial responde.

  1. Idempotencia y reintentos

Un despliegue automatizado se relanzará: por un fallo de red, porque alguien pulsa Re-run, porque dos merges seguidos disparan dos ejecuciones. La propiedad que lo hace seguro es la idempotencia: ejecutarlo dos veces deja el sistema igual que ejecutarlo una.

Operación ¿Idempotente? Por qué
update-service con el mismo digest El estado deseado ya coincide; ECS no hace nada
Invalidación de CloudFront Se puede repetir sin efecto adicional
INSERT en la tabla despliegues No Duplica filas y contamina las métricas DORA
Migración ALTER TABLE … ADD COLUMN No, salvo IF NOT EXISTS Falla la segunda vez
Enviar un correo a los negocios No Lo reciben dos veces

Tres reglas prácticas: usar operaciones declarativas ("quiero este digest") y no imperativas ("añade una instancia"); insertar en despliegues con ON CONFLICT DO NOTHING sobre un identificador derivado del run; y proteger con concurrency, como hicimos en el apartado 2. Sobre los reintentos: son correctos para fallos transitorios (timeout de red, ThrottlingException) y peligrosos para fallos deterministas —reintentar diez veces un despliegue al que le falta una variable solo produce diez fallos idénticos—. La regla: reintenta la comprobación, no la decisión.

  1. Desplegar la web: S3, CloudFront e invalidación de caché

apps/web no es un contenedor: son ficheros estáticos generados por Vite que se suben a S3 y se sirven por CloudFront. Su sutileza propia es la caché.

  desplegar-web-dev:
    needs: [desplegar-dev]
    environment: dev
    steps:
      # checkout(head_sha) · setup-node(.nvmrc) · npm ci · npm run build --workspace apps/web
      # · credenciales OIDC
      - name: Assets con hash — caché larga                    # 1
        run: aws s3 sync apps/web/dist/assets s3://reservalia-web-dev/assets
             --cache-control "public,max-age=31536000,immutable"

      - name: HTML — sin caché                                 # 2
        run: aws s3 sync apps/web/dist s3://reservalia-web-dev
             --exclude "assets/*" --delete --cache-control "no-cache"

      - name: Invalidar CloudFront                             # 3
        run: aws cloudfront create-invalidation
             --distribution-id ${{ vars.CLOUDFRONT_DIST_ID }} --paths "/index.html" "/"
  1. Los assets van primero y con caché de un año: Vite les pone un hash en el nombre (indice-4f3c21a9.js), así que cada build genera ficheros nuevos y nunca hay que invalidarlos.
  2. El HTML va después y con no-cache. El orden importa: subir el index.html nuevo antes que los assets deja unos segundos de usuarios pidiendo un JavaScript que aún no existe. El --delete borra lo obsoleto pero excluye assets/*, para no romper a quien tenga cargado el HTML anterior.
  3. La invalidación solo afecta al HTML. Invalidar /* funciona, pero es lento y AWS solo regala 1.000 rutas al mes.

  1. El cd.yml de Reservalia, de principio a fin

Con la cabecera del apartado 2, el workflow tiene tres jobs: desplegar-dev (~3 min: checkout del head_sha, OIDC, digest, taskdef, deploy y smoke), desplegar-web-dev (needs: [desplegar-dev], build de Vite, sync a S3 e invalidación) y desplegar-staging (~4 min, needs los dos anteriores, environment: staging, el mismo digest sobre el clúster reservalia-staging, con smoke y las pruebas E2E críticas de la 02-04).

flowchart LR
    CI["ci.yml verde en main<br/>publica a3f9c21"] --> W["workflow_run"]
    W --> D["desplegar-dev<br/>~3 min"]
    D --> S1{"smoke /version"}
    S1 -- ok --> WD["desplegar-web-dev"]
    WD --> ST["desplegar-staging<br/>mismo digest"]
    ST --> S2{"smoke + E2E"}
    S2 -- ok --> P["prod · pendiente<br/>03-04"]
    S1 -- falla --> X["workflow rojo<br/>rollback en 03-05"]

Con la perspectiva del módulo 1: desde que Diego fusiona un PR hasta que el cambio está en staging pasan unos once minutos —cuatro de CI, tres de dev, cuatro de staging— sin que nadie escriba un comando; el ritual del viernes duraba tres horas y solo llegaba a un entorno. Falta prod, que se añade en la 03-04 con una estrategia progresiva, y falta qué ocurre cuando el smoke test falla: hoy el workflow se pone rojo y dev se queda con la versión rota. Ese hueco lo tapa la 03-05.

Errores Comunes y Consejos

Error 1: olvidar if: github.event.workflow_run.conclusion == 'success'. workflow_run se dispara también cuando el CI falla; sin ese filtro despliegas el resultado de un pipeline rojo. Error 2: hacer checkout sin ref: head_sha, con lo que despliegas la imagen de un commit y ejecutas los scripts de otro.

Error 3: guardar claves de AWS de larga vida como secretos. No caducan, hay que rotarlas a mano y si se filtran siguen sirviendo. Con OIDC no hay nada que filtrar.

Error 4: hornear la configuración en la imagen. Una imagen por entorno significa que lo que llega a producción es un artefacto que nunca se probó.

Error 5: dar por bueno el despliegue sin verificar la versión. El pipeline verde solo dice que el comando no dio error; si /version sigue devolviendo el SHA anterior, no has desplegado nada. Error 6: cancel-in-progress: true en el CD, que deja el servicio con dos versiones conviviendo y nadie vigilando.

Consejo 1: versiona la definición de tarea en el repositorio, para que un cambio de memoria o de variable pase por pull request. Consejo 2: despliega por digest, no por etiqueta. Consejo 3: mide el tiempo de cada despliegue y trátalo como presupuesto: uno que crece de 3 a 12 minutos frena todo lo que viene detrás.

Ejercicios

Ejercicio 1

El despliegue a dev de Reservalia sale verde, pero https://api-dev.reservalia.com/version sigue devolviendo b7e2d10 en lugar de a3f9c21. Enumera cuatro causas posibles y cómo distinguirlas.

Ejercicio 2

Un compañero propone: "En vez de Secrets Manager, pasemos DATABASE_URL como secreto de GitHub y lo inyectamos en la task definition durante el despliegue". Explica tres problemas concretos de ese diseño.

Ejercicio 3

Diego fusiona dos pull requests con dos minutos de diferencia. Describe qué ocurre con la configuración de concurrency mostrada, qué ocurriría con cancel-in-progress: true y qué ocurriría sin concurrency ninguna.

Soluciones

Solución 1. Cuatro causas y cómo distinguirlas:

  1. Se desplegó la imagen equivocada. Se comprueba con aws ecs describe-task-definition mirando el campo image: dice qué digest se registró de verdad.
  2. Las tasks nuevas no arrancaron y ECS revirtió solo. Aparece en aws ecs describe-services --query 'services[0].events' como tasks que nacen y mueren; casi siempre falta una variable o no puede leer un secreto.
  3. El ALB aún manda tráfico a las tasks antiguas porque no ha vencido el deregistration delay. Se reconoce porque /version alterna entre los dos SHA: es intermitente, no constante. Se resuelve reintentando, como hace el smoke test.
  4. La imagen se construyó sin build-arg COMMIT_SHA, así que el endpoint miente aunque el código sea el nuevo. Si describe-task-definition muestra el digest correcto pero /version no, el problema está en el build, no en el despliegue.

Solución 2. Tres problemas: (1) rotación acoplada al despliegue —cambiar la contraseña exige editar un secreto de GitHub y lanzar un despliegue completo, cuando con Secrets Manager basta con reiniciar el servicio—; (2) el secreto queda escrito en la task definition, un documento legible por cualquiera con permisos sobre ECS y conservado en todas sus revisiones; (3) mayor superficie de exposición: el valor pasa por el runner, donde un set -x o una acción de terceros comprometida pueden verlo, mientras que con valueFrom el runner solo conoce un ARN y cada lectura queda auditada en CloudTrail.

Un cuarto argumento, menos obvio: si el secreto solo existe en GitHub, la aplicación no puede arrancar fuera del pipeline. La configuración pertenece al entorno, no al sistema que despliega.

Solución 3. Con la configuración mostrada, el primero se ejecuta entero y el segundo espera en la cola: dos despliegues consecutivos, cada uno completo y verificado. Con cancel-in-progress: true, el segundo cancelaría el primero a media ejecución; en el peor momento —durante el rolling update— ECS queda con tasks de dos versiones, sin nadie esperando la estabilización ni ejecutando el smoke test. Y sin concurrency ambos correrían en paralelo sobre el mismo servicio: el estado final depende del orden en que lleguen las llamadas a la API de AWS, así que es posible que el segundo acabe antes y el servicio quede con el commit más antiguo, con los dos pipelines en verde. Es la peor opción, porque el fallo es silencioso.

Conclusión

Reservalia ya tiene despliegue automatizado hasta staging. El artefacto reservalia/api:a3f9c21 sale de ECR, se despliega en dev en unos tres minutos, se verifica con un smoke test contra /version y continúa hasta staging con el mismo digest, sin reconstruir nada. La web viaja en paralelo hacia S3 y CloudFront con su invalidación de caché.

Las decisiones que sostienen el diseño: workflow_run para encadenar CI y CD manteniendo ejecuciones separadas y relanzables, con el filtro obligatorio sobre conclusion == 'success'; los entornos de GitHub como lugar donde viven secretos, variables y reglas de protección, de modo que el mismo YAML asume un rol distinto en cada entorno; OIDC para obtener credenciales temporales sin guardar claves; el despliegue por digest con una task definition versionada; la separación entre configuración no sensible y secretos, con Secrets Manager resolviendo los segundos en el arranque; y las comprobaciones posteriores —estabilización más smoke test— que distinguen "he lanzado un despliegue" de "el despliegue funciona".

Queda una fragilidad de fondo. Todo este workflow da por hecho que existen un clúster reservalia-dev, un servicio reservalia-api, una base de datos RDS, un ALB y unos roles IAM… y esos recursos los creó Nuria a mano en la consola de AWS hace meses. Nadie sabe con certeza en qué se diferencia staging de prod, porque la única fuente de verdad es lo que hay dentro de AWS. Es el requisito 3 de la lección anterior —entornos equivalentes— y sigue sin cumplirse.

La siguiente lección, Infraestructura como Código y Entornos Reproducibles, ataca justo eso: por qué un despliegue automatizado sobre infraestructura artesanal sigue siendo frágil, qué es la deriva de configuración, y cómo el módulo infra/ de Reservalia se escribe una sola vez en Terraform y se instancia tres veces para que dev, staging y prod salgan literalmente del mismo código.

Curso de CI/CD: Integración y Despliegue Continuo

Módulo 1: Introducción a CI/CD

Módulo 2: Integración Continua (CI)

Módulo 3: Despliegue Continuo (CD)

Módulo 4: Prácticas Avanzadas de CI/CD

Módulo 5: Implementación de CI/CD en Proyectos Reales

Módulo 6: Herramientas y Tecnologías

Módulo 7: Ejercicios Prácticos

Módulo 8: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados