MercadoFresco tiene ya las tres piezas: un repositorio gobernado, una construcción que verifica y produce artefactos, y un despliegue que sabe volver atrás solo. Y sin embargo, cuando Luis fusiona una PR sigue teniendo que acordarse de lanzar la construcción, copiar la ruta del ZIP, escribir create-deployment con el bucket y la clave correctos, esperar, mirar si fue bien, y repetirlo para el entorno siguiente. Ocho comandos, cuatro esperas y una secuencia que solo vive en su cabeza.

AWS CodePipeline es la orquestación que falta. Define el camino que recorre un cambio desde el commit hasta producción como una serie de etapas que se ejecutan en orden, cada una con sus acciones, con artefactos que fluyen de una a otra y con la garantía de que una etapa no empieza si la anterior no terminó bien. Lo que hoy es memoria de una persona pasa a ser una definición versionada que se ejecuta igual todas las veces.

Aviso de coste. CodePipeline V1 cuesta 1 USD por pipeline activo y mes —un pipeline es activo si tuvo alguna ejecución— con el primero gratis. V2 cobra 0,002 USD por minuto de acción, sin cuota fija, con 100 minutos gratis al mes. Para MercadoFresco, con unas 20 ejecuciones diarias, V2 sale a unos 4 USD al mes. A eso se suman los minutos de CodeBuild de 08-02 y el almacenamiento del bucket de artefactos, que crece rápido si nadie le pone ciclo de vida. Datos ficticios.

Contenido

  1. Entrega continua frente a despliegue continuo
  2. Estructura: pipeline, etapas, acciones, ejecuciones y transiciones
  3. Los artefactos y el bucket que los mueve
  4. El pipeline de MercadoFresco
  5. Tipos V1 y V2, disparadores y variables
  6. La acción de origen con CodeConnections
  7. Construcción, despliegue y acciones en paralelo con runOrder
  8. Aprobación manual: qué debe leer Marta en treinta segundos
  9. Entornos: desarrollo, preproducción y producción
  10. Pruebas de humo y puertas de calidad con una acción Lambda
  11. Reintentos, ejecuciones en cola y modo superpuesto
  12. Permisos: rol del pipeline, roles por acción y entre cuentas
  13. Notificaciones hacia correo y Slack
  14. El pipeline como código
  15. Coste y limpieza
  16. Errores comunes y consejos
  17. Ejercicios
  18. Conclusión

Entrega continua frente a despliegue continuo

Los dos términos se confunden constantemente y la diferencia es una sola cosa: si hay o no una persona entre la validación y producción.

Aspecto Entrega continua Despliegue continuo
Qué automatiza Todo hasta la puerta de producción Todo, producción incluida
Quién decide que salga Una persona aprueba Nadie: si pasa las pruebas, sale
Requisito previo Pipeline fiable Pipeline fiable y pruebas en las que confías
Tiempo hasta producción Minutos + espera humana Minutos
Riesgo por despliegue Igual Igual, pero más despliegues y más pequeños
Cuándo elegirlo Al empezar; cambios sensibles Cuando la reversión automática está probada

Hay una trampa habitual: pensar que la aprobación manual reduce el riesgo. No lo reduce por sí sola. Lo reduce solo si quien aprueba tiene información suficiente para decidir; si Marta pulsa «aprobar» sin mirar nada, la aprobación es una fricción que retrasa el despliegue sin filtrar nada. Y hay un efecto secundario perverso: las aprobaciones acumulan cambios —«ya que apruebo, que vayan los tres juntos»— y un despliegue de tres cambios es más difícil de diagnosticar que tres despliegues de uno.

La decisión de MercadoFresco: entrega continua, con aprobación manual antes de producción, y con fecha de revisión. Marta lo plantea explícitamente como una etapa transitoria: la aprobación se mantiene hasta que se cumplan tres condiciones —que las pruebas de humo cubran el flujo de compra completo, que la reversión automática se haya probado a propósito al menos tres veces, y que pasen dos meses sin un despliegue que haya que revertir a mano—. Cumplidas las tres, la aprobación desaparece. Poner condiciones de salida a un control manual es lo que impide que se quede para siempre «por si acaso».

Estructura: pipeline, etapas, acciones, ejecuciones y transiciones

Concepto Qué es Regla que hay que recordar
Pipeline El flujo completo Un pipeline por aplicación desplegable
Etapa Un grupo de acciones con nombre Solo empieza si la anterior tuvo éxito
Acción Una unidad de trabajo Seis tipos: origen, construcción, prueba, despliegue, aprobación, invocación
Ejecución Un recorrido concreto Identificada, con su commit y sus artefactos
Transición El paso entre dos etapas Se puede deshabilitar para retener cambios
Artefacto Lo que fluye entre acciones Va por S3, no en memoria

Dos matices que ahorran sorpresas. Dentro de una etapa, las acciones con el mismo runOrder se ejecutan en paralelo, y las de runOrder mayor esperan a las anteriores; es el mecanismo para paralelizar sin crear etapas nuevas. Y una transición deshabilitada es una herramienta operativa poco conocida y muy útil: durante el pico del viernes, Marta deshabilita la transición hacia producción y los cambios se acumulan validados en preproducción, listos para salir el lunes con un clic.

Los artefactos y el bucket que los mueve

Cada acción puede declarar artefactos de entrada y de salida. CodePipeline los guarda comprimidos en un bucket de S3 y cada acción recibe los suyos descomprimidos. Tres consecuencias prácticas. El bucket debe tener versionado activado: sin él el pipeline no funciona, y es un requisito duro. Los artefactos se acumulan: con 20 ejecuciones diarias y 45 MB por artefacto son 27 GB al año, así que sin regla de ciclo de vida la factura de S3 crece sola. Y es el punto donde se garantiza la inmutabilidad: el ZIP que aprueba Marta es literalmente el mismo objeto que se despliega en producción, sin reconstruirse entre entornos, de modo que «lo que se validó en preproducción» y «lo que salió» son el mismo fichero byte a byte. Esa garantía es la razón de ser de esta lección.

