El módulo 9 dejó una infraestructura reproducible, gobernada y repartida en cinco cuentas, y una frase incómoda al final: la tienda de MercadoFresco sigue viviendo en instancias EC2 que hay que parchear, sobre una AMI que hay que reconstruir cada vez que cambia una dependencia, y que tardan dos minutos en arrancar. El viernes a las 19:00, con 900 pedidos por hora, esos dos minutos son exactamente el tiempo que los clientes pasan esperando mientras el ASG hace su trabajo.
Esta lección ataca el problema por la raíz: sustituir la máquina por el contenedor. Verás qué es un contenedor y en qué se diferencia de verdad de una máquina virtual, cómo se construye la imagen de la tienda con un Dockerfile serio, dónde se guarda esa imagen (Amazon ECR) y quién la ejecuta (Amazon ECS). Al final tendrás la tienda y los trabajadores de la cola de pedidos contenerizados, con su definición de tarea, su servicio detrás del ALB que ya existe y su autoescalado.
Aviso de coste. Amazon ECS no cuesta nada por sí mismo: es un plano de control gratuito, y pagas únicamente el cómputo que ejecuta las tareas —las instancias EC2 en esta lección, Fargate en 10-02—. Amazon ECR cobra 0,10 USD por GB y mes de almacenamiento y la transferencia de datos hacia fuera de la región; la capa gratuita incluye 500 MB durante 12 meses. El escaneo básico es gratuito; el escaneo mejorado con Amazon Inspector se cobra por imagen escaneada. Sigue todos los ejercicios con etiquetas correctas y borra al final lo que hayas creado. Todos los datos, cuentas e identificadores son ficticios.
Contenido
- Los tres problemas que dejó el módulo 9
- Qué es un contenedor y en qué se diferencia de una máquina virtual
- Imagen, capas, registro y contenedor en ejecución
- El Dockerfile de la tienda de MercadoFresco
.dockerignore, tamaño y caché de capas- Amazon ECR: el registro que sustituye a las AMI
- Etiquetado, inmutabilidad y ciclo de vida de las imágenes
- Escaneo de vulnerabilidades: básico y mejorado
- Replicación, cifrado y política de repositorio
- Amazon ECS: clúster, definición de tarea, tarea y servicio
- Tipos de lanzamiento, agente de ECS y proveedores de capacidad
- La definición de tarea de la tienda, comentada
- Rol de ejecución de la tarea frente a rol de la tarea
- Modo de red
awsvpcy qué implica - El servicio: ALB, comprobaciones de estado y colocación
- Despliegues: rolling, porcentajes y circuit breaker
- Autoescalado del servicio y descubrimiento con Cloud Map
- Observabilidad: Container Insights, registros y X-Ray
- La migración de MercadoFresco
- Coste y limpieza
- Errores comunes y consejos
- Ejercicios
- Conclusión
Los tres problemas que dejó el módulo 9
Conviene nombrarlos con precisión antes de resolverlos, porque cada uno tiene una causa distinta y el contenedor los resuelve por caminos distintos.
| Problema | Qué pasa hoy en MercadoFresco | Coste real |
|---|---|---|
| Parcheo | Cada instancia del ASG lleva un sistema operativo completo que recibe CVE de OpenSSL, del kernel y de glibc |
Una ventana de mantenimiento al mes, con reinicios escalonados |
| Reconstrucción de AMI | Añadir una librería de Python obliga a lanzar una instancia, instalarla, crear la AMI y actualizar la plantilla de lanzamiento | 40-60 minutos por cambio de dependencia |
| Arranque de dos minutos | Arrancar EC2, ejecutar cloud-init, arrancar la aplicación, superar las comprobaciones del grupo de destino |
El pico de los viernes llega antes que la capacidad |
El contenedor no elimina el sistema operativo del planeta —sigue habiendo un kernel debajo—, pero cambia dónde vive la frontera entre lo tuyo y lo de AWS. El parcheo del sistema operativo base pasa a ser una línea del Dockerfile y una reconstrucción de treinta segundos, no una ventana de mantenimiento; con Fargate (10-02), el anfitrión deja de existir para ti. La AMI desaparece como artefacto y su lugar lo ocupa la imagen, que se construye en CodeBuild en dos minutos, se versiona por commit y se guarda en un registro. Y el arranque deja de incluir el arranque de una máquina: una tarea de ECS sobre una instancia existente arranca en 5-15 segundos, y sobre Fargate en 30-45 incluida la ENI. Frente a los 120 segundos actuales, es otra categoría de respuesta.
Qué es un contenedor y en qué se diferencia de una máquina virtual
Un contenedor es un proceso del sistema operativo anfitrión al que el kernel le ha mentido sobre el mundo: ve su propio sistema de archivos, su propia tabla de procesos, su propia red y sus propios límites de CPU y memoria. Esa mentira la construyen dos mecanismos del kernel de Linux: los namespaces (aislamiento de lo que se ve) y los cgroups (limitación de lo que se consume).
Una máquina virtual es otra cosa: un hipervisor emula hardware completo y dentro arranca un kernel propio, con su sistema de arranque, sus servicios y su sistema de archivos.
| Aspecto | Máquina virtual (EC2) | Contenedor |
|---|---|---|
| Aislamiento | Hardware virtualizado; kernel propio. Frontera muy fuerte | Namespaces y cgroups; kernel compartido. Frontera fuerte pero menor |
| Arranque | 30 s - 2 min (BIOS, kernel, systemd, cloud-init) |
0,1 - 2 s (es lanzar un proceso) |
| Tamaño | AMI de 2-8 GB | Imagen de 80-400 MB bien construida |
| Densidad | Unidades por servidor físico | Decenas o centenares por servidor |
| Sobrecarga | Un sistema operativo completo por instancia | Solo el proceso y sus librerías |
| Portabilidad | AMI ligada a la región y al proveedor | La imagen corre igual en el portátil, en ECS, en EKS o en otro proveedor |
| Inmutabilidad | Posible, pero cuesta: hay que reconstruir la AMI | Natural: la imagen no se modifica, se sustituye |
| Estado | Persiste entre reinicios (EBS) | Efímero por definición; el estado va fuera |
La diferencia práctica que más importa para MercadoFresco está en dos filas: arranque y portabilidad. El arranque resuelve el pico de los viernes. La portabilidad resuelve la frase que Luis repite desde el módulo 8: «en mi máquina funciona». Con una imagen, en su máquina funciona exactamente lo mismo que en producción, byte a byte, porque es el mismo artefacto.
Y una advertencia honesta sobre el aislamiento, porque el marketing la suele omitir: los contenedores comparten el kernel del anfitrión. Una vulnerabilidad de escape de contenedor permite, en teoría, saltar de un contenedor a otro en la misma máquina. Por eso AWS no ejecuta contenedores de clientes distintos en el mismo kernel: Fargate (10-02) da a cada tarea su propia frontera de virtualización ligera. Para MercadoFresco, donde todos los contenedores son suyos, el riesgo es aceptable; para una plataforma multiinquilino, no lo sería.
graph TB
subgraph VM["Modelo de máquina virtual"]
H1[Hardware] --> HV[Hipervisor]
HV --> V1["VM 1<br/>SO completo<br/>+ app"]
HV --> V2["VM 2<br/>SO completo<br/>+ app"]
end
subgraph CT["Modelo de contenedor"]
H2[Hardware] --> SO["SO anfitrión<br/>+ kernel compartido"]
SO --> RT[Runtime de contenedores]
RT --> C1["Contenedor 1<br/>app + librerías"]
RT --> C2["Contenedor 2<br/>app + librerías"]
RT --> C3["Contenedor 3<br/>app + librerías"]
end
Imagen, capas, registro y contenedor en ejecución
Cuatro palabras que se confunden todo el rato y que conviene fijar:
- Imagen: un artefacto de solo lectura que contiene el sistema de archivos y los metadatos necesarios para arrancar el proceso. Es inmutable. Se identifica por un digest (
sha256:...) y, opcionalmente, por una o varias etiquetas. - Capa: una imagen no es un bloque monolítico sino una pila de capas, cada una con las diferencias que introdujo una instrucción del Dockerfile. Las capas se comparten entre imágenes: si diez imágenes usan
python:3.12-slim, esa capa se almacena y se descarga una sola vez. - Registro: el almacén de imágenes. Amazon ECR es el registro gestionado de AWS; Docker Hub es el registro público más conocido.
- Contenedor: una imagen en ejecución, con una capa de escritura efímera encima. Al parar el contenedor, esa capa desaparece.
La consecuencia operativa de las capas es enorme y define cómo se escribe un Dockerfile: si una capa cambia, todas las de abajo se invalidan. Poner COPY . . antes de pip install significa que cualquier cambio en cualquier fichero del proyecto obliga a reinstalar todas las dependencias. Poner primero el requirements.txt y después el código significa que un cambio de código reutiliza la capa de dependencias y la construcción baja de tres minutos a veinte segundos.
El Dockerfile de la tienda de MercadoFresco
La tienda es una aplicación Python con Gunicorn detrás del ALB. Este es su Dockerfile completo, con construcción multietapa, usuario sin privilegios, versiones fijadas y un HEALTHCHECK coherente con el /salud que usa tg-mercadofresco-tienda desde el módulo 3.
# ===== Etapa 1: construccion. Aqui vive el compilador; no llega a produccion. =====
FROM python:3.12.4-slim-bookworm AS constructor
# Herramientas que necesitan psycopg2 y algunas ruedas nativas: se quedan en esta etapa.
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential=12.9 libpq-dev=15.* \
&& rm -rf /var/lib/apt/lists/*
# Entorno virtual propio: se copia entero a la etapa final en un solo COPY.
RUN python -m venv /opt/entorno
ENV PATH="/opt/entorno/bin:$PATH"
WORKDIR /construccion
# --- Capa de dependencias, PRIMERO y sola: se reutiliza mientras no cambien
# las dependencias, aunque cambie todo el codigo de la tienda. ---
COPY requirements.txt requirements-bloqueo.txt ./
RUN pip install --no-cache-dir --require-hashes -r requirements-bloqueo.txt
# ===== Etapa 2: ejecucion. Minima, sin compiladores, sin root. =====
FROM python:3.12.4-slim-bookworm AS ejecucion
LABEL org.opencontainers.image.title="mercadofresco-tienda" \
org.opencontainers.image.vendor="MercadoFresco" \
org.opencontainers.image.source="https://github.com/mercadofresco/mercadofresco-tienda"
# Solo la libreria de cliente de PostgreSQL, no el -dev con las cabeceras.
RUN apt-get update && apt-get install -y --no-install-recommends libpq5=15.* curl=7.88.* \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --gid 10001 tienda \
&& useradd --uid 10001 --gid tienda --no-create-home --shell /usr/sbin/nologin tienda
# El entorno virtual completo llega de la etapa anterior: una sola capa.
COPY --from=constructor /opt/entorno /opt/entorno
ENV PATH="/opt/entorno/bin:$PATH" PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
# El codigo, al final: es lo que cambia en cada commit.
COPY --chown=tienda:tienda ./tienda ./tienda
COPY --chown=tienda:tienda ./gunicorn.conf.py ./
USER 10001
EXPOSE 8080
# Coherente con el health check del grupo de destino: mismo camino, mismo puerto.
HEALTHCHECK --interval=15s --timeout=3s --start-period=20s --retries=3 \
CMD curl --fail --silent http://127.0.0.1:8080/salud || exit 1
ENTRYPOINT ["gunicorn"]
CMD ["--config", "gunicorn.conf.py", "tienda.wsgi:aplicacion"]Cada decisión de ese fichero responde a un problema concreto:
python:3.12.4-slim-bookwormcon versión completa, nopython:3.12nipython:latest. Una etiqueta flotante convierte una construcción reproducible en una lotería: la misma orden, ejecutada dos semanas después, produce otra imagen. Es la disciplina del bloqueo deaws-cdk-liben 09-02. Yslimen lugar de la imagen completa baja de ~1,0 GB a ~130 MB: la diferencia es documentación y utilidades que en producción son superficie de ataque, no funcionalidad.- Multietapa.
build-essentialylibpq-devpesan más de 300 MB y solo hacen falta para compilar. Con dos etapas se quedan en la primera. Una imagen de producción con compilador es una imagen en la que un atacante puede compilar. --require-hashescon un fichero de bloqueo. Fija no solo la versión sino el hash de cada rueda descargada. Protege del ataque de sustitución de paquete en el índice.USER 10001con un UID numérico explícito. ECS y Kubernetes pueden imponer «no ejecutar como root»; un UID numérico permite verificar la regla sin resolver nombres, y el--no-create-home --shell /usr/sbin/nologinevita que ese usuario sirva para nada más.HEALTHCHECKcon--start-period. Los 20 segundos de gracia evitan que el contenedor se marque como no sano mientras Gunicorn levanta sus procesos hijos. En ECS es complementario al del grupo de destino: el del contenedor decide si ECS reinicia la tarea; el del ALB decide si le manda tráfico.ENTRYPOINT+CMDseparados. ElENTRYPOINTfija el ejecutable y elCMDlos argumentos por omisión, de modo que la definición de tarea puede sobrescribir solo los argumentos (command) sin cambiar el binario.
.dockerignore, tamaño y caché de capas
El .dockerignore es el fichero que más gente olvida y el que más problemas causa. Sin él, COPY . . mete en la imagen el directorio .git completo —que puede pesar cientos de megas y contiene el historial, incluidos secretos borrados—, el entorno virtual local, la caché de pruebas y los ficheros .env con credenciales de desarrollo.
.git .gitignore .github .venv venv __pycache__ *.pyc *.pyo .pytest_cache .mypy_cache .coverage htmlcov .env .env.* *.log infra-cdk cdk.out docs README.md Dockerfile .dockerignore
En el fichero real va una entrada por línea; aquí se agrupan por familia. Reglas de tamaño y caché que se aplican en este orden:
| Regla | Efecto | Aplicación en MercadoFresco |
|---|---|---|
| Base mínima | -80 % de tamaño | slim en vez de la completa; distroless si no hiciera falta curl |
| Multietapa | Quita compiladores | Etapa constructor con build-essential |
| Ordenar de estable a volátil | Máxima reutilización de caché | requirements.txt antes que el código |
Agrupar RUN con && |
Menos capas, sin residuos | apt-get update && install && rm -rf /var/lib/apt/lists/* |
| Limpiar en la misma capa | Lo borrado en otra capa sigue ocupando | El rm -rf va pegado al apt-get, no en un RUN aparte |
--no-cache-dir en pip |
-50-150 MB | En la etapa de construcción |
.dockerignore completo |
Evita contexto enorme y fugas | El de arriba |
El punto que más sorprende es el de «limpiar en la misma capa». Como cada capa guarda diferencias, borrar en la capa 7 un fichero creado en la capa 5 no reduce el tamaño de la imagen: el fichero sigue en la capa 5 y sigue descargándose. Solo desaparece de la vista. Un RUN apt-get install ... seguido de un RUN rm -rf /var/lib/apt/lists/* en línea aparte no ahorra nada.
En CodeBuild (08-02), la caché se aprovecha con docker pull "$REPO:cache" || true seguido de docker build --cache-from "$REPO:cache" --build-arg BUILDKIT_INLINE_CACHE=1 ..., apuntando a la imagen anterior del registro. Con este patrón, la construcción de la tienda baja de unos 3 minutos a 35-50 segundos cuando solo cambia el código, que es el caso habitual.
Amazon ECR: el registro que sustituye a las AMI
Amazon Elastic Container Registry es el registro de imágenes gestionado de AWS: privado por omisión, integrado con IAM, con cifrado en reposo, escaneo de vulnerabilidades, replicación y políticas de ciclo de vida. Ocupa exactamente el lugar que ocupaba la AMI en la arquitectura anterior, con dos diferencias: es regional pero replicable, y la unidad de versión es el digest, no un identificador opaco. MercadoFresco crea dos repositorios en la cuenta de herramientas 555566667777, que es donde vive el pipeline desde 09-04:
for REPO in mercadofresco/tienda mercadofresco/trabajadores; do
aws ecr create-repository --repository-name "$REPO" --region eu-west-1 \
--image-tag-mutability IMMUTABLE \
--image-scanning-configuration scanOnPush=true \
--encryption-configuration '{"encryptionType":"KMS","kmsKey":"alias/mercadofresco-datos"}' \
--tags Key=Proyecto,Value=mercadofresco Key=Entorno,Value=compartido \
Key=Componente,Value=registro-imagenes Key=Propietario,Value=plataforma \
Key=CentroCoste,Value=tecnologia
done
# La autenticacion no usa contrasenas guardadas: token temporal de IAM entregado a Docker.
aws ecr get-login-password --region eu-west-1 \
| docker login --username AWS --password-stdin 555566667777.dkr.ecr.eu-west-1.amazonaws.comEl token dura 12 horas y va asociado al principal de IAM que hizo la llamada. En CodeBuild, la línea anterior es literalmente la primera de la fase pre_build del buildspec.yml, y el rol del proyecto de compilación necesita ecr:GetAuthorizationToken (que es a nivel de cuenta, con Resource: "*") más los permisos de escritura sobre el repositorio concreto.
version: 0.2
env:
variables: { REGION: eu-west-1, CUENTA_HERRAMIENTAS: "555566667777", REPOSITORIO: mercadofresco/tienda }
phases:
pre_build:
commands:
- REGISTRO="${CUENTA_HERRAMIENTAS}.dkr.ecr.${REGION}.amazonaws.com"; URI="${REGISTRO}/${REPOSITORIO}"
- SHA_CORTO=$(echo "$CODEBUILD_RESOLVED_SOURCE_VERSION" | cut -c1-7)
- ETIQUETA="${VERSION_SEMANTICA}-${SHA_CORTO}" # p. ej. v1.6.0-a3f9c21
- aws ecr get-login-password --region "$REGION" | docker login --username AWS --password-stdin "$REGISTRO"
build:
commands:
- docker build --cache-from "${URI}:cache" --build-arg BUILDKIT_INLINE_CACHE=1 -t "${URI}:${ETIQUETA}" -t "${URI}:cache" .
- docker run --rm "${URI}:${ETIQUETA}" python -c "import tienda; print(tienda.__version__)"
post_build:
commands:
- docker push "${URI}:${ETIQUETA}" && docker push "${URI}:cache"
# El digest es lo que se despliega de verdad; se pasa a la fase siguiente.
- DIGEST=$(aws ecr describe-images --repository-name "$REPOSITORIO" --image-ids imageTag="$ETIQUETA" --query 'imageDetails[0].imageDigest' --output text)
- printf '{"imagen":"%s@%s","etiqueta":"%s"}' "$URI" "$DIGEST" "$ETIQUETA" > imagen.json
artifacts:
files: [imagen.json]Etiquetado, inmutabilidad y ciclo de vida de las imágenes
latest es una etiqueta como cualquier otra: no significa «la más reciente», solo significa «la que alguien etiquetó así la última vez». Y como etiqueta de despliegue es directamente peligrosa:
Problema de latest |
Consecuencia concreta |
|---|---|
| No es reproducible | «Desplegamos latest» no dice qué código hay en producción |
| No se puede revertir | Volver atrás exige saber cuál era la anterior, y latest ya no lo es |
| Rompe el reinicio | Una tarea que se reinicia descarga otra imagen distinta de sus hermanas |
| Convivencia mixta | Con imagePullPolicy agresivo, unas tareas ejecutan una versión y otras, otra |
| Auditoría imposible | CloudTrail registra un despliegue, pero no qué se desplegó |
MercadoFresco usa el esquema que ya introdujo el módulo 8: versión semántica más SHA corto del commit, por ejemplo v1.6.0-a3f9c21. La versión semántica la lee un humano; el SHA la ata a un commit exacto del repositorio mercadofresco-tienda. Y en la definición de tarea que se despliega, no se usa la etiqueta sino el digest:
555566667777.dkr.ecr.eu-west-1.amazonaws.com/mercadofresco/tienda@sha256:9c1e...f4a2
La razón es sutil pero decisiva: una etiqueta es un puntero mutable —aunque la inmutabilidad la proteja dentro del repositorio, un fallo de configuración o una replicación mal hecha pueden desalinearla—, mientras que el digest es el contenido. Desplegar por digest garantiza que lo que arranca hoy en producción es exactamente lo que pasó las pruebas ayer en preproducción, bit a bit. La etiqueta sirve para que un humano encuentre la imagen; el digest, para que la máquina la despliegue.
Con --image-tag-mutability IMMUTABLE, ECR rechaza cualquier intento de reutilizar una etiqueta ya existente. Eso convierte un accidente silencioso —sobrescribir v1.6.0-a3f9c21 con otro contenido— en un error de compilación visible.
El otro control imprescindible es la política de ciclo de vida. Sin ella, el repositorio crece indefinidamente: MercadoFresco despliega 4,8 veces por semana, con imágenes de unos 220 MB, y sumando las de las ramas de característica el registro engorda unos 6 GB al año por repositorio. No es dinero, pero sí es ruido y sí complica las auditorías.
{"rules": [
{"rulePriority": 1, "description": "Conservar las 10 ultimas versiones publicadas", "action": {"type": "expire"},
"selection": {"tagStatus": "tagged", "tagPrefixList": ["v"], "countType": "imageCountMoreThan", "countNumber": 10}},
{"rulePriority": 2, "description": "Imagenes de rama de caracteristica: 14 dias", "action": {"type": "expire"},
"selection": {"tagStatus": "tagged", "tagPrefixList": ["rama-"], "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 14}},
{"rulePriority": 3, "description": "Capas huerfanas sin etiqueta: 1 dia", "action": {"type": "expire"},
"selection": {"tagStatus": "untagged", "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 1}}
]}Tres advertencias sobre estas políticas, que son la fuente número uno de sustos con ECR:
- Las reglas se evalúan por prioridad y una imagen solo la afecta la primera que la selecciona. Una regla amplia con prioridad 1 anula todo lo que venga detrás.
- La expiración no comprueba si la imagen está en uso. ECR borrará la imagen que está ejecutando producción si la regla la selecciona. Por eso la regla 1 conserva diez versiones publicadas y no dos: hay que cubrir el peor caso de reversión.
- Se prueba primero en seco. La consola ofrece una vista previa de la política sobre el contenido real del repositorio; ejecutarla antes de aplicar es obligatorio.
Escaneo de vulnerabilidades: básico y mejorado
ECR ofrece dos modos de escaneo, y la diferencia entre ambos es mayor de lo que sugiere el nombre.
| Aspecto | Escaneo básico | Escaneo mejorado (Amazon Inspector) |
|---|---|---|
| Motor | Base de datos de CVE del sistema operativo | Amazon Inspector, continuo |
| Alcance | Paquetes del sistema operativo | Sistema operativo y dependencias de la aplicación (Python, npm, Java, Go) |
| Cuándo | Al empujar la imagen (o manual) | Al empujar y de forma continua al aparecer CVE nuevas |
| Alcance de cuenta | Por repositorio | A nivel de registro, se activa para toda la cuenta |
| Integración | Consola y API | Security Hub, EventBridge, panel de Inspector |
| Coste | Gratuito | Se cobra por imagen escaneada y por reescaneo |
Para MercadoFresco importa la fila de «alcance»: la mayoría de las vulnerabilidades reales de la tienda están en dependencias de Python —una versión antigua de una librería de serialización, por ejemplo—, y el escaneo básico no las ve. Marta activa el escaneo mejorado en la cuenta de herramientas y conecta el resultado a la puerta de calidad del pipeline:
aws ecr put-registry-scanning-configuration --scan-type ENHANCED --region eu-west-1 \
--rules '[{"scanFrequency": "CONTINUOUS_SCAN",
"repositoryFilters": [{"filter": "mercadofresco/*", "filterType": "WILDCARD"}]}]'
# La regla de bloqueo que ejecuta la Lambda mercadofresco-puerta-calidad (modulo 8):
aws ecr describe-image-scan-findings --repository-name mercadofresco/tienda \
--image-id imageTag=v1.6.0-a3f9c21 \
--query 'imageScanFindingsSummary.findingSeverityCounts'
# {"HIGH": 0, "MEDIUM": 3, "LOW": 12} -> pasa: la regla bloquea CRITICAL y HIGHLa política acordada es pragmática y por eso se cumple: CRITICAL bloquea siempre; HIGH bloquea salvo excepción documentada con fecha de caducidad; MEDIUM y LOW generan un ticket. Una política que bloquea con MEDIUM no sobrevive dos semanas: alguien la desactiva y entonces no queda ninguna. El escaneo continuo tiene además una propiedad que el básico no puede dar: la imagen que hoy está limpia y mañana no. Cuando aparece una CVE nueva que afecta a una imagen ya desplegada, Inspector emite un evento a EventBridge y bus-mercadofresco (07-03) lo enruta a alertas-mercadofresco. Ese aviso es el sustituto directo del ciclo de parcheo de las instancias.
Replicación, cifrado y política de repositorio
La imagen se construye en la cuenta de herramientas 555566667777 y se ejecuta en producción 111122223333, preproducción 222233334444 y desarrollo 333344445555. Hay dos formas de resolverlo:
| Opción | Cómo funciona | Cuándo conviene |
|---|---|---|
| Repositorio central compartido | Un solo repositorio en herramientas con política que autoriza a las cuentas de carga | Menos copias, una fuente de verdad, dependencia de una cuenta |
| Replicación entre cuentas | ECR copia la imagen a un repositorio en cada cuenta destino | Aísla fallos, permite políticas distintas, duplica almacenamiento |
MercadoFresco usa repositorio central compartido para desarrollo y preproducción, y replicación hacia producción, porque quiere que un incidente en la cuenta de herramientas no impida que producción escale el viernes por la tarde.
aws ecr put-replication-configuration --region eu-west-1 --replication-configuration '{
"rules": [{"destinations": [{"region": "eu-west-1", "registryId": "111122223333"},
{"region": "eu-central-1", "registryId": "111122223333"}],
"repositoryFilters": [{"filter": "mercadofresco/", "filterType": "PREFIX_MATCH"}]}]}'La segunda línea de destino replica además a eu-central-1, que es donde MercadoFresco tendría su plan de recuperación si la región principal fallara. La replicación es asíncrona y tarda de segundos a algunos minutos según el tamaño; el pipeline debe esperar a que la imagen exista en destino antes de desplegar, con un wait explícito.
La política de repositorio autoriza a las cuentas de la organización a descargar, sin autorizar a nadie a subir:
{"Version": "2012-10-17", "Statement": [
{"Sid": "DescargaDesdeLaOrganizacion", "Effect": "Allow", "Principal": "*",
"Action": ["ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:BatchCheckLayerAvailability"],
"Condition": {"StringEquals": {"aws:PrincipalOrgID": "o-a1b2c3d4e5"}}},
{"Sid": "SoloElPipelinePublica", "Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::555566667777:role/rol-build-mercadofresco-tienda"},
"Action": ["ecr:PutImage", "ecr:InitiateLayerUpload", "ecr:UploadLayerPart", "ecr:CompleteLayerUpload"]}
]}Es el mismo patrón de 09-04: aws:PrincipalOrgID en lugar de una lista de cuentas que habría que mantener a mano. Y la separación entre quién lee y quién escribe es la que convierte al pipeline en el único camino hacia producción. El cifrado con alias/mercadofresco-datos (04-02) tiene una consecuencia que sorprende: si la clave es de la cuenta de herramientas y producción debe descargar la imagen, la política de la clave KMS debe autorizar a los principales de producción a usar kms:Decrypt. Es el error clásico de las claves gestionadas por el cliente entre cuentas: la política del repositorio está bien y la descarga falla igualmente, con un mensaje que habla de acceso denegado y no menciona KMS.
Amazon ECS: clúster, definición de tarea, tarea y servicio
Amazon Elastic Container Service es el orquestador de contenedores propio de AWS. Su modelo tiene cuatro objetos y conviene no confundirlos nunca:
| Objeto | Qué es | Analogía con lo ya visto |
|---|---|---|
| Clúster | Agrupación lógica donde se ejecutan las tareas | Similar a un ASG más su entorno |
| Definición de tarea | La plantilla inmutable y versionada: qué imágenes, cuánta CPU y memoria, qué red, qué roles | La plantilla de lanzamiento lt-mercadofresco-tienda |
| Tarea | Una instancia en ejecución de una definición de tarea; uno o varios contenedores que viven y mueren juntos | Una instancia EC2 del ASG |
| Servicio | Mantiene N tareas en ejecución, las registra en el ALB, las repone y las despliega | El ASG asg-mercadofresco-tienda |
La correspondencia con lo que MercadoFresco ya tiene es casi uno a uno, y es la mejor forma de entender ECS: el servicio hace lo que hacía el ASG, la definición de tarea hace lo que hacía la plantilla de lanzamiento más la AMI, y la tarea sustituye a la instancia. Las definiciones de tarea son versionadas e inmutables: cada register-task-definition crea una revisión nueva (mercadofresco-tienda:23) y las anteriores siguen existiendo. Revertir un despliegue es apuntar el servicio a la revisión anterior, y eso es un cambio de un campo.
graph LR TD["Definicion de tarea<br/>mercadofresco-tienda:23"] --> SRV["Servicio<br/>svc-mercadofresco-tienda<br/>desiredCount = 4"] SRV --> T1["Tarea 1<br/>AZ a"] SRV --> T2["Tarea 2<br/>AZ a"] SRV --> T3["Tarea 3<br/>AZ b"] SRV --> T4["Tarea 4<br/>AZ b"] ALB["alb-mercadofresco-tienda"] --> TG["tg-mercadofresco-tienda<br/>tipo de destino: ip"] TG --> T1 TG --> T2 TG --> T3 TG --> T4 ECR["ECR<br/>mercadofresco/tienda"] -.imagen.-> TD CL["Clúster<br/>ecs-mercadofresco"] --- SRV
Tipos de lanzamiento, agente de ECS y proveedores de capacidad
ECS ejecuta las tareas de dos maneras, y la elección cambia mucho más el día a día que la arquitectura.
| Aspecto | Tipo de lanzamiento EC2 | Tipo de lanzamiento Fargate (10-02) |
|---|---|---|
| Quién gestiona las máquinas | Tú: AMI, parches, escalado del clúster | AWS: no ves máquinas |
| Unidad de facturación | La instancia EC2, esté llena o vacía | vCPU-hora y GB-hora de la tarea |
| Densidad | Puedes empaquetar muchas tareas por instancia | Una tarea es su propia unidad |
| Arranque de tarea | 5-15 s si hay hueco; 2 min si hay que añadir instancia | 30-45 s siempre |
| GPU, instancias especiales | Sí | No (o con limitaciones) |
| Modos de red | awsvpc, bridge, host, none |
Solo awsvpc |
| Acceso al anfitrión | SSH, DaemonSet, volúmenes del host | No: ECS Exec para depurar |
| Coste con uso alto y constante | Más barato si el empaquetado es bueno | Más caro por unidad, sin coste ocioso |
En el tipo de lanzamiento EC2, cada instancia ejecuta el agente de ECS (amazon-ecs-agent), un contenedor que se registra en el clúster, informa de los recursos disponibles y recibe las órdenes del plano de control; la forma correcta de crear esas instancias es con la AMI optimizada para ECS, que ya lleva el agente y el runtime. Los proveedores de capacidad son la abstracción que evita gestionar el escalado del clúster a mano. Un proveedor de capacidad para EC2 se asocia a un ASG y activa el escalado gestionado: ECS calcula cuántas instancias hacen falta para las tareas pendientes y ajusta la capacidad deseada del ASG por sí mismo.
aws ecs create-capacity-provider --name cp-mercadofresco-ec2 --auto-scaling-group-provider '{
"autoScalingGroupArn": "arn:aws:autoscaling:eu-west-1:111122223333:autoScalingGroup:...:autoScalingGroupName/asg-mercadofresco-tienda",
"managedScaling": { "status": "ENABLED", "targetCapacity": 90, "minimumScalingStepSize": 1, "maximumScalingStepSize": 4 },
"managedTerminationProtection": "ENABLED" }'targetCapacity: 90 significa «mantén el clúster al 90 % de ocupación»: deja un 10 % de holgura para absorber una subida sin esperar a una instancia nueva. Y managedTerminationProtection evita que el ASG termine una instancia que aún ejecuta tareas, el fallo más molesto del modelo EC2. Aun así, ese 2 min de la tabla sigue ahí en la peor situación: si no hay hueco, hace falta una instancia, y una instancia tarda lo que tarda. Es exactamente el motivo por el que MercadoFresco no se quedará aquí y 10-02 existe.
La definición de tarea de la tienda, comentada
Esta es la definición completa de mercadofresco-tienda, con secretos inyectados, registros hacia CloudWatch y modo de red awsvpc.
{
"family": "mercadofresco-tienda",
"networkMode": "awsvpc",
"requiresCompatibilities": ["EC2", "FARGATE"],
"cpu": "1024", "memory": "2048",
"executionRoleArn": "arn:aws:iam::111122223333:role/rol-ejecucion-mercadofresco-tienda",
"taskRoleArn": "arn:aws:iam::111122223333:role/rol-tarea-mercadofresco-tienda",
"runtimePlatform": { "cpuArchitecture": "X86_64", "operatingSystemFamily": "LINUX" },
"containerDefinitions": [
{
"name": "tienda",
"image": "555566667777.dkr.ecr.eu-west-1.amazonaws.com/mercadofresco/tienda@sha256:9c1e...f4a2",
"essential": true, "cpu": 896, "memoryReservation": 1536, "memory": 1792,
"portMappings": [{ "name": "http", "containerPort": 8080, "protocol": "tcp", "appProtocol": "http" }],
"environment": [
{ "name": "ENTORNO", "value": "produccion" },
{ "name": "COLA_PEDIDOS", "value": "cola-mercadofresco-pedidos" },
{ "name": "TABLA_CARRITOS", "value": "mercadofresco-carritos" },
{ "name": "AWS_XRAY_DAEMON_ADDRESS", "value": "127.0.0.1:2000" }
],
"secrets": [
{ "name": "BD_CONTRASENA",
"valueFrom": "arn:aws:secretsmanager:eu-west-1:111122223333:secret:mercadofresco/produccion/rds/mfadmin:password::" },
{ "name": "BD_USUARIO",
"valueFrom": "arn:aws:secretsmanager:eu-west-1:111122223333:secret:mercadofresco/produccion/rds/mfadmin:username::" },
{ "name": "ENDPOINT_CACHE",
"valueFrom": "arn:aws:ssm:eu-west-1:111122223333:parameter/mercadofresco/produccion/cache/endpoint" }
],
"logConfiguration": {
"logDriver": "awslogs",
"options": { "awslogs-group": "/ecs/mercadofresco-tienda", "awslogs-region": "eu-west-1",
"awslogs-stream-prefix": "tienda", "awslogs-create-group": "true",
"mode": "non-blocking", "max-buffer-size": "4m" }
},
"healthCheck": {
"command": ["CMD-SHELL", "curl -f -s http://127.0.0.1:8080/salud || exit 1"],
"interval": 15, "timeout": 3, "retries": 3, "startPeriod": 30
},
"readonlyRootFilesystem": true,
"linuxParameters": { "initProcessEnabled": true },
"mountPoints": [{ "sourceVolume": "temporal", "containerPath": "/tmp", "readOnly": false }],
"stopTimeout": 30
},
{
"name": "xray",
"image": "public.ecr.aws/xray/aws-xray-daemon:3.x",
"essential": false, "cpu": 32, "memoryReservation": 256,
"portMappings": [{ "containerPort": 2000, "protocol": "udp" }],
"logConfiguration": { "logDriver": "awslogs", "options": {
"awslogs-group": "/ecs/mercadofresco-tienda", "awslogs-region": "eu-west-1",
"awslogs-stream-prefix": "xray" } }
}
],
"volumes": [{ "name": "temporal" }],
"tags": [ { "key": "Proyecto", "value": "mercadofresco" }, { "key": "Entorno", "value": "produccion" },
{ "key": "Componente", "value": "tienda" }, { "key": "Propietario", "value": "plataforma" },
{ "key": "CentroCoste", "value": "tecnologia" } ]
}Los puntos que hay que entender de ese JSON:
- CPU y memoria a dos niveles. El
cpuymemoryde la tarea son el total reservado; los de cada contenedor reparten ese total. Con Fargate, los valores de tarea son obligatorios y solo admiten combinaciones concretas (10-02); con EC2 pueden omitirse y entonces la reserva se hace solo por contenedor. memoryReservationfrente amemory.memoryReservationes el mínimo garantizado que ECS usa para colocar la tarea;memoryes el límite duro: si el contenedor lo supera, el kernel lo mata con OOM. Fijar solomemorydesperdicia; fijar solomemoryReservationdeja que un contenedor con fuga se coma la instancia. Se ponen los dos.essential: trueen la tienda yfalseen el sidecar de X-Ray. Si un contenedor esencial muere, ECS mata la tarea entera y la repone. El demonio de X-Ray no debe tumbar la tienda si se cae.secretsen lugar deenvironmentpara lo sensible. El agente de ECS resuelve el valor en el arranque usando el rol de ejecución y lo inyecta como variable de entorno. Nunca aparece en la definición de tarea, ni en la consola, ni en CloudTrail. La sintaxis:password::selecciona una clave concreta del JSON del secreto (04-03).mode: non-blockingen los registros. Es la opción que evita el fallo más desagradable del controladorawslogs: si CloudWatch Logs va lento, en modo bloqueante la aplicación se para esperando a escribir su log. Connon-blockingse pierden líneas en el peor caso, que es infinitamente mejor que perder pedidos.readonlyRootFilesystem: truecon un volumen para/tmp. El sistema de archivos de solo lectura impide que un atacante escriba binarios; el volumen efímero da el/tmpque Python necesita.stopTimeout: 30. Segundos que ECS espera entre elSIGTERMy elSIGKILL. Es el margen que tiene Gunicorn para terminar las peticiones en curso durante un despliegue.
Rol de ejecución de la tarea frente a rol de la tarea
Es la confusión número uno de quien empieza con ECS, y produce errores que parecen no tener sentido: la tarea no arranca pero los permisos de la aplicación están bien, o la tarea arranca y la aplicación no puede leer una cola.
Rol de ejecución de la tarea (executionRoleArn) |
Rol de la tarea (taskRoleArn) |
|
|---|---|---|
| Quién lo asume | El agente de ECS, antes de arrancar el contenedor | El código de tu aplicación, dentro del contenedor |
| Para qué sirve | Descargar la imagen de ECR, escribir en CloudWatch Logs, resolver los secrets |
Llamar a las API de AWS que necesita la aplicación |
| Cuándo se usa | En la fase de aprovisionamiento | Durante toda la vida de la tarea |
| Síntoma si falta o falla | La tarea no llega a arrancar: CannotPullContainerError, ResourceInitializationError |
La aplicación arranca y falla con AccessDenied al llamar a AWS |
| Es obligatorio | Sí en Fargate y con secretos o awslogs |
No, pero sin él la aplicación no puede llamar a nada |
{"Version": "2012-10-17", "Statement": [
{"Sid": "DescargarImagenDeECR", "Effect": "Allow", "Resource": "*",
"Action": ["ecr:GetAuthorizationToken", "ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage"]},
{"Sid": "DescifrarLaImagen", "Effect": "Allow", "Action": ["kms:Decrypt"],
"Resource": "arn:aws:kms:eu-west-1:555566667777:key/*",
"Condition": {"StringEquals": {"kms:ViaService": "ecr.eu-west-1.amazonaws.com"}}},
{"Sid": "EscribirRegistros", "Effect": "Allow",
"Action": ["logs:CreateLogStream", "logs:PutLogEvents", "logs:CreateLogGroup"],
"Resource": "arn:aws:logs:eu-west-1:111122223333:log-group:/ecs/mercadofresco-*:*"},
{"Sid": "ResolverSecretos", "Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue", "ssm:GetParameters"],
"Resource": ["arn:aws:secretsmanager:eu-west-1:111122223333:secret:mercadofresco/produccion/*",
"arn:aws:ssm:eu-west-1:111122223333:parameter/mercadofresco/produccion/*"]}
]}Y el rol de la tarea, que es el que usa el código y por tanto el que debe ser mínimo:
{"Version": "2012-10-17", "Statement": [
{"Effect": "Allow", "Action": ["sqs:SendMessage"],
"Resource": "arn:aws:sqs:eu-west-1:111122223333:cola-mercadofresco-pedidos"},
{"Effect": "Allow", "Action": ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem"],
"Resource": "arn:aws:dynamodb:eu-west-1:111122223333:table/mercadofresco-carritos"},
{"Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "arn:aws:s3:::mercadofresco-catalogo-fotos/*"},
{"Effect": "Allow", "Action": ["xray:PutTraceSegments", "xray:PutTelemetryRecords"], "Resource": "*"}
]}La regla mnemotécnica que evita el 90 % de los errores: el rol de ejecución trabaja para AWS, el rol de tarea trabaja para tu código. Si el fallo ocurre antes de ver un solo log de la aplicación, es el de ejecución. Si el fallo aparece en los logs de la aplicación, es el de tarea.
Modo de red awsvpc y qué implica
Con networkMode: awsvpc, cada tarea recibe su propia interfaz de red elástica (ENI) con su propia IP privada dentro de la VPC. Las consecuencias son concretas y todas relevantes para MercadoFresco:
- Grupos de seguridad por tarea.
sg-mercadofresco-tiendase aplica ahora a la tarea, no a la instancia. Dos servicios distintos en la misma instancia pueden tener reglas distintas, cosa imposible conbridge. - Sin conflictos de puerto. En
bridgehabía que usarhostPort: 0para asignación dinámica y el grupo de destino registrabainstancia:puerto. Conawsvpc, cada tarea escucha en su propio 8080 y el grupo de destino es de tipoip. - La tarea consume una IP de la subred. Con
snet-mercadofresco-app-ay-bde /20 hay direcciones de sobra, pero en subredes pequeñas es un límite real que se alcanza antes de lo esperado. - Límite de ENI por instancia. En el tipo de lanzamiento EC2, cada instancia soporta un número limitado de ENI según su tamaño. Sin el ajuste
awsvpcTrunkingactivado (aws ecs put-account-setting-default --name awsvpcTrunking --value enabled), unam5.largesolo puede ejecutar dos o tres tareas conawsvpc, por mucha CPU libre que tenga. Es una de las trampas más caras del modelo EC2. - Sin acceso al
localhostdel anfitrión. Los contenedores de una misma tarea sí se ven entre sí por127.0.0.1—por eso el sidecar de X-Ray funciona conAWS_XRAY_DAEMON_ADDRESS=127.0.0.1:2000—, pero no ven a los contenedores de otras tareas.
| Modo de red | Aislamiento | SG por tarea | Puertos | Disponible en Fargate |
|---|---|---|---|---|
awsvpc |
ENI propia por tarea | Sí | Sin conflictos | Sí (único) |
bridge |
Red virtual del anfitrión | No (son de la instancia) | Necesita mapeo dinámico | No |
host |
Comparte la pila de red del anfitrión | No | Conflictos directos | No |
none |
Sin red | — | — | No |
El servicio: ALB, comprobaciones de estado y colocación
El servicio es lo que convierte la tarea en algo que se puede llamar producción: mantiene el número deseado, repone lo que muere, registra en el ALB y gestiona los despliegues.
aws ecs create-service --cluster ecs-mercadofresco --region eu-west-1 \
--service-name svc-mercadofresco-tienda --task-definition mercadofresco-tienda:23 \
--desired-count 4 \
--capacity-provider-strategy capacityProvider=cp-mercadofresco-ec2,weight=1,base=2 \
--network-configuration 'awsvpcConfiguration={subnets=[snet-mercadofresco-app-a,snet-mercadofresco-app-b],
securityGroups=[sg-mercadofresco-tienda],assignPublicIp=DISABLED}' \
--load-balancers 'targetGroupArn=arn:aws:elasticloadbalancing:eu-west-1:111122223333:targetgroup/tg-mercadofresco-tienda/abc123,containerName=tienda,containerPort=8080' \
--health-check-grace-period-seconds 60 \
--deployment-configuration '{"minimumHealthyPercent": 100, "maximumPercent": 200,
"deploymentCircuitBreaker": { "enable": true, "rollback": true }}' \
--placement-strategy 'type=spread,field=attribute:ecs.availability-zone' 'type=spread,field=instanceId' \
--enable-execute-command --propagate-tags SERVICETres detalles que importan:
--health-check-grace-period-seconds 60. Es el período durante el cual el servicio ignora el estado del grupo de destino tras arrancar una tarea. Sin él, una aplicación que tarda 40 segundos en calentar la caché entra en un bucle infinito: el ALB la marca como no sana, ECS la mata, arranca otra, y así indefinidamente. Es el error más frustrante de todos porque no da ninguna pista.- Grupo de destino de tipo
ip. Obligatorio conawsvpc. Sitg-mercadofresco-tiendase creó en el módulo 3 con tipoinstance, hay que crear un grupo de destino nuevo: el tipo no se puede cambiar. - Estrategias de colocación (solo con tipo de lanzamiento EC2, no aplican en Fargate):
| Estrategia | Qué hace | Cuándo usarla |
|---|---|---|
spread por AZ |
Reparte por zona de disponibilidad | Siempre, la primera: es la disponibilidad |
spread por instanceId |
Reparte entre instancias | Segunda: evita perder varias tareas con una instancia |
binpack por memoria o CPU |
Llena una instancia antes de usar la siguiente | Optimizar coste con cargas tolerantes |
random |
Al azar | Casi nunca |
Para la tienda, spread por AZ y luego por instancia. Para los trabajadores de la cola, que son tolerantes a fallos, binpack por memoria ahorra instancias.
Despliegues: rolling, porcentajes y circuit breaker
El despliegue por omisión de ECS es la actualización renovada (ECS rolling update). El servicio arranca tareas con la revisión nueva, espera a que estén sanas en el grupo de destino, drena y para las viejas, y repite. Los dos parámetros que lo gobiernan son:
minimumHealthyPercent: porcentaje deldesiredCountque debe seguir sano durante el despliegue.maximumPercent: porcentaje máximo que puede estar en ejecución a la vez.
| Configuración | Comportamiento con desiredCount = 4 |
Uso |
|---|---|---|
100 / 200 |
Arranca 4 nuevas, luego para 4 viejas. Nunca menos de 4 sanas | Producción: sin pérdida de capacidad, coste doble temporal |
50 / 100 |
Para 2, arranca 2, repite. Nunca más de 4 | Sin capacidad de sobra; degrada durante el despliegue |
100 / 150 |
Ventana intermedia, tanda a tanda | Compromiso razonable |
0 / 100 |
Para todo y arranca todo | Solo desarrollo: hay corte |
MercadoFresco usa 100 / 200 en producción y 50 / 100 en desarrollo, donde el coste importa más que la disponibilidad. Con 100 / 200 y un arranque de tarea de 15 segundos, un despliegue completo de cuatro tareas termina en poco más de un minuto, frente a los ocho o nueve del ASG con instancias.
El circuit breaker de despliegue es la red de seguridad. Con enable: true, ECS cuenta los fallos consecutivos de arranque de tarea; al superar el umbral —calculado a partir del desiredCount, con un mínimo de 10— declara el despliegue fallido. Con rollback: true, además revierte automáticamente a la última revisión que sí funcionó.
Esto sustituye, en el caso más frecuente, a la alarma manual: si la imagen nueva tiene un error de configuración que impide arrancar, el despliegue se para y se revierte solo. Lo que el circuit breaker no detecta es una imagen que arranca bien y responde mal: para eso hacen falta las alarmas de CloudWatch asociadas al despliegue, con alarmNames en la configuración, y el blue/green. El despliegue blue/green con CodeDeploy (08-03) es la otra opción: se cambia el controlador de despliegue a CODE_DEPLOY, y CodeDeploy levanta el conjunto verde completo en tg-mercadofresco-verde, permite probarlo por un puerto de pruebas y desplaza el tráfico de golpe o en canario. Es lo que MercadoFresco integra en el pipeline en 10-02, cuando el servicio ya esté sobre Fargate.
Autoescalado del servicio y descubrimiento con Cloud Map
El autoescalado de ECS lo proporciona Application Auto Scaling, el mismo servicio que escala Aurora Serverless o DynamoDB. Se registra el servicio como destino escalable y se le asocia una política.
DESTINO="--service-namespace ecs --scalable-dimension ecs:service:DesiredCount \
--resource-id service/ecs-mercadofresco/svc-mercadofresco-tienda"
aws application-autoscaling register-scalable-target $DESTINO --min-capacity 4 --max-capacity 20
aws application-autoscaling put-scaling-policy $DESTINO --policy-type TargetTrackingScaling \
--policy-name seguimiento-peticiones-por-destino \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 120.0, "ScaleInCooldown": 300, "ScaleOutCooldown": 30,
"PredefinedMetricSpecification": { "PredefinedMetricType": "ALBRequestCountPerTarget",
"ResourceLabel": "app/alb-mercadofresco-tienda/abc123/targetgroup/tg-mercadofresco-tienda/def456" } }'La elección de métrica no es indiferente:
| Métrica | Qué mide | Cuándo es la correcta |
|---|---|---|
ECSServiceAverageCPUUtilization |
CPU media del servicio | La carga es proporcional a la CPU |
ECSServiceAverageMemoryUtilization |
Memoria media | Rara vez: la memoria no baja al bajar la carga |
ALBRequestCountPerTarget |
Peticiones por destino | La tienda: reacciona antes que la CPU |
Métrica propia (ApproximateNumberOfMessagesVisible) |
Profundidad de cola | Los trabajadores de cola-mercadofresco-pedidos |
Para la tienda, ALBRequestCountPerTarget con destino 120 es mejor que la CPU por una razón de tiempo: el tráfico sube antes que la CPU, así que escalar por peticiones adelanta la reacción entre 30 y 60 segundos. Y los enfriamientos son asimétricos a propósito: 30 segundos para crecer, 300 para decrecer. Crecer tarde cuesta pedidos; decrecer pronto cuesta una oscilación. Para los trabajadores, la métrica correcta es la profundidad de la cola dividida entre el número de tareas —el «backlog por tarea» de 07-01—, calculada como métrica matemática de CloudWatch y usada en una política de seguimiento de destino personalizada.
Descubrimiento de servicios con Cloud Map
Cuando un servicio necesita llamar a otro sin pasar por el ALB, la pregunta es cómo encuentra su IP, que cambia con cada tarea. AWS Cloud Map lo resuelve con DNS: ECS registra y desregistra automáticamente cada tarea en un espacio de nombres privado (aws servicediscovery create-private-dns-namespace --name interno.mercadofresco --vpc vpc-mercadofresco). Con el servicio configurado con serviceRegistries, la tienda alcanza al inventario en inventario.interno.mercadofresco y el DNS devuelve las IP de las tareas sanas. Las alternativas son:
| Mecanismo | Ventaja | Inconveniente |
|---|---|---|
| ALB interno | Comprobaciones de estado, TLS, enrutado por ruta | Coste fijo y un salto más de latencia |
| Cloud Map (DNS) | Sin coste de balanceador, directo | El cliente debe manejar reintentos y caché de DNS |
| ECS Service Connect | Proxy gestionado con reintentos y métricas por servicio | Añade un sidecar y algo de complejidad |
MercadoFresco no necesita esto todavía porque la tienda es un monolito detrás del ALB y todo lo demás va por colas (módulo 7). Cuando la tienda se divida, ECS Service Connect será la opción preferible: da métricas por llamada entre servicios sin instrumentar el código.
Observabilidad: Container Insights, registros y X-Ray
Todo lo del módulo 5 sigue valiendo, con tres ajustes:
-
Container Insights se activa a nivel de clúster (
aws ecs update-cluster-settings --cluster ecs-mercadofresco --settings name=containerInsights,value=enhanced) y publica métricas de CPU, memoria, red y disco por servicio y por tarea en el espacio de nombresECS/ContainerInsights. Sin él, CloudWatch solo daCPUUtilizationyMemoryUtilizationa nivel de servicio, que no basta para diagnosticar qué contenedor consume. -
Los registros van al grupo
/ecs/mercadofresco-tiendacon un flujo por tarea. La consecuencia práctica: como las tareas son efímeras y se reponen constantemente, la única forma razonable de buscar es CloudWatch Logs Insights (05-01), no abrir flujos a mano. Y la retención hay que fijarla explícitamente: conawslogs-create-group: true, el grupo se crea sin caducidad y guarda para siempre. -
X-Ray funciona con el sidecar del ejemplo. El SDK de la aplicación envía los segmentos por UDP a
127.0.0.1:2000, el demonio los agrupa y los sube. La alternativa moderna es el AWS Distro for OpenTelemetry como sidecar, que además de trazas recoge métricas. En ambos casos, el rol de tarea —no el de ejecución— necesitaxray:PutTraceSegments.
Las métricas nuevas que Marta añade al panel mercadofresco-produccion:
| Métrica | Umbral de alarma | Qué indica |
|---|---|---|
RunningTaskCount frente a DesiredTaskCount |
Diferencia > 0 durante 5 min | Tareas que no consiguen arrancar |
CpuUtilized / CpuReserved por servicio |
> 85 % sostenido | Reserva mal dimensionada |
MemoryUtilized / MemoryReserved |
> 90 % | Riesgo de OOM |
TaskStopped con exitCode: 137 |
Cualquiera | El kernel mató el contenedor por memoria |
DeploymentCount |
> 1 durante 15 min | Despliegue atascado |
El exitCode: 137 merece una nota: es 128 + 9, es decir, SIGKILL. En ECS casi siempre significa OOM: el contenedor superó su memory y el kernel lo mató. El síntoma es una tarea que reaparece cada pocos minutos sin que los logs de la aplicación digan nada, porque no hubo tiempo de escribir nada.
La migración de MercadoFresco
El plan de Marta separa lo que se contenerizará ahora, lo que se contenerizará después y lo que no se toca nunca.
| Componente | Decisión | Motivo |
|---|---|---|
Tienda (asg-mercadofresco-tienda) |
Contenerizar ya | Es la que sufre el pico del viernes y la que tiene el arranque de dos minutos |
Trabajadores de cola-mercadofresco-pedidos (asg-mercadofresco-trabajadores) |
Contenerizar ya | Misma AMI, mismo problema de parcheo, y escalan por cola |
Lambdas (-cobrar-pago, -reservar-stock, …) |
No tocar | Ya no tienen servidor; contenerizarlas sería un retroceso |
| Aurora, DynamoDB, Redshift, ElastiCache | No tocar | Son servicios gestionados; el contenedor no aporta nada |
| ALB, CloudFront, Route 53, WAF | No tocar | Solo cambia el tipo de destino del grupo, a ip |
| Buckets y colas | No tocar | La interfaz es la misma desde un contenedor |
Los pasos, en el orden en que reducen riesgo:
- Escribir el Dockerfile y ejecutarlo en local. Antes de tocar AWS, la imagen debe arrancar y responder
200en/saluden el portátil de Luis. - Crear los repositorios de ECR con inmutabilidad, escaneo mejorado y ciclo de vida, y añadir la fase de construcción de imagen al proyecto
build-mercadofresco-tienda. - Crear el clúster
ecs-mercadofrescoen desarrollo, con un proveedor de capacidad sobre un ASG pequeño de instancias con la AMI optimizada para ECS. - Registrar la definición de tarea y lanzar una tarea suelta con
run-task. Aquí aparecen los errores de rol de ejecución, secretos y grupos de seguridad, y aquí es donde deben aparecer. - Crear el grupo de destino de tipo
ipy el servicio en desarrollo, y validar con las pruebas de humo del proyectobuild-mercadofresco-humo(08-02). - Repetir en preproducción con la carga sintética del viernes, midiendo tiempo de arranque de tarea y comparándolo con los 120 segundos actuales.
- En producción, convivencia: el mismo ALB con dos grupos de destino y un reparto ponderado —90 % al ASG, 10 % al servicio de ECS—, subiendo el peso a lo largo de dos semanas.
- Retirar el ASG y la AMI cuando el servicio lleve dos semanas al 100 % sin incidentes, y borrar la plantilla de lanzamiento del CDK en un PR que deja constancia.
El paso 7 es el que hace que esto sea una migración y no un salto de fe. Con el reparto ponderado del ALB (03-03), un problema en el servicio de ECS afecta al 10 % de las peticiones durante el tiempo que se tarde en poner el peso a cero, que son segundos.
Coste y limpieza
ECS no cuesta nada. El plano de control, las definiciones de tarea, los servicios y los despliegues son gratuitos. Lo que se paga con el tipo de lanzamiento EC2 es exactamente lo que ya se pagaba: las instancias, sus volúmenes EBS y la transferencia de datos.
La comparación honesta para MercadoFresco, con la tienda contenerizada sobre las mismas instancias:
| Concepto | Antes (ASG con AMI) | Ahora (ECS sobre EC2) |
|---|---|---|
| Instancias en horario normal | 2 × m5.large |
2 × m5.large (más densas: caben los trabajadores también) |
| Instancias en el pico | Hasta 4 | Hasta 4, con mejor empaquetado |
| ECR | — | ~2 GB × 0,10 = 0,20 USD/mes |
| Escaneo mejorado | — | ~4 USD/mes con 4,8 imágenes por semana |
| Ahorro real | — | Consolidar tienda y trabajadores en el mismo clúster elimina 2 instancias dedicadas |
El ahorro de esta lección no viene de ECS sino de la densidad: los trabajadores tenían su propio ASG con sus propias instancias, muchas veces ociosas, y en un clúster compartido se empaquetan con la tienda. El ahorro grande —eliminar la capacidad ociosa por completo— llega con Fargate en 10-02, y el análisis fino de costes, en el módulo 11. La limpieza, en este orden estricto:
# 1. El servicio a cero antes de borrarlo, o el borrado falla
aws ecs update-service --cluster ecs-mercadofresco --service svc-mercadofresco-tienda --desired-count 0
aws ecs delete-service --cluster ecs-mercadofresco --service svc-mercadofresco-tienda --force
# 2. Proveedor de capacidad y cluster
aws ecs delete-capacity-provider --capacity-provider cp-mercadofresco-ec2
aws ecs delete-cluster --cluster ecs-mercadofresco
# 3. El ASG de instancias del cluster: ESTO es lo que cuesta dinero
aws autoscaling delete-auto-scaling-group --auto-scaling-group-name asg-cluster-mercadofresco --force-delete
# 4. Imagenes y repositorio; 5. el grupo de registros, que si no guarda para siempre
aws ecr delete-repository --repository-name mercadofresco/tienda --force
aws logs delete-log-group --log-group-name /ecs/mercadofresco-tiendaEl paso 4 es el único imprescindible desde el punto de vista económico: borrar el clúster de ECS no borra las instancias EC2, que siguen facturando alegremente sin nada que ejecutar. Es el residuo más caro de esta lección.
Errores Comunes y Consejos
Error: usar latest en la definición de tarea. Produce despliegues no reproducibles y reversiones imposibles. Consejo: etiqueta con v1.6.0-a3f9c21 y despliega por digest; que el pipeline sea el único que decide qué digest va a cada entorno.
Error: confundir el rol de ejecución con el de tarea. Media hora perdida por cada incidente. Consejo: si la tarea no llega a arrancar, mira el rol de ejecución; si la aplicación falla llamando a AWS, el de tarea. Los mensajes CannotPullContainerError y ResourceInitializationError son siempre del de ejecución.
Error: no fijar --health-check-grace-period-seconds. La aplicación tarda en calentar, el ALB la mata, ECS la repone, bucle infinito. Consejo: mide el tiempo real hasta el primer 200 en /salud y pon el doble.
Error: COPY . . sin .dockerignore. Mete .git completo y ficheros .env en la imagen. Consejo: el .dockerignore se escribe antes que el Dockerfile, no después.
Error: capas en mal orden. COPY . . antes de pip install invalida la caché en cada commit. Consejo: de lo que menos cambia a lo que más: base, sistema, dependencias, código.
Error: contenedor como root con sistema de archivos escribible. Es innecesario y convierte cualquier fallo de la aplicación en escritura de binarios. Consejo: USER numérico, readonlyRootFilesystem: true y un volumen efímero para /tmp.
Error: olvidar el límite de ENI con awsvpc. Una m5.large ejecuta dos tareas y el clúster escala «sin motivo». Consejo: activa awsvpcTrunking a nivel de cuenta antes de dimensionar nada.
Error: awslogs en modo bloqueante. Un retraso de CloudWatch Logs frena la aplicación. Consejo: mode: non-blocking con max-buffer-size explícito en todos los servicios con tráfico de usuario.
Error: política de ciclo de vida agresiva. Borra la imagen a la que había que revertir. Consejo: conserva al menos diez versiones publicadas y prueba la política en seco antes de aplicarla.
Consejo: activa el circuit breaker con reversión en todos los servicios desde el primer día. No tiene coste y convierte un despliegue roto en un incidente de tres minutos que se resuelve solo.
Consejo: fija la retención de los grupos de registros de ECS. Con awslogs-create-group el grupo nace sin caducidad, y con tareas efímeras el volumen de logs crece más rápido de lo que nadie espera.
Consejo: etiqueta las tareas con propagateTags: SERVICE. Es la única forma de que las cinco etiquetas obligatorias lleguen a la tarea y de que el módulo 11 pueda repartir costes.
Consejo: mantén la definición de tarea en el CDK. El ecs.FargateTaskDefinition o ecs.Ec2TaskDefinition de 09-02 genera roles con permisos mínimos automáticamente con los métodos grant*, incluidos los de KMS que casi nadie recuerda.
Ejercicios
Ejercicio 1: el Dockerfile de los trabajadores
Escribe el Dockerfile completo del componente de trabajadores de MercadoFresco, que consume cola-mercadofresco-pedidos, escribe en Aurora y publica en el tema mercadofresco-pedido-confirmado. Es una aplicación Python sin servidor HTTP: no expone puertos y no puede usar un HEALTHCHECK basado en curl. Resuelve (a) cómo haces la comprobación de estado sin HTTP y por qué ECS la necesita igualmente; (b) qué cambia respecto al Dockerfile de la tienda en cuanto a etapas, usuario y .dockerignore; (c) cómo gestionas el apagado ordenado cuando ECS envía SIGTERM en mitad del procesamiento de un mensaje; y (d) qué stopTimeout pones y con qué relación respecto al tiempo de visibilidad de la cola.
Ejercicio 2: la tarea que no arranca
Luis registra la definición de tarea de la tienda y lanza el servicio en desarrollo. Ninguna tarea llega a estado RUNNING. En la consola aparecen, en distintos intentos, estos tres motivos de parada:
CannotPullContainerError: pull access denied for 555566667777.dkr.ecr.eu-west-1.amazonaws.com/mercadofresco/tienda, repository does not exist or may require 'docker login'ResourceInitializationError: unable to pull secrets or registry auth: execution resource retrieval failed: unable to retrieve secret from asm: AccessDeniedException- La tarea llega a
RUNNING, pero el servicio la mata a los 90 segundos y arranca otra, indefinidamente.
Para cada uno: diagnostica la causa exacta, indica dónde lo comprobarías, y da la corrección concreta. Para el tercero, explica además por qué el desired count nunca se estabiliza y qué dos configuraciones distintas podrían estar causándolo.
Ejercicio 3: decidir el etiquetado y el ciclo de vida
MercadoFresco despliega 4,8 veces por semana a producción, mantiene entre 6 y 10 ramas de característica vivas a la vez y necesita poder revertir hasta cuatro versiones atrás en producción. Cumplimiento exige poder demostrar, para cualquier día de los últimos 12 meses, qué imagen exacta estaba en producción. Diseña (a) el esquema completo de etiquetado de imágenes, incluyendo qué etiquetas lleva una imagen de rama y cuáles una de producción; (b) la política de ciclo de vida completa en JSON, con sus prioridades; (c) cómo satisfaces el requisito de cumplimiento de 12 meses sin guardar 250 imágenes; y (d) qué riesgo concreto tendría poner la regla de imágenes sin etiqueta con prioridad 1.
Soluciones
Solución 1
# La etapa "constructor" es identica a la de la tienda (venv en /opt/entorno con
# --require-hashes); cambia solo la etapa de ejecucion:
FROM python:3.12.4-slim-bookworm AS ejecucion
RUN apt-get update && apt-get install -y --no-install-recommends libpq5=15.* \
&& rm -rf /var/lib/apt/lists/* && groupadd --gid 10002 trabajador \
&& useradd --uid 10002 --gid trabajador --no-create-home --shell /usr/sbin/nologin trabajador
COPY --from=constructor /opt/entorno /opt/entorno
ENV PATH="/opt/entorno/bin:$PATH" PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
COPY --chown=trabajador:trabajador ./trabajadores ./trabajadores
USER 10002
# (a) Sin HTTP ni curl: el bucle escribe una marca de tiempo tras cada iteracion
# de sondeo y este comando falla si esa marca tiene mas de 90 segundos.
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD python -m trabajadores.salud || exit 1
ENTRYPOINT ["python", "-m", "trabajadores.principal"](a) La comprobación sin HTTP. El bucle de sondeo escribe /tmp/latido con la marca de tiempo tras cada ciclo de ReceiveMessage, y trabajadores.salud devuelve error si el fichero tiene más de 90 segundos. ECS la necesita igualmente porque un trabajador colgado no muere: se queda esperando en un socket, deja de consumir la cola y el servicio no se entera. Sin comprobación de estado, ese trabajador zombi cuenta como capacidad sana y la cola crece con cuatro tareas «vivas». Con el HEALTHCHECK, ECS lo mata y lo repone. Como readonlyRootFilesystem estará activo, /tmp debe ir montado como volumen efímero.
(b) Qué cambia respecto a la tienda. La estructura multietapa es idéntica —es el patrón, no la aplicación—. No hay EXPOSE ni portMappings porque nadie llama al trabajador. No hace falta curl, lo que permite una imagen aún más pequeña y una superficie de ataque menor: el HEALTHCHECK usa el propio Python. El UID es distinto (10002) por higiene, para que los procesos sean distinguibles en el anfitrión. Y el .dockerignore es el mismo, con una adición importante: el directorio de datos de prueba con mensajes de ejemplo, que puede contener datos que parecen reales y no deben viajar en una imagen.
(c) Apagado ordenado. El proceso registra un manejador de SIGTERM que activa una bandera; el bucle principal la comprueba antes de pedir el siguiente mensaje, no en mitad del procesamiento. El mensaje en curso se termina y se borra de la cola con DeleteMessage; después el proceso sale con código 0. Si el mensaje no se puede terminar a tiempo, no se borra: al expirar el tiempo de visibilidad volverá a la cola y otro trabajador lo procesará, y de ahí la importancia de la idempotencia con mercadofresco-idempotencia (07-05). El error a evitar es borrar el mensaje al recibirlo: entonces el SIGKILL lo pierde para siempre.
(d) stopTimeout y visibilidad. El procesamiento de un pedido tarda como máximo unos 20 segundos, así que stopTimeout: 30 da margen suficiente. La relación con el tiempo de visibilidad de la cola es la clave: el tiempo de visibilidad debe ser mayor que stopTimeout más el tiempo de procesamiento, para que un mensaje interrumpido no reaparezca mientras el trabajador antiguo aún lo está terminando y un segundo trabajador lo procese en paralelo. Con procesamiento de 20 s y stopTimeout de 30 s, un tiempo de visibilidad de 180 s es holgado y correcto. El máximo de ECS para stopTimeout es 120 segundos, lo que además obliga a que ningún procesamiento individual dure más que eso.
Solución 2
Motivo 1: CannotPullContainerError — permisos del rol de ejecución o red. Hay tres causas posibles y se distinguen rápido. La primera, que el rol de ejecución no tenga los permisos de ECR (GetAuthorizationToken, BatchGetImage, GetDownloadUrlForLayer); se comprueba en CloudTrail (05-03) buscando AccessDenied sobre ecr: con el principal del rol. La segunda, que la política del repositorio en la cuenta de herramientas no autorice a la cuenta de desarrollo: es cruzado y hacen falta las dos políticas, la del rol y la del repositorio. La tercera, y la que produce este mensaje engañoso exacto, es de red: las tareas están en snet-mercadofresco-app-a sin ruta hacia ECR, porque el NAT está caído o porque no hay endpoints de VPC. Se distingue mirando si el mensaje llega tras un tiempo de espera largo (red) o inmediato (permisos). Corrección: los permisos de la política de ejecución más los endpoints de ecr.api, ecr.dkr y el de S3 —las capas se descargan de S3, y ese es el que todo el mundo olvida—.
Motivo 2: ResourceInitializationError ... unable to retrieve secret from asm. Es inequívoco: el rol de ejecución no puede leer mercadofresco/produccion/rds/mfadmin. Dos causas, y hay que revisar las dos: falta secretsmanager:GetSecretValue sobre el ARN del secreto, o falta kms:Decrypt sobre alias/mercadofresco-datos, porque el secreto está cifrado con una clave gestionada por el cliente (04-02) y leer el secreto exige descifrarlo. La segunda es la que más se olvida, y el mensaje de error no menciona KMS. Se comprueba en CloudTrail con el evento GetSecretValue fallido. Corrección: añadir ambos permisos al rol de ejecución —no al de tarea, que es el error siguiente que cometerá Luis— y verificar que la política de la clave KMS incluya al rol como usuario.
Motivo 3: el bucle infinito de arranque y muerte. La tarea llega a RUNNING, luego muere. Dos configuraciones pueden causarlo:
- El período de gracia de la comprobación de estado. Si la aplicación tarda 60 segundos en responder
200en/salud—conexión a Aurora, precarga de catálogo desde ElastiCache— y el grupo de destino la declara no sana antes, el servicio la desregistra y la mata. ECS repone, y el ciclo se repite. Corrección:--health-check-grace-period-secondsal doble del tiempo medido, y revisar el umbral del grupo de destino (HealthyThresholdCount,Interval,Timeout). - El grupo de seguridad. Si
sg-mercadofresco-tiendano permite la entrada desdesg-mercadofresco-alben el puerto 8080 —y no en el 80, que es lo que estaba configurado cuando el destino era la instancia—, la comprobación del ALB nunca pasa. Conawsvpc, el SG se aplica a la tarea y el puerto es el del contenedor, no un puerto dinámico del anfitrión.
Por qué el desired count nunca se estabiliza: porque el servicio no distingue entre «la tarea murió» y «la tarea nunca llegó a estar sana». Repone indefinidamente, consumiendo cuota de ejecución y llenando el registro de eventos. Y es exactamente el escenario para el que existe el circuit breaker de despliegue: con enable: true y rollback: true, tras el umbral de fallos consecutivos el despliegue se declara fallido y se revierte, en lugar de girar en vacío durante horas. Si Luis lo hubiera activado, el diagnóstico habría llegado como un evento de despliegue fallido en lugar de como un panel que no se estabiliza.
Solución 3
(a) Esquema de etiquetado. Cada imagen lleva varias etiquetas, porque una etiqueta es un puntero y se pueden poner varios al mismo digest:
| Origen | Etiquetas | Ejemplo |
|---|---|---|
| Rama de característica | rama-<nombre>-<sha7> |
rama-cesta-rapida-b71c4e0 |
main sin publicar |
main-<sha7> |
main-a3f9c21 |
| Versión publicada | v<semver>-<sha7> y v<semver> |
v1.6.0-a3f9c21 y v1.6.0 |
| Caché de construcción | cache (mutable, repositorio aparte) |
cache |
La etiqueta cache no puede convivir con IMMUTABLE, así que va en un repositorio distinto —mercadofresco/tienda-cache— con inmutabilidad desactivada y ciclo de vida de tres días. Es el matiz que se descubre al aplicar la inmutabilidad y romper la construcción.
(b) Política de ciclo de vida.
{"rules": [
{"rulePriority": 10, "description": "Versiones publicadas: conservar 12", "action": {"type": "expire"},
"selection": {"tagStatus": "tagged", "tagPrefixList": ["v"], "countType": "imageCountMoreThan", "countNumber": 12}},
{"rulePriority": 20, "description": "main sin publicar: 30 dias", "action": {"type": "expire"},
"selection": {"tagStatus": "tagged", "tagPrefixList": ["main-"], "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 30}},
{"rulePriority": 30, "description": "Ramas de caracteristica: 14 dias", "action": {"type": "expire"},
"selection": {"tagStatus": "tagged", "tagPrefixList": ["rama-"], "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 14}},
{"rulePriority": 90, "description": "Sin etiqueta: 1 dia", "action": {"type": "expire"},
"selection": {"tagStatus": "untagged", "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 1}}
]}Las prioridades van de diez en diez para poder intercalar reglas después sin renumerar todo, que es la misma disciplina de las reglas de WAF (04-05). Doce versiones publicadas cubren con holgura las cuatro reversiones exigidas y unas dos semanas y media de despliegues.
(c) El requisito de cumplimiento de 12 meses. No se resuelve guardando imágenes, se resuelve guardando evidencia. Cumplimiento no necesita poder ejecutar la imagen de hace once meses; necesita poder demostrar qué imagen estaba desplegada. Tres fuentes lo dan sin coste de almacenamiento:
- CloudTrail (05-03) registra cada
RegisterTaskDefinitionyUpdateServicecon el digest exacto y quién lo hizo; con el trail de organización en la cuenta de seguridad y S3 con Object Lock, la evidencia es inmutable y consultable con Athena. - El repositorio Git
mercadofresco-tiendaconserva el commit y elimagen.jsondel artefacto de cada ejecución del pipeline. - AWS Config (05-04) mantiene el histórico de configuración del servicio de ECS.
Si además hiciera falta poder reconstruir una imagen antigua, la respuesta es reconstruirla desde el commit con el Dockerfile reproducible —versiones fijadas y hashes—, no guardarla.
(d) El riesgo de poner la regla de imágenes sin etiqueta con prioridad 1. Sería catastrófico por cómo evalúa ECR: una imagen solo la afecta la primera regla que la selecciona, y una regla de untagged con prioridad 1 se evalúa antes que todas. Cuando una imagen publicada pierde su etiqueta —porque otra imagen la reutiliza, o durante una replicación, o al retirar una etiqueta v1.6.0 a mano—, pasa a ser untagged y la regla 1 la borra en 24 horas, incluso si es la que está ejecutando producción. Las reglas de untagged van siempre al final, con la prioridad más alta, y solo después de que las reglas específicas hayan tenido su oportunidad de retener lo que importa.
Conclusión
MercadoFresco ya no despliega máquinas: despliega imágenes. Esta lección ha sustituido los tres artefactos que dolían —la AMI, el ciclo de parcheo y el arranque de dos minutos— por un Dockerfile de cuarenta líneas, un registro y un orquestador.
Tienes claro qué es un contenedor de verdad: namespaces y cgroups sobre un kernel compartido, no una máquina virtual pequeña, con lo que eso implica en arranque (segundos frente a minutos), en tamaño (megas frente a gigas), en densidad y en portabilidad —el mismo artefacto en el portátil de Luis y en producción, byte a byte—, y con la advertencia honesta sobre el aislamiento que el marketing suele omitir. Tienes el Dockerfile de la tienda con construcción multietapa que deja los compiladores fuera, usuario 10001 sin privilegios, versiones fijadas con hashes, HEALTHCHECK coherente con /salud y el orden de capas que convierte una construcción de tres minutos en una de cuarenta segundos. Y el .dockerignore que se escribe antes que el Dockerfile, no después de haber metido .git en una imagen.
Tienes Amazon ECR con mercadofresco/tienda y mercadofresco/trabajadores: autenticación por token temporal, inmutabilidad de etiquetas, el esquema v1.6.0-a3f9c21 que ata cada imagen a un commit, y la regla que hay que interiorizar —la etiqueta sirve para que un humano encuentre la imagen; el digest, para que la máquina la despliegue—. Con las políticas de ciclo de vida y sus tres trampas, el escaneo mejorado de Inspector que sí ve las dependencias de Python y avisa cuando una imagen ya desplegada deja de estar limpia, la replicación desde la cuenta de herramientas 555566667777 hacia producción, y el cifrado con alias/mercadofresco-datos que exige acordarse de la política de la clave.
Y tienes Amazon ECS: clúster ecs-mercadofresco, la definición de tarea mercadofresco-tienda con secretos inyectados desde Secrets Manager y Parameter Store, registros no bloqueantes, sidecar de X-Ray y awsvpc con grupo de seguridad por tarea; el servicio svc-mercadofresco-tienda detrás de tg-mercadofresco-tienda con período de gracia, despliegue 100/200 y circuit breaker con reversión automática; el autoescalado por ALBRequestCountPerTarget con enfriamientos asimétricos; y la distinción que ahorra media hora en cada incidente: el rol de ejecución trabaja para AWS, el rol de tarea trabaja para tu código.
Pero queda un residuo, y se ve en la tabla de proveedores de capacidad. Debajo del clúster siguen existiendo instancias EC2: hay que elegir su tamaño, mantener su AMI optimizada para ECS, vigilar que el escalado gestionado no se quede corto y aceptar que, cuando no hay hueco para una tarea nueva, hace falta una instancia nueva y una instancia tarda dos minutos en arrancar. Es el mismo número del que huíamos. Lo hemos escondido detrás de un proveedor de capacidad, no lo hemos eliminado. Y sigue habiendo capacidad ociosa que se paga: dos instancias encendidas a las cuatro de la mañana de un martes.
En 10-02, «AWS Fargate», las instancias desaparecen. Sin AMI que mantener, sin clúster que escalar, sin empaquetado de tareas en máquinas, sin capacidad ociosa: se declara cuánta CPU y cuánta memoria necesita cada tarea y AWS pone el resto. Verás las combinaciones válidas de recursos, el ahorro real de Graviton, cómo se depura sin SSH con ECS Exec, los endpoints de VPC que evitan pagar el NAT, el escalado programado para el viernes a las 17:00, y Fargate Spot para los trabajadores de la cola. Y la comparativa honesta entre Fargate, EC2 y Lambda, con la decisión razonada de MercadoFresco para cada componente.
Curso de AWS
Módulo 1: Introducción a AWS
- ¿Qué es AWS?
- Configuración de tu cuenta de AWS
- Infraestructura global de AWS
- Consola de administración de AWS
- AWS CLI y SDKs
Módulo 2: Servicios principales de AWS
Módulo 3: Redes y entrega de contenido
- Amazon VPC
- Grupos de seguridad y listas de control de acceso
- Elastic Load Balancing
- Amazon CloudFront
- Route 53
Módulo 4: Seguridad e identidad
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager y Parameter Store
- AWS Shield
- AWS WAF
Módulo 5: Monitorización y gestión
- Amazon CloudWatch
- AWS X-Ray y trazabilidad distribuida
- AWS CloudTrail
- AWS Config
- AWS Trusted Advisor
Módulo 6: Bases de datos
- Cómo elegir la base de datos adecuada
- Amazon DynamoDB
- Amazon Aurora
- Amazon Redshift
- Amazon ElastiCache
Módulo 7: Integración de aplicaciones
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Patrones de integración: idempotencia, reintentos y colas de mensajes fallidos