aws s3api put-bucket-lifecycle-configuration --bucket mercadofresco-artefactos \
  --lifecycle-configuration '{"Rules":[{
    "ID":"caducar-artefactos-pipeline","Status":"Enabled",
    "Filter":{"Prefix":"pipeline/"},
    "Expiration":{"Days":60},
    "NoncurrentVersionExpiration":{"NoncurrentDays":15},
    "AbortIncompleteMultipartUpload":{"DaysAfterInitiation":7}}]}' \
  --profile mercadofresco-dev --region eu-west-1

El pipeline de MercadoFresco

flowchart TB
    A[Origen<br/>GitHub main] --> B[Construccion<br/>build-mercadofresco-tienda]
    B --> C{Etapa Calidad}
    C --> C1[Analisis estatico]
    C --> C2[Pruebas de contrato]
    C1 --> D[Migracion esquema<br/>fase EXPANDIR]
    C2 --> D
    D --> E[Desplegar en desarrollo<br/>dg-...-desarrollo]
    E --> F[Pruebas de humo<br/>build-mercadofresco-humo]
    F --> G[Desplegar en preproduccion]
    G --> H[Pruebas de humo preprod]
    H --> I[APROBACION MANUAL<br/>alertas-mercadofresco]
    I --> J[Desplegar en produccion<br/>blue/green + canario]
    J --> K[Puerta de calidad Lambda<br/>metricas MercadoFresco/Tienda]
    K -->|Metricas OK| L[Ejecucion correcta]
    K -->|Degradado| M[Detener y revertir]

Ocho etapas, y cada una responde a una pregunta concreta: ¿qué ha cambiado? ¿compila y pasa las pruebas? ¿cumple la calidad? ¿está el esquema preparado? ¿funciona en un entorno real? ¿pasa el flujo de compra? ¿lo autoriza alguien? ¿sigue sano después?

Tipos V1 y V2, disparadores y variables

Aspecto V1 V2
Precio 1 USD/mes por pipeline activo 0,002 USD por minuto de acción
Disparadores por filtro No : rama, etiqueta, ruta de fichero
Variables de pipeline No Sí, con valores en cada ejecución
Etapas en paralelo Limitado
Cuándo compensa Pipelines con muchísimas ejecuciones Casi siempre

Elige V2 salvo que tengas un motivo claro. Los disparadores con filtro por sí solos justifican el cambio: sin ellos, cualquier commit a main lanza el pipeline entero, incluida una corrección en el README. Con ellos:

{
  "triggers": [{
    "providerType": "CodeStarSourceConnection",
    "gitConfiguration": {
      "sourceActionName": "Origen",
      "push": [{
        "branches": { "includes": ["main"] },
        "filePaths": {
          "includes": ["app/**", "scripts/**", "requirements.txt", "appspec.yml"],
          "excludes": ["**/*.md", "docs/**", ".github/**"]
        }
      }],
      "pullRequest": [{
        "events": ["OPEN", "UPDATED"],
        "branches": { "includes": ["desarrollo", "main"] }
      }]
    }
  }]
}

Este disparador hace dos cosas distintas. En push a main, ejecuta el pipeline completo, pero solo si cambió código de verdad: un cambio en docs/ no gasta minutos ni molesta a nadie. Y en pull request, ejecuta —con las etapas de despliegue omitidas— la construcción y las pruebas, que es lo que cierra la protección de main que dejamos abierta en 08-01: la PR no se puede fusionar si la comprobación no está en verde.

Las variables de pipeline permiten parametrizar una ejecución sin tocar la definición. Se declaran en un bloque variables con nombre, valor por defecto y descripción, se referencian como #{variables.nivelLog} y se pueden fijar al lanzar la ejecución a mano. Una advertencia: si alguna vez te tienta declarar algo tipo omitirHumo, recuerda que una variable que permite saltarse una puerta es una puerta que se saltará. Si la pones, que su uso quede en CloudTrail y que el runbook exija justificarla por escrito.

La acción de origen con CodeConnections

{
  "name": "Origen",
  "actionTypeId": {
    "category": "Source", "owner": "AWS",
    "provider": "CodeStarSourceConnection", "version": "1"
  },
  "configuration": {
    "ConnectionArn": "arn:aws:codeconnections:eu-west-1:111122223333:connection/a1b2c3d4-EXAMPLE",
    "FullRepositoryId": "mercadofresco/mercadofresco-tienda",
    "BranchName": "main",
    "DetectChanges": "false",
    "OutputArtifactFormat": "CODEBUILD_CLONE_REF"
  },
  "outputArtifacts": [{ "name": "CodigoFuente" }],
  "runOrder": 1
}

Tres campos merecen explicación. DetectChanges: false apaga el webhook implícito porque en V2 los disparos los gobierna el bloque triggers; dejarlo en true con triggers definidos produce ejecuciones duplicadas, que es un desconcierto clásico. OutputArtifactFormat: CODEBUILD_CLONE_REF entrega a CodeBuild una referencia al repositorio en lugar de un ZIP, de modo que la construcción tiene la historia de Git disponible —necesaria para git describe o git-secrets --scan—; el precio es que el rol de CodeBuild necesita codeconnections:UseConnection. Y el ConnectionArn debe apuntar a una conexión en estado AVAILABLE: si sigue en PENDING, como avisamos en 08-01, la acción falla con un error de permisos que no menciona la conexión.

Construcción, despliegue y acciones en paralelo con runOrder

La etapa de construcción consume CodigoFuente y produce el artefacto desplegable:

{
  "name": "Construccion",
  "actions": [{
    "name": "ConstruirYProbar",
    "actionTypeId": { "category": "Build", "owner": "AWS",
                      "provider": "CodeBuild", "version": "1" },
    "configuration": { "ProjectName": "build-mercadofresco-tienda" },
    "inputArtifacts": [{ "name": "CodigoFuente" }],
    "outputArtifacts": [{ "name": "PaqueteTienda" }],
    "namespace": "construccion",
    "runOrder": 1
  }]
}

El namespace es la pieza que conecta esta lección con 08-02: expone las variables que el buildspec declaró en exported-variables, de modo que etapas posteriores pueden usar #{construccion.VERSION_APP} sin recalcular nada. Es así como el mensaje de aprobación sabrá qué versión está aprobando Marta.

La etapa de calidad muestra el paralelismo:

{
  "name": "Calidad",
  "actions": [
    { "name": "AnalisisEstatico", "runOrder": 1,
      "actionTypeId": { "category": "Test", "owner": "AWS",
                        "provider": "CodeBuild", "version": "1" },
      "configuration": { "ProjectName": "build-mercadofresco-analisis" },
      "inputArtifacts": [{ "name": "CodigoFuente" }] },
    { "name": "PruebasContrato", "runOrder": 1,
      "actionTypeId": { "category": "Test", "owner": "AWS",
                        "provider": "CodeBuild", "version": "1" },
      "configuration": { "ProjectName": "build-mercadofresco-contrato" },
      "inputArtifacts": [{ "name": "CodigoFuente" }] }
  ]
}

Las dos tienen runOrder: 1 y corren a la vez; si cualquiera falla, la etapa falla y el pipeline se detiene. Una tercera con runOrder: 2 esperaría a ambas. Y el despliegue consume el artefacto:

{
  "name": "DesplegarProduccion",
  "actions": [{
    "name": "CodeDeployProduccion",
    "actionTypeId": { "category": "Deploy", "owner": "AWS",
                      "provider": "CodeDeploy", "version": "1" },
    "configuration": {
      "ApplicationName": "app-mercadofresco-tienda",
      "DeploymentGroupName": "dg-mercadofresco-tienda-produccion"
    },
    "inputArtifacts": [{ "name": "PaqueteTienda" }],
    "runOrder": 1
  }]
}

Fíjate en que el inputArtifacts es el mismo PaqueteTienda que se desplegó en desarrollo y en preproducción. No se reconstruye. Es la garantía de inmutabilidad que hace que las pruebas anteriores signifiquen algo.

Aprobación manual: qué debe leer Marta en treinta segundos

Una aprobación mal diseñada es un botón que se pulsa sin mirar. Una bien diseñada da la información justa para decidir.

{
  "name": "AprobacionProduccion",
  "actions": [{
    "name": "AprobarMarta",
    "actionTypeId": { "category": "Approval", "owner": "AWS",
                      "provider": "Manual", "version": "1" },
    "configuration": {
      "NotificationArn": "arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco",
      "CustomData": "Version #{construccion.VERSION_APP} | Commit #{construccion.COMMIT_CORTO} | Autor #{construccion.AUTOR} | Humo preprod: OK | Migracion: EXPANDIR aplicada | Ventana: NO desplegar viernes 16-22h",
      "ExternalEntityLink": "https://github.com/mercadofresco/mercadofresco-tienda/compare/#{construccion.VERSION_ANTERIOR}...#{construccion.VERSION_APP}"
    },
    "timeoutInMinutes": 1440,
    "runOrder": 1
  }]
}

Lo que hace útil esta aprobación son los dos campos de texto. CustomData lleva las cinco cosas que Marta necesita: qué versión, qué commit, quién lo hizo, si las pruebas de humo pasaron en preproducción y si hay una migración de esquema en juego, más el recordatorio de la ventana prohibida. Y ExternalEntityLink es un enlace al diff entre la versión desplegada y la candidata: en un clic ve exactamente qué cambia, que es la pregunta que de verdad importa.

El timeoutInMinutes: 1440 es una decisión, no un detalle: la aprobación caduca a las 24 horas y la ejecución falla. Es correcto —un cambio que lleva tres días esperando ya no es el cambio que se validó, porque main ha avanzado— y evita que se acumule una cola de aprobaciones zombis.

Tres reglas para que esto funcione: quien aprueba no es quien programó —o la aprobación no filtra nada—; el permiso codepipeline:PutApprovalResult se concede solo a quien debe aprobar; y si la aprobación se pulsa siempre sin mirar, hay que quitarla, porque un control que no filtra solo añade retraso y una falsa sensación de seguridad.

Entornos: desarrollo, preproducción y producción

Las tres etapas de despliegue del pipeline usan grupos de despliegue distintos:

Entorno Grupo de despliegue Estrategia Quién dispara Datos
Desarrollo dg-mercadofresco-tienda-desarrollo En el sitio, AllAtOnce Automático Sintéticos
Preproducción dg-mercadofresco-tienda-preproduccion Blue/green Automático Anonimizados
Producción dg-mercadofresco-tienda-produccion Blue/green + canario Tras aprobación Reales

Aquí hay que ser honesto sobre lo que MercadoFresco tiene hoy: los tres entornos viven en la misma cuenta 111122223333, separados por etiquetas, subredes y políticas de IAM. Funciona, pero tiene tres límites que conviene nombrar sin adornos. El radio de explosión no está acotado: un error en una política o un script con el perfil equivocado puede tocar producción desde una tarea de desarrollo. Los límites de servicio se comparten: las pruebas de carga en preproducción consumen la misma cuota de invocaciones concurrentes de Lambda que la tienda real. Y la factura no se separa de verdad: las etiquetas ayudan —lo veremos en 11-02— pero no son una frontera.

La separación seria es una cuenta de AWS por entorno, con el pipeline en una cuenta de herramientas que asume roles en las demás. Eso es AWS Organizations y es 09-04; aquí basta con saber que la separación por etiquetas es un punto de partida y no el destino, y que el pipeline ya está preparado para el cambio porque cada etapa apunta a un grupo de despliegue independiente.

Pruebas de humo y puertas de calidad con una acción Lambda

Las pruebas de humo verifican que lo desplegado funciona de verdad, contra el entorno recién actualizado y por la puerta principal:

# tests/humo/test_flujo_compra.py  -> lo ejecuta build-mercadofresco-humo
import os, requests, pytest

BASE = os.environ["URL_ENTORNO"]      # p. ej. https://preprod.mercadofresco.example

def test_salud_responde_y_dice_la_version():
    r = requests.get(f"{BASE}/salud", timeout=5)
    assert r.status_code == 200
    assert r.json()["version"] == os.environ["VERSION_ESPERADA"]

def test_flujo_de_compra_completo():
    s = requests.Session()
    assert s.get(f"{BASE}/productos?categoria=fruta", timeout=5).status_code == 200
    s.post(f"{BASE}/carrito", json={"sku": "FRUT-0012", "uds": 2}, timeout=5)
    r = s.post(f"{BASE}/pedidos", timeout=10, json={
        "franja_entrega": "tarde", "metodo_pago": "tarjeta_prueba",
        "clave_idempotencia": f"humo-{os.environ['ID_EJECUCION']}"})
    assert r.status_code == 201
    assert r.json()["estado"] == "confirmado"
    assert r.elapsed.total_seconds() < 2.0     # el objetivo de 07-05

La tercera aserción es la que suele faltar y la que más incidentes previene: no solo que el pedido se confirme, sino que se confirme dentro del presupuesto de latencia. Un pedido que tarda 6 segundos está roto aunque devuelva 201.

La puerta de calidad va un paso más allá: después de desplegar en producción, una acción de invocación consulta las métricas reales y decide si el pipeline continúa.

# mercadofresco-puerta-calidad
import boto3
from datetime import datetime, timedelta, timezone

cp = boto3.client("codepipeline")
cw = boto3.client("cloudwatch")

UMBRALES = {
    "TiempoConfirmacionPedido": {"stat": "p95", "max": 1500},
    "PedidosConfirmados":       {"stat": "Sum", "min": 5},
}

def _metrica(nombre, stat, minutos=10):
    """Devuelve el valor agregado, o None si no hay datos."""
    fin = datetime.now(timezone.utc)
    es_percentil = stat.startswith("p")
    r = cw.get_metric_statistics(
        Namespace="MercadoFresco/Tienda", MetricName=nombre,
        StartTime=fin - timedelta(minutes=minutos), EndTime=fin, Period=60,
        ExtendedStatistics=[stat] if es_percentil else None,
        Statistics=None if es_percentil else [stat])
    puntos = r["Datapoints"]
    if not puntos:
        return None
    if es_percentil:
        return max(p["ExtendedStatistics"][stat] for p in puntos)
    return sum(p[stat] for p in puntos)

def handler(event, context):
    job_id = event["CodePipeline.job"]["id"]
    fallos = []
    try:
        for nombre, regla in UMBRALES.items():
            valor = _metrica(nombre, regla["stat"])
            if valor is None:
                # Sin datos NO es aprobar: es no saber. Y no saber, en produccion, es fallar.
                fallos.append(f"{nombre}: sin datos en 10 min")
            elif "max" in regla and valor > regla["max"]:
                fallos.append(f"{nombre} {regla['stat']}={valor:.0f} > {regla['max']}")
            elif "min" in regla and valor < regla["min"]:
                fallos.append(f"{nombre}={valor:.0f} < {regla['min']}")

        if fallos:
            cp.put_job_failure_result(jobId=job_id,
                failureDetails={"type": "JobFailed", "message": "; ".join(fallos)[:265]})
        else:
            cp.put_job_success_result(jobId=job_id)
    except Exception as e:
        # Ante un error inesperado, fallar. Nunca aprobar por defecto.
        cp.put_job_failure_result(jobId=job_id,
            failureDetails={"type": "JobFailed", "message": str(e)[:265]})

Tres decisiones de diseño en este código, y las tres son deliberadas. Sin datos se falla, porque PedidosConfirmados a cero durante diez minutos en horario comercial no significa «todo bien», significa que nadie está comprando —que es exactamente lo que quieres detectar—. Ante una excepción se falla, porque una puerta que se abre cuando se rompe no es una puerta. Y es obligatorio llamar a put_job_success_result o put_job_failure_result: si la Lambda termina sin hacerlo, la acción se queda en InProgress hasta el timeout de una hora, igual que el gancho de CodeDeploy en 08-03.

Reintentos, ejecuciones en cola y modo superpuesto

# Reintentar solo lo que fallo, sin volver a construir
aws codepipeline retry-stage-execution \
  --pipeline-name pipeline-mercadofresco-tienda \
  --stage-name DesplegarPreproduccion \
  --pipeline-execution-id 7a1f2c33-EXAMPLE \
  --retry-mode FAILED_ACTIONS \
  --profile mercadofresco-dev --region eu-west-1

FAILED_ACTIONS reintenta solo las acciones fallidas y ALL_ACTIONS la etapa entera: el primero cuando el fallo fue transitorio, el segundo si la etapa tiene acciones interdependientes. Los tres modos de ejecución son una decisión que se toma una vez y afecta a todos los días:

Modo Comportamiento Cuándo
SUPERSEDED (por defecto) El cambio nuevo sustituye al que espera Entrega continua rápida
QUEUED Se encolan y se ejecutan en orden Cuando cada cambio debe desplegarse
PARALLEL Ejecuciones simultáneas independientes Pipelines por rama de funcionalidad

SUPERSEDED es el correcto para MercadoFresco y conviene entender por qué, porque suena a que se pierden cambios y no es así. Si Luis empuja tres commits en diez minutos, no tiene sentido desplegar tres veces: el tercero contiene a los dos anteriores, así que desplegar solo el último es a la vez más rápido y equivalente. Lo que sustituye es la ejecución en espera, no el código. La excepción es cuando cada ejecución tiene un efecto propio que no es acumulativo —por ejemplo un pipeline que publica versiones etiquetadas—; ahí QUEUED es lo correcto.

Permisos: rol del pipeline, roles por acción y entre cuentas

El rol del pipeline es el que asume CodePipeline para orquestar, y merece la misma disciplina de 04-01: no ejecuta el trabajo, solo lo lanza.

{
  "Version": "2012-10-17",
  "Statement": [
    { "Sid": "Artefactos", "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:GetObjectVersion", "s3:PutObject", "s3:GetBucketVersioning"],
      "Resource": ["arn:aws:s3:::mercadofresco-artefactos",
                   "arn:aws:s3:::mercadofresco-artefactos/*"] },
    { "Sid": "LanzarConstrucciones", "Effect": "Allow",
      "Action": ["codebuild:StartBuild", "codebuild:BatchGetBuilds"],
      "Resource": "arn:aws:codebuild:eu-west-1:111122223333:project/build-mercadofresco-*" },
    { "Sid": "LanzarDespliegues", "Effect": "Allow",
      "Action": ["codedeploy:CreateDeployment", "codedeploy:GetDeployment",
                 "codedeploy:GetDeploymentConfig", "codedeploy:RegisterApplicationRevision"],
      "Resource": "arn:aws:codedeploy:eu-west-1:111122223333:*:app-mercadofresco-*" },
    { "Sid": "PuertaDeCalidad", "Effect": "Allow",
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:eu-west-1:111122223333:function:mercadofresco-puerta-calidad" },
    { "Sid": "Origen", "Effect": "Allow",
      "Action": "codeconnections:UseConnection",
      "Resource": "arn:aws:codeconnections:eu-west-1:111122223333:connection/a1b2c3d4-EXAMPLE" },
    { "Sid": "CifradoDeArtefactos", "Effect": "Allow",
      "Action": ["kms:Decrypt", "kms:GenerateDataKey"],
      "Resource": "arn:aws:kms:eu-west-1:111122223333:key/*",
      "Condition": {"StringEquals": {"kms:ViaService": "s3.eu-west-1.amazonaws.com"}} }
  ]
}

Fíjate en que el rol del pipeline no tiene permisos sobre EC2, Aurora ni SQS: solo puede lanzar CodeBuild y CodeDeploy, que a su vez actúan con sus propios roles. Esa separación en tres roles —el del pipeline, el de la construcción y el de la instancia— es lo que impide que comprometer uno dé acceso a todo. Y kms:GenerateDataKey es imprescindible: sin él, el pipeline puede leer artefactos pero no escribirlos en un bucket cifrado, con un error de acceso denegado que no menciona KMS.

El acceso entre cuentas, que será el modelo de 09-04, funciona con un rol asumible: la cuenta de producción publica un rol que confía en la cuenta de herramientas, y la acción de despliegue lleva un roleArn. La condición imprescindible: la clave de KMS que cifra el bucket de artefactos debe ser una clave de cliente compartida con las cuentas destino, porque la clave gestionada por AWS no se puede compartir y el despliegue fallaría al no poder descifrar el artefacto.

Notificaciones hacia correo y Slack

Dos mecanismos, como en 08-01, y conviene elegir bien:

aws codestar-notifications create-notification-rule \
  --name notif-pipeline-mercadofresco \
  --resource arn:aws:codepipeline:eu-west-1:111122223333:pipeline-mercadofresco-tienda \
  --detail-type FULL \
  --event-type-ids codepipeline-pipeline-pipeline-execution-failed \
                   codepipeline-pipeline-manual-approval-needed \
                   codepipeline-pipeline-stage-execution-failed \
  --targets TargetType=SNS,TargetAddress=arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

Fíjate en lo que NO está en la lista: codepipeline-pipeline-pipeline-execution-succeeded. Con 20 ejecuciones diarias, notificar los éxitos son 400 mensajes al mes que enseñan al equipo a ignorar el canal, y entonces el aviso que importa —el fallo de un viernes— pasa desapercibido. Notifica solo lo accionable: fallos y aprobaciones pendientes. Es la misma disciplina de 05-01 con las alarmas.

Para Slack, AWS Chatbot conecta el tema de SNS con un canal y añade lo importante: botones para aprobar desde el propio chat, lo que baja el tiempo de aprobación de Marta de horas a minutos. Y para lógica propia, EventBridge recibe todos los eventos del pipeline con source: ["aws.codepipeline"], como vimos en 07-03.

El pipeline como código

Todo lo de esta lección se ha creado con aws codepipeline create-pipeline y un JSON en el portátil de Luis. Es decir: hemos automatizado el despliegue de la aplicación con una herramienta que a su vez está configurada a mano. Si alguien borra el pipeline, no hay forma fiable de recrearlo; si Marta cambia un umbral de la puerta de calidad, no queda registro de quién ni por qué.

La salida provisional es exportar la definición y versionarla en mercadofresco-infra:

aws codepipeline get-pipeline --name pipeline-mercadofresco-tienda \
  --query 'pipeline' --output json > pipeline-mercadofresco-tienda.json
aws codepipeline update-pipeline --cli-input-json file://pipeline-mercadofresco-tienda.json \
  --profile mercadofresco-dev --region eu-west-1

Funciona, pero es un apaño: el JSON exportado trae metadatos que hay que limpiar, no parametriza por entorno y no gestiona dependencias entre recursos. La solución de verdad es definir el pipeline —y toda la infraestructura— como código, y eso es el módulo 9: CloudFormation en 09-01 y CDK en 09-02.

Coste y limpieza

Concepto Cálculo Coste
CodePipeline V2, ~20 ejecuciones/día ~1.700 min de acción/mes × 0,002 3,40 USD
Lambda de la puerta de calidad 600 invocaciones de 3 s ~0,01 USD
Artefactos en S3, con caducidad de 60 días ~3 GB 0,07 USD
CodePipeline ≈ 3,50 USD/mes
Módulo 8 completo (con CodeBuild y CodeDeploy) ≈ 28 USD/mes

Veintiocho dólares al mes por eliminar los despliegues manuales de un equipo de tres personas. La comparación honesta no es contra cero: es contra el coste de una caída de treinta minutos un viernes a las 19:00, con 900 pedidos/hora, más las horas que Luis dedicaba a desplegar a mano.

# Retener cambios sin borrar nada: la transicion deshabilitada
aws codepipeline disable-stage-transition --pipeline-name pipeline-mercadofresco-tienda \
  --stage-name DesplegarProduccion --transition-type Inbound \
  --reason "Pico del viernes: sin despliegues hasta el lunes" \
  --profile mercadofresco-dev --region eu-west-1

# Borrar un pipeline de pruebas (no borra el bucket ni sus artefactos)
aws codepipeline delete-pipeline --name pipeline-pruebas-borrar \
  --profile mercadofresco-dev --region eu-west-1

Borrar el pipeline no borra los artefactos, que siguen ocupando y facturando en S3. Es el residuo más común de este módulo: revisa el bucket y su regla de ciclo de vida.

Errores Comunes y Consejos

Dejar DetectChanges: true con triggers de V2 definidos. Cada push lanza dos ejecuciones, se pisan y nadie entiende por qué.

Bucket de artefactos sin versionado. El pipeline no funciona y el mensaje no es evidente. Es un requisito duro.

Sin regla de ciclo de vida en el bucket. Veinte ejecuciones diarias de 45 MB son 27 GB al año de ZIP que nadie va a desplegar.

Reconstruir el artefacto en cada etapa. Rompe la garantía de inmutabilidad: lo que aprueba Marta deja de ser lo que sale a producción. Construye una vez, despliega el mismo objeto.

Una aprobación manual sin CustomData útil. Se convierte en un botón que se pulsa sin mirar, y entonces es solo retraso.

Que apruebe la misma persona que programó el cambio. La aprobación deja de filtrar nada.

Una puerta de calidad que aprueba cuando no hay datos o cuando lanza una excepción. Una puerta que se abre al romperse no es una puerta.

Olvidar put_job_success_result / put_job_failure_result en la Lambda de invocación. La acción se queda en InProgress hasta el timeout de una hora.

Notificar los éxitos. Cuatrocientos mensajes al mes enseñan al equipo a ignorar el canal, y el aviso que importa se pierde.

Un rol de pipeline con PowerUserAccess. El pipeline solo debe lanzar acciones; el trabajo lo hacen los roles de CodeBuild y CodeDeploy. Tres roles separados, tres radios de explosión acotados.

Olvidar kms:GenerateDataKey en el rol del pipeline, o usar la clave gestionada por AWS en un pipeline entre cuentas: el despliegue falla al no poder descifrar el artefacto.

Consejo: usa V2 y filtra por ruta de fichero. Un cambio en el README no debe gastar minutos ni molestar a nadie.

Consejo: conecta el pipeline a las PR, no solo a main. Es lo que cierra la protección de rama que dejamos abierta en 08-01.

Consejo: pon condiciones de salida a la aprobación manual. Un control temporal sin criterio para retirarlo se queda para siempre.

Consejo: aprende a usar la transición deshabilitada. Es la forma limpia de decir «hoy no se despliega a producción» sin desmontar nada.

Ejercicios

Ejercicio 1: el pipeline que desplegó lo que no era

Viernes, 18:50. Marta aprueba la versión 1.7.0 tras ver que las pruebas de humo pasaron en preproducción. El pipeline despliega en producción y a los cuatro minutos salta mercadofresco-alb-latencia-alta. CodeDeploy revierte. Investigando, descubren que la etapa de producción reconstruía el artefacto con una acción de CodeBuild propia en lugar de reutilizar PaqueteTienda, y que entre la construcción de preproducción (18:20) y la de producción (18:52) alguien había fusionado otra PR en main.

Responde: (a) qué se desplegó exactamente en producción; (b) por qué las pruebas de humo de preproducción no valían nada en este diseño; (c) cómo se corrige el pipeline; (d) qué otras dos salvaguardas del módulo habrían limitado el daño; (e) qué medida de proceso propondrías para el horario.

Ejercicio 2: diseñar la puerta de calidad

MercadoFresco quiere pasar de entrega continua a despliegue continuo: quitar la aprobación de Marta y que la puerta de calidad decida sola. Datos: en horario comercial se confirman entre 120 y 900 pedidos por hora; de 02:00 a 07:00 hay entre 0 y 5; TiempoConfirmacionPedido p95 normal es 400 ms y el objetivo es no superar 1.500 ms; el despliegue canario de la Lambda de cobros dura 5 minutos y el blue/green de la tienda tarda unos 12 en completarse.

Diseña la puerta: qué métricas, qué umbrales, qué ventana de observación, qué hace ante ausencia de datos y en qué punto del pipeline se coloca. Justifica cada decisión y di qué tres condiciones deberían cumplirse antes de quitar la aprobación manual.

Ejercicio 3: el pipeline de las tres emergencias

El lunes ocurren tres cosas. (a) A las 09:15, Luis empuja cinco commits seguidos a main corrigiendo un error de estilo; el pipeline arranca cinco veces y la cuarta se queda esperando. (b) A las 11:40, la etapa DesplegarPreproduccion falla porque el agente de CodeDeploy de una instancia no respondió; el resto del pipeline había ido bien y la construcción tardó 9 minutos. (c) A las 17:20 hay que desplegar una corrección urgente de seguridad, pero Marta está en un avión y no puede aprobar hasta las 21:00.

Para cada situación indica qué ocurre con la configuración descrita en la lección, qué comando o acción concreta usarías, y qué cambiarías para que no vuelva a ser un problema.

Soluciones

Solución 1

(a) Se desplegó la 1.7.0 más el cambio de la PR fusionada a las 18:45, es decir, código que nunca pasó por preproducción ni por la aprobación. Marta aprobó una cosa y salió otra. Y lo grave es que nada en la interfaz lo indicaba: la ejecución seguía llamándose 1.7.0.

(b) Porque validaban un artefacto distinto del que se desplegó. Una prueba solo dice algo sobre el binario concreto que probó; si reconstruyes después, has tirado esa información. Aquí la reconstrucción tomó main en su estado del momento, no el commit de la ejecución, así que arrastró un cambio nuevo. Es la diferencia entre «probamos esto» y «probamos algo parecido a esto».

(c) Eliminando la acción de construcción de la etapa de producción y haciendo que la acción de despliegue consuma el PaqueteTienda producido en la etapa de construcción, que es el mismo objeto de S3 que ya se desplegó en desarrollo y preproducción:

{ "name": "CodeDeployProduccion",
  "inputArtifacts": [{ "name": "PaqueteTienda" }] }

Construir una vez, desplegar muchas es la regla, y no admite excepciones «porque en producción hace falta otra configuración»: la configuración por entorno se resuelve en tiempo de ejecución con Parameter Store, no reconstruyendo el paquete. Si hiciera falta comprobarlo, basta con verificar que el commit de los metadatos del artefacto coincide con el de la ejecución del pipeline.

(d) La puerta de calidad habría detectado la degradación por métricas incluso si la reversión de CodeDeploy no hubiera saltado, deteniendo el pipeline y dejando constancia. Y la transición deshabilitada hacia producción durante el pico del viernes habría impedido el despliegue a las 18:50, que es cuando más caro sale equivocarse. Vale la pena notar que la reversión automática funcionó: cuatro minutos de degradación en lugar de treinta, y sin que nadie tuviera que hacer nada. El fallo estuvo aguas arriba, en el diseño del pipeline, no en el mecanismo de seguridad.

(e) Una ventana de despliegue explícita: nada a producción los viernes de 16:00 a 22:00, ni en festivos ni después de las 18:00 cualquier día. No es desconfianza en el pipeline, es aritmética: el coste de un incidente en el pico con 900 pedidos/hora es varias veces el de un lunes por la mañana, y a las 19:00 de un viernes hay menos gente disponible para responder. Implementarlo con la transición deshabilitada de forma programada, no confiando en que alguien se acuerde.

Solución 2

Métricas y umbrales. Tres señales complementarias, porque una sola es fácil de engañar:

Métrica Umbral Por qué
TiempoConfirmacionPedido p95 > 1.500 ms → fallar Objetivo de negocio de 07-05; el normal es 400 ms
PedidosConfirmados (Sum) Caída > 40 % frente a la misma franja de los 7 días previos Detecta que nadie compra, que es el fallo silencioso
Errores 5xx del ALB (ratio) > 1 % de las peticiones Detecta el fallo evidente y rápido

El umbral relativo de PedidosConfirmados es la decisión clave del ejercicio. Un umbral absoluto no puede funcionar cuando el volumen normal oscila entre 0 y 900 según la hora: si pones «al menos 5 pedidos en 10 minutos», la puerta fallará todos los despliegues nocturnos legítimos; si pones 0, no detectará nunca nada. Comparar con la misma franja horaria de los siete días anteriores se adapta solo al patrón real, incluido el pico del viernes.

Ventana de observación: 15 minutos, y la razón es aritmética. El blue/green tarda unos 12 minutos en completarse, así que una ventana más corta mediría en parte el entorno azul y en parte el verde, mezclando las dos versiones y diluyendo cualquier degradación. Quince minutos garantizan al menos tres de tráfico íntegramente sobre la versión nueva.

Ante ausencia de datos, depende de la hora, y esto es lo que hace la puerta utilizable de noche. En horario comercial (07:00-02:00), sin datos de PedidosConfirmados es fallar: significa que nadie compra. De 02:00 a 07:00 es lo normal, así que en esa franja PedidosConfirmados se ignora y la puerta se apoya en TiempoConfirmacionPedido medido con tráfico sintético —una prueba de humo cada minuto desde CloudWatch Synthetics—, sin el cual de madrugada no habría nada que medir. La regla general se mantiene: sin datos no es aprobar; es no saber, y no saber solo es aceptable si has decidido de antemano que en esa franja no hay nada que saber.

Dónde se coloca: dos puertas, no una. Una inmediatamente después del despliegue canario de la Lambda de cobros, con ventana de 5 minutos ajustada a la duración del canario, para cortar antes de que el canario promocione al 100 %. Y otra después del blue/green de la tienda, con la ventana de 15 minutos, dentro del terminationWaitTimeInMinutes de 30 para que la reversión siga siendo de 90 segundos. Una puerta que evalúa cuando ya se ha terminado el entorno azul llega tarde.

Las tres condiciones para quitar la aprobación manual, que son las que Marta ya escribió: que las pruebas de humo cubran el flujo de compra completo —incluido el cobro con tarjeta de prueba—, que la reversión automática se haya probado a propósito al menos tres veces con tiempos medidos, y que pasen dos meses sin ningún despliegue que haya habido que revertir a mano. A eso conviene añadir una cuarta que el ejercicio sugiere: que la puerta de calidad haya funcionado en modo observación durante un mes — registrando lo que habría hecho sin llegar a bloquear— y no haya producido falsos positivos. Quitar la aprobación humana y estrenar la puerta automática el mismo día es cambiar un control por una hipótesis.

Solución 3

(a) Los cinco commits. Con el modo SUPERSEDED por defecto, no se ejecutan cinco pipelines completos: la primera ejecución sigue su curso y las siguientes se van sustituyendo entre sí, de modo que quedan dos —la que corría y la última—. No se pierde nada, porque el quinto commit contiene a los cuatro anteriores. Así que técnicamente el comportamiento es correcto. Lo que sí es un desperdicio es haber arrancado siquiera: eran correcciones de estilo. El arreglo es el filtro por ruta de fichero de V2, con excludes para **/*.md y docs/**, y si el cambio afecta a app/ pero es trivial, el flujo correcto es agruparlo en un solo commit antes de fusionar. Nada que hacer en el momento salvo dejar que termine.

(b) El fallo del agente. Es un fallo transitorio de infraestructura, no del código, así que no hay que reconstruir: reconstruir tiraría nueve minutos y, peor, produciría un artefacto nuevo con el riesgo del ejercicio 1.

aws codepipeline retry-stage-execution \
  --pipeline-name pipeline-mercadofresco-tienda \
  --stage-name DesplegarPreproduccion \
  --pipeline-execution-id <id> --retry-mode FAILED_ACTIONS \
  --profile mercadofresco-dev --region eu-west-1

FAILED_ACTIONS reintenta solo la acción de despliegue reutilizando el mismo PaqueteTienda. Para que no vuelva a ser un problema: el agente instalado como asociación de Systems Manager con actualización automática, como vimos en 08-03, y una alarma sobre el estado del agente en las instancias. Y si es recurrente, revisar si a la instancia le falta salida de red o si el ASG la creó con una AMI sin agente.

(c) La corrección urgente sin Marta. El diseño ya tiene la respuesta y lo importante es no improvisar. La aprobación admite varios aprobadores: quien tenga codepipeline:PutApprovalResult puede autorizar, así que debe existir un suplente designado de antemano —Luis no, porque es quien escribió el cambio y la regla de que no apruebe el autor es justamente la que protege aquí; el suplente sería un segundo responsable técnico, o Sara si la organización lo acepta para casos documentados—. Si de verdad no hay nadie, la salida es AWS Chatbot en Slack, que permite a Marta aprobar desde el móvil en cuanto tenga red, incluida la del avión.

Lo que no hay que hacer es saltarse el pipeline y desplegar a mano con create-deployment: se pierden las pruebas de humo, la puerta de calidad y la trazabilidad, justo en un cambio de seguridad que es de los que más conviene tener registrados. Y tampoco usar una variable tipo omitirHumo, por la razón de la lección: una puerta que se puede saltar se saltará.

Como medida permanente, dos cosas. Una política de aprobadores escrita con titular y suplente, revisada cada trimestre. Y valorar un camino rápido documentado para correcciones de seguridad: un pipeline con las mismas puertas automáticas pero con la aprobación humana sustituida por la notificación inmediata a todo el equipo y un post-mortem obligatorio en 24 horas. Que un cambio sea urgente no justifica saltarse los controles automáticos; como mucho justifica sustituir el control humano por una rendición de cuentas posterior.

Conclusión

La secuencia que vivía en la cabeza de Luis es ahora pipeline-mercadofresco-tienda, una definición versionada que se ejecuta igual todas las veces. Un commit en main que toque app/ arranca el flujo, construye, prueba, comprueba la calidad, aplica la fase de expansión del esquema, despliega en desarrollo, pasa las pruebas de humo, despliega en preproducción, vuelve a pasarlas, avisa a Marta con la información para decidir en treinta segundos, despliega en producción con blue/green y canario, y comprueba con las métricas reales de MercadoFresco/Tienda que la tienda sigue sana. Nadie escribe un comando.

Sabes distinguir entrega continua de despliegue continuo —la diferencia es una persona entre la validación y producción— y por qué MercadoFresco elige la primera con condiciones de salida escritas, porque un control temporal sin criterio para retirarlo se queda para siempre. Conoces la estructura del pipeline y los dos matices que más juego dan: que las acciones con el mismo runOrder corren en paralelo y que una transición deshabilitada es la forma limpia de decir «hoy no se despliega». Y sobre todo tienes la garantía que da sentido a todo lo demás: el artefacto se construye una vez y el mismo objeto de S3 recorre los tres entornos, de modo que lo que Marta aprueba y lo que sale a producción son el mismo fichero byte a byte —romper eso, como en el ejercicio 1, invalida todas las pruebas anteriores—.

Sabes por qué V2 es la elección por defecto: los disparadores con filtro por rama, etiqueta y ruta de fichero evitan que un cambio en el README gaste minutos, y las ejecuciones sobre pull request son lo que cierra la protección de main que quedó abierta en 08-01. Conoces la acción de origen con CodeConnections y sus dos trampas —DetectChanges: true junto a triggers produce ejecuciones duplicadas, y una conexión en PENDING falla con un error que no la menciona—, el namespace que expone las variables exportadas por el buildspec de 08-02, y la aprobación manual que sirve de verdad: CustomData con versión, commit, autor, estado del humo y migración pendiente, más un ExternalEntityLink al diff.

Tienes las puertas de calidad con sus tres decisiones deliberadas —sin datos se falla, ante excepción se falla, y hay que llamar siempre a put_job_success_result o put_job_failure_result—, los tres modos de ejecución con SUPERSEDED como el correcto porque el commit nuevo contiene a los anteriores, y el reintento con FAILED_ACTIONS que no reconstruye. Sabes separar los permisos en tres roles —el del pipeline, que solo lanza; el de la construcción; y el de la instancia— y que kms:GenerateDataKey es lo que falta cuando un pipeline no puede escribir en un bucket cifrado. Y sabes notificar solo lo accionable: fallos y aprobaciones, nunca los éxitos, o el canal se vuelve ruido.

Todo por unos 28 dólares al mes para el módulo completo, con dos residuos que vigilar: los artefactos que sobreviven al pipeline borrado y el bucket sin ciclo de vida.

Quedan dos cabos sueltos que la lección ha dejado a la vista a propósito. El primero es que los tres entornos comparten la cuenta 111122223333: el radio de explosión no está acotado, los límites de servicio se comparten y la factura no se separa de verdad. El segundo es más incómodo: este pipeline, que existe para que nadie despliegue a mano, se ha creado a mano, con un JSON en el portátil de Luis y un create-pipeline.

En 08-05, «Un pipeline de extremo a extremo», cerramos el módulo siguiendo un cambio real desde el portátil de Luis hasta producción: el campo «franja horaria de entrega», que toca la tienda, un consumidor de cola-mercadofresco-pedidos, el esquema de Aurora y el contrato del evento PedidoConfirmado. Veremos paso a paso qué se comprueba y qué pasa si falla; cómo se aplica expansión-contracción en Aurora y se versiona un evento sin romper a los consumidores existentes, con el orden correcto entre productor y consumidor que cierra el incidente del módulo 7; la pirámide de pruebas y cuáles bloquean; las métricas DORA de MercadoFresco antes y después; el runbook de reversión cuando el fallo se detecta media hora tarde; y las banderas de funcionalidad con AppConfig que convierten «desplegar un viernes» en una decisión de negocio y no de fe.

Curso de AWS

Módulo 1: Introducción a AWS

Módulo 2: Servicios principales de AWS

Módulo 3: Redes y entrega de contenido

Módulo 4: Seguridad e identidad

Módulo 5: Monitorización y gestión

Módulo 6: Bases de datos

Módulo 7: Integración de aplicaciones

Módulo 8: Herramientas para desarrolladores

Módulo 9: Infraestructura como código y gobierno de cuentas

Módulo 10: Contenedores en AWS

Módulo 11: Mejores prácticas y gestión de costos

© Copyright 2026. Todos los derechos reservados