Heroku puso a CicloUrbana en Internet en una tarde, y también nos enseñó dónde están sus límites: sin red privada propia, sin control del entorno de ejecución, con un coste por unidad de capacidad que crece mal y con la base de datos en una infraestructura que no es nuestra. El ayuntamiento de Ribalta ha decidido que la red municipal de bicicletas es un servicio público permanente, con datos personales sujetos al RGPD, y pide una infraestructura seria: red privada, base de datos inaccesible desde Internet, secretos auditados, alta disponibilidad y capacidad de crecer.

Esta lección construye ese entorno en AWS. No cubre AWS entero —es un proveedor con más de doscientos servicios— sino el camino mínimo y completo para llevar la imagen de 07-04 a producción: un repositorio de imágenes en ECR, PostgreSQL gestionado en RDS dentro de subredes privadas, los secretos en Secrets Manager, un servicio de ECS Fargate que ejecuta la imagen sin que administremos ningún servidor, y un balanceador con certificado gestionado que comprueba la salud contra las sondas de 07-01. Y termina, como siempre, con la lista exacta de recursos que hay que destruir, porque aquí la factura es real y llega puntual.

Contenido

  1. Panorama: qué servicio de AWS ejecuta una aplicación Spring Boot
  2. Conceptos base imprescindibles
  3. Preparar la imagen: ECR
  4. La base de datos: RDS PostgreSQL
  5. Secretos: Secrets Manager y Parameter Store
  6. Desplegar en ECS Fargate
  7. El balanceador, la salud y el TLS
  8. Migraciones de Flyway en AWS
  9. Autoescalado
  10. Observabilidad: CloudWatch
  11. Camino corto: Elastic Beanstalk
  12. Infraestructura como código
  13. Costes y limpieza
  14. Seguridad: las reglas que no se negocian
  15. Errores Comunes y Consejos
  16. Ejercicios

  1. Panorama: qué servicio de AWS ejecuta una aplicación Spring Boot

Servicio Qué gestionas tú Esfuerzo Coste típico (mes) Cuándo elegirlo
EC2 Máquina virtual completa: SO, Java, arranque, parches, TLS Muy alto 15-80 € Requisitos muy específicos o coste mínimo con alguien que administre Linux
Elastic Beanstalk Subes un JAR; AWS crea EC2, balanceador y autoescalado Bajo 40-120 € Camino corto desde un JAR, sin aprender ECS
ECS + Fargate Una imagen y su definición de tarea; sin servidores Medio-bajo 60-200 € La opción del curso: producción real sin operar un orquestador
EKS (Kubernetes gestionado) Manifiestos, complementos, versiones del clúster Alto 200 € + clúster Muchos servicios y equipos, portabilidad (08-04)
App Runner Solo la imagen o el repositorio Muy bajo 30-90 € Un PaaS dentro de AWS; menos control de red
Lambda + SnapStart Solo el código de la función Bajo Por invocación Cargas esporádicas; encaja mal con JPA y un pool

La decisión razonada del curso es ECS Fargate + RDS, por cuatro motivos en orden de peso. No hay servidores que administrar: con Fargate no existen instancias EC2 que parchear, dimensionar ni vigilar, solo se declara cuánta CPU y memoria necesita cada tarea. Es el destino natural de la imagen de 07-04, sin cambiar el artefacto, sin buildpacks y sin Procfile. Da red privada de verdad, con la base de datos en subredes sin salida a Internet, que es el requisito que nos sacó de Heroku. Y es notablemente más simple que Kubernetes para una sola aplicación: si CicloUrbana fuese lo único que hay que desplegar, montar EKS sería pagar la complejidad de un orquestador sin la escala que la justifica.

Elastic Beanstalk queda como camino corto documentado en el apartado 11: si el objetivo es solo «tener el JAR corriendo en AWS esta tarde», es más rápido. Lambda se descarta por diseño: una aplicación con Hibernate, un pool de conexiones y un contexto de Spring encaja mal en un modelo de invocaciones efímeras, y aunque SnapStart mitiga el arranque en frío, la multiplicación de conexiones a PostgreSQL sigue siendo un problema real.

  1. Conceptos base imprescindibles

Concepto Qué es En CicloUrbana
Región Zona geográfica con centros de datos independientes eu-west-1 (Irlanda): datos en la UE, requisito del RGPD
Zona de disponibilidad (AZ) Centro de datos aislado dentro de una región Al menos dos, para tolerar la caída de una
VPC Red privada virtual propia, con su rango de direcciones 10.20.0.0/16
Subred pública Tiene ruta a Internet a través de un Internet Gateway Donde vive el balanceador
Subred privada Sin ruta de entrada desde Internet Donde viven las tareas y la base de datos
Grupo de seguridad Cortafuegos con estado, a nivel de recurso Uno para el ALB, otro para la app, otro para RDS
Rol IAM Identidad que asume un recurso para obtener permisos temporales La tarea asume un rol; nunca hay claves en el código
Política IAM Documento JSON que concede o deniega acciones Permitir leer un secreto concreto
ALB Balanceador de aplicación (capa 7): enruta HTTP y comprueba salud Terminación TLS y /actuator/health/readiness
Route 53 DNS gestionado ciclourbana.ribalta.example → ALB
ACM Certificados TLS gratuitos y de renovación automática El certificado del dominio del ayuntamiento

La arquitectura completa que vamos a construir:

flowchart TD
    U[Ciudadanos] --> R53[Route 53<br/>ciclourbana.ribalta.example]
    R53 --> ALB[ALB · subredes publicas<br/>TLS con certificado de ACM]
    subgraph VPC["VPC 10.20.0.0/16 · eu-west-1"]
      subgraph PUB[Subredes publicas · AZ a y b]
        ALB
      end
      subgraph PRIV[Subredes privadas · AZ a y b]
        T1[Tarea Fargate 1]
        T2[Tarea Fargate 2]
        RDS[(RDS PostgreSQL 16<br/>Multi-AZ)]
      end
    end
    ALB -->|8080| T1
    ALB -->|8080| T2
    T1 --> RDS
    T2 --> RDS
    ECR[(ECR<br/>ciclourbana:2.4.0)] -.imagen.-> T1
    SM[Secrets Manager] -.credenciales.-> T1
    T1 -.logs.-> CW[CloudWatch Logs]

Tres reglas de esa arquitectura, que son las que separan un despliegue serio de uno improvisado: solo el ALB está en subredes públicas; RDS no tiene dirección pública ni la tendrá nunca; y cada grupo de seguridad solo acepta tráfico del grupo anterior, no de rangos de direcciones sueltos.

  1. Preparar la imagen: ECR

Elastic Container Registry es el registro de imágenes de AWS. Podría usarse GHCR (07-04), pero ECR está en la misma cuenta y región, las tareas de Fargate acceden con permisos de IAM sin credenciales de larga duración, y la descarga no sale de la red de AWS.

export CUENTA=111122223333          # identificador ficticio de cuenta
export REGION=eu-west-1
export REGISTRO=$CUENTA.dkr.ecr.$REGION.amazonaws.com

# 1. Crear el repositorio con escaneo de vulnerabilidades e inmutabilidad
aws ecr create-repository \
  --repository-name ciclourbana \
  --region $REGION \
  --image-scanning-configuration scanOnPush=true \
  --image-tag-mutability IMMUTABLE

# 2. Autenticar Docker contra ECR (el testigo dura 12 horas)
aws ecr get-login-password --region $REGION \
  | docker login --username AWS --password-stdin $REGISTRO

# 3. Etiquetar y publicar la imagen de 07-04
docker build -t ciclourbana:2.4.0 .
docker tag ciclourbana:2.4.0 $REGISTRO/ciclourbana:2.4.0
docker push $REGISTRO/ciclourbana:2.4.0

Dos opciones del primer comando merecen explicación. scanOnPush=true analiza cada imagen publicada en busca de vulnerabilidades conocidas, complementando el docker scout de 07-04 con una segunda red de seguridad automática. IMMUTABLE impide sobrescribir una etiqueta ya publicada: ciclourbana:2.4.0 significará siempre la misma imagen, byte a byte. Es lo que hace que la reversión de 08-01 sea exacta y elimina el error clásico de latest apuntando a algo distinto de lo que se probó.

Conviene además añadir una política de ciclo de vida que conserve solo las últimas 15 imágenes: el registro se factura por GB almacenado y crece sin límite si nadie lo poda.

Importante para Fargate: la imagen debe ser de arquitectura linux/amd64 salvo que se configure Fargate con Graviton (ARM). Si construyes en un portátil con Apple Silicon, docker build produce arm64 y la tarea fallará con exec format error. La solución es docker buildx build --platform linux/amd64 ... o construir en la canalización (08-05).

  1. La base de datos: RDS PostgreSQL

# Grupo de subredes: RDS debe vivir en subredes PRIVADAS de al menos dos AZ
aws rds create-db-subnet-group \
  --db-subnet-group-name ciclourbana-privadas \
  --db-subnet-group-description "Subredes privadas de CicloUrbana" \
  --subnet-ids subnet-0aa11bb22cc33dd44 subnet-0ee55ff66gg77hh88

aws rds create-db-instance \
  --db-instance-identifier ciclourbana-prod \
  --engine postgres --engine-version 16.4 \
  --db-instance-class db.t4g.micro \
  --allocated-storage 20 --storage-type gp3 --storage-encrypted \
  --db-name ciclourbana \
  --master-username ciclourbana_admin \
  --manage-master-user-password \
  --db-subnet-group-name ciclourbana-privadas \
  --vpc-security-group-ids sg-0bd99ee88ff77aa66 \
  --no-publicly-accessible \
  --multi-az \
  --backup-retention-period 7 \
  --preferred-backup-window "02:00-03:00" \
  --preferred-maintenance-window "sun:04:00-sun:05:00" \
  --deletion-protection

Parámetro a parámetro, y por qué cada uno importa:

Parámetro Qué hace Por qué así
--engine-version 16.4 Fija PostgreSQL 16 Paridad de entornos (08-01): la misma versión que Testcontainers en 06-05
--db-instance-class db.t4g.micro Tamaño de la instancia Suficiente para empezar; se cambia después con una parada breve
--storage-encrypted Cifrado en reposo con KMS Obligatorio para datos personales; no se puede activar después
--manage-master-user-password AWS genera la contraseña y la guarda en Secrets Manager Nadie ve nunca la contraseña; se puede rotar
--no-publicly-accessible Sin dirección pública La regla que no se negocia
--multi-az Réplica en espera en otra AZ con conmutación automática Tolera la caída de una zona; duplica el coste
--backup-retention-period 7 Copias automáticas durante 7 días con PITR RPO de segundos, RTO de minutos (08-01)
--preferred-maintenance-window Ventana de actualizaciones menores Domingo de madrugada, cuando Ribalta duerme
--deletion-protection Impide borrarla por accidente Hay que desactivarla explícitamente para eliminarla

El grupo de seguridad es la pieza crítica. No debe permitir el puerto 5432 desde 0.0.0.0/0 ni desde tu IP de casa: solo desde el grupo de seguridad de la aplicación.

aws ec2 authorize-security-group-ingress \
  --group-id sg-0bd99ee88ff77aa66 \
  --protocol tcp --port 5432 \
  --source-group sg-0cc44dd55ee66ff77        # el grupo de las tareas, no un CIDR

Referenciar grupo a grupo en lugar de rangos de direcciones tiene una ventaja decisiva: las tareas de Fargate reciben direcciones IP nuevas cada vez que se despliegan, y con esta regla la autorización sigue siendo válida sin tocar nada.

Por qué RDS nunca es accesible desde Internet. Un PostgreSQL con dirección pública recibe intentos de autenticación automatizados a los pocos minutos de existir, y contiene los datos personales de los ciudadanos de Ribalta: basta una contraseña débil, una versión con un fallo conocido o una regla temporal que alguien olvidó cerrar. Cuando haya que consultarla desde un portátil, la forma correcta es un bastión con Session Manager (sin abrir puertos ni gestionar claves SSH), nunca abrir el grupo de seguridad «un momento».

  1. Secretos: Secrets Manager y Parameter Store

Secrets Manager SSM Parameter Store (SecureString)
Coste ~0,40 $/secreto/mes + peticiones Estándar gratuito; avanzado de pago
Rotación automática Sí, integrada con RDS No, manual
Tamaño 64 KB 4 KB (8 KB avanzado)
Uso recomendado Credenciales de base de datos, claves de API Configuración no crítica, URLs, parámetros

Con --manage-master-user-password, la contraseña de RDS ya está en Secrets Manager. Falta el secreto del JWT:

aws secretsmanager create-secret \
  --name ciclourbana/prod/jwt \
  --description "Secreto HS256 de firma del JWT de CicloUrbana" \
  --secret-string "$(openssl rand -base64 48)"
# {"ARN": "arn:aws:secretsmanager:eu-west-1:111122223333:secret:ciclourbana/prod/jwt-Ab3xYz"}

Hay dos formas de que ese valor llegue a la aplicación.

Opción A (recomendada): que ECS lo inyecte como variable de entorno. En la definición de tarea se usa el bloque secrets en lugar de environment; el agente resuelve el ARN al arrancar el contenedor y lo pone como variable. La aplicación no sabe que existe AWS: sigue leyendo ${JWT_SECRETO} como en 07-02, así que el mismo artefacto funciona en Heroku, en Kubernetes o en el portátil — la opción coherente con 08-01.

Opción B: que la aplicación lo lea directamente con spring-cloud-aws-starter-parameter-store o -secrets-manager:

spring:
  config:
    import:
      - aws-secretsmanager:ciclourbana/prod/jwt
      - aws-parameterstore:/ciclourbana/prod/

Es útil cuando hay muchos parámetros o cuando se quiere refrescar en caliente, pero acopla el artefacto a AWS y requiere permisos IAM adicionales.

El rol de ejecución de la tarea necesita permiso para leer ese secreto y ninguno más — mínimo privilegio en su forma más concreta:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["secretsmanager:GetSecretValue"],
    "Resource": [
      "arn:aws:secretsmanager:eu-west-1:111122223333:secret:ciclourbana/prod/jwt-Ab3xYz",
      "arn:aws:secretsmanager:eu-west-1:111122223333:secret:rds!db-9f3a2b1c-Kd7pQr"
    ]
  }]
}

Fíjate en que Resource enumera ARNs concretos. Un "Resource": "*" daría a la aplicación acceso a todos los secretos de la cuenta, incluidos los de otros sistemas del ayuntamiento.

Advertencia. Jamás se ponen secretos como environment en texto plano en la definición de tarea: ese JSON se versiona en el repositorio de infraestructura y se consulta con aws ecs describe-task-definition, que cualquiera con permisos de lectura puede ejecutar. Y jamás se crean claves de acceso (AWS_ACCESS_KEY_ID) para la aplicación: los roles IAM dan credenciales temporales y rotadas automáticamente.

  1. Desplegar en ECS Fargate

ECS organiza el despliegue en tres piezas: un clúster (agrupación lógica), una definición de tarea (la plantilla de qué contenedor ejecutar y cómo) y un servicio (mantiene N tareas vivas y las conecta al balanceador).

aws ecs create-cluster --cluster-name ciclourbana \
  --capacity-providers FARGATE --region eu-west-1

La definición de tarea, comentada campo a campo:

{
  "family": "ciclourbana",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "runtimePlatform": { "cpuArchitecture": "X86_64", "operatingSystemFamily": "LINUX" },
  "executionRoleArn": "arn:aws:iam::111122223333:role/ciclourbanaEjecucionRol",
  "taskRoleArn": "arn:aws:iam::111122223333:role/ciclourbanaTareaRol",
  "containerDefinitions": [{
    "name": "ciclourbana",
    "image": "111122223333.dkr.ecr.eu-west-1.amazonaws.com/ciclourbana:2.4.0",
    "essential": true,
    "portMappings": [{ "containerPort": 8080, "protocol": "tcp" }],
    "environment": [
      { "name": "SPRING_PROFILES_ACTIVE", "value": "prod" },
      { "name": "TZ", "value": "Europe/Madrid" },
      { "name": "JAVA_TOOL_OPTIONS", "value": "-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError" },
      { "name": "SPRING_DATASOURCE_URL",
        "value": "jdbc:postgresql://ciclourbana-prod.abc123xyz.eu-west-1.rds.amazonaws.com:5432/ciclourbana?sslmode=require" },
      { "name": "SPRING_FLYWAY_ENABLED", "value": "false" },
      { "name": "MANAGEMENT_SERVER_PORT", "value": "8080" }
    ],
    "secrets": [
      { "name": "JWT_SECRETO",
        "valueFrom": "arn:aws:secretsmanager:eu-west-1:111122223333:secret:ciclourbana/prod/jwt-Ab3xYz" },
      { "name": "SPRING_DATASOURCE_PASSWORD",
        "valueFrom": "arn:aws:secretsmanager:eu-west-1:111122223333:secret:rds!db-9f3a2b1c-Kd7pQr:password::" }
    ],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/ciclourbana",
        "awslogs-region": "eu-west-1",
        "awslogs-stream-prefix": "app",
        "awslogs-create-group": "true"
      }
    },
    "stopTimeout": 60
  }]
}

Lo que hay que entender de este JSON:

  • cpu: 512 y memory: 1024 son 0,5 vCPU y 1 GB. Fargate solo admite combinaciones concretas (0,5 vCPU admite 1, 2, 3 o 4 GB). Con MaxRAMPercentage=75, el heap queda en 768 MB y los 256 MB restantes cubren metaespacio, pilas e hilos: el dimensionado de 08-01 aplicado.
  • Dos roles distintos, y no es un detalle burocrático. El rol de ejecución lo usa el agente de ECS antes de arrancar el contenedor: descargar la imagen de ECR, resolver los secretos, escribir en CloudWatch. El rol de tarea lo usa la aplicación en ejecución para llamar a otros servicios de AWS (S3, SQS). Separarlos significa que la aplicación no puede leer secretos por su cuenta aunque la comprometan.
  • secrets frente a environment: el valor nunca aparece en la definición. El sufijo :password:: extrae un campo concreto del JSON del secreto de RDS, que contiene username y password.
  • SPRING_FLYWAY_ENABLED=false: las migraciones se ejecutan aparte (apartado 8).
  • MANAGEMENT_SERVER_PORT=8080: en 07-01 separamos Actuator al 8081; aquí lo devolvemos al puerto principal para que el ALB pueda consultar /actuator/health/readiness con un solo destino. La alternativa —dos target groups— añade complejidad sin ganancia. La protección sigue siendo la cadena de seguridad de 07-01, no la separación de puertos.
  • stopTimeout: 60: segundos entre SIGTERM y SIGKILL. Debe superar el timeout-per-shutdown-phase del apagado ordenado (01-05), o ECS matará el proceso a mitad.
aws ecs register-task-definition --cli-input-json file://tarea-ciclourbana.json

aws ecs create-service \
  --cluster ciclourbana --service-name ciclourbana-web \
  --task-definition ciclourbana:1 \
  --desired-count 2 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-0aa11bb22cc33dd44,subnet-0ee55ff66gg77hh88],securityGroups=[sg-0cc44dd55ee66ff77],assignPublicIp=DISABLED}" \
  --load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:eu-west-1:111122223333:targetgroup/ciclourbana-tg/1a2b3c4d5e6f7g8h,containerName=ciclourbana,containerPort=8080" \
  --health-check-grace-period-seconds 90 \
  --deployment-configuration "minimumHealthyPercent=100,maximumPercent=200"
  • desired-count 2 reparte las tareas entre las dos AZ: si una zona cae, el servicio sigue.
  • assignPublicIp=DISABLED mantiene las tareas fuera de Internet. Necesitan salida para descargar la imagen y hablar con Secrets Manager, lo que exige un NAT Gateway (unos 35 €/mes) o, más barato y más seguro, VPC endpoints para ECR, S3, Secrets Manager y CloudWatch Logs.
  • health-check-grace-period-seconds 90 da margen al arranque de la JVM antes de que el ALB empiece a penalizar. Sin él, el ALB marca la tarea como no sana a los pocos segundos, ECS la mata y arranca otra: un bucle infinito de tareas que nunca llegan a estar listas.
  • minimumHealthyPercent=100, maximumPercent=200 define el rolling update de 08-01: con 2 tareas deseadas, ECS puede llegar a 4 y nunca baja de 2. Arranca las nuevas, espera a que estén sanas, y solo entonces retira las viejas. Sin corte de servicio, con convivencia de versiones — de ahí la exigencia de compatibilidad hacia atrás del esquema.

  1. El balanceador, la salud y el TLS

aws elbv2 create-target-group \
  --name ciclourbana-tg --protocol HTTP --port 8080 \
  --vpc-id vpc-0f1e2d3c4b5a69788 --target-type ip \
  --health-check-path /actuator/health/readiness \
  --health-check-interval-seconds 15 --health-check-timeout-seconds 5 \
  --healthy-threshold-count 2 --unhealthy-threshold-count 3 \
  --matcher HttpCode=200

Este es el momento en que 07-01 encaja en su sitio. La comprobación de estado apunta a /actuator/health/readiness, no a /actuator/health ni a /, y la diferencia es sustancial:

Ruta Qué agrega Consecuencia
/ Nada; es un 404 o el controlador raíz No dice nada sobre si la app puede trabajar
/actuator/health Todos los indicadores, incluidos los no críticos Una pasarela de pagos caída sacaría a todas las tareas de rotación
/actuator/health/readiness Solo db y el estado de disponibilidad (07-01) Lo correcto: sale de rotación quien no puede atender, sin efectos colaterales

Y el registro se cierra con deregistration_delay, la pieza que evita los 502 del apagado:

aws elbv2 modify-target-group-attributes \
  --target-group-arn arn:aws:elasticloadbalancing:eu-west-1:111122223333:targetgroup/ciclourbana-tg/1a2b3c4d5e6f7g8h \
  --attributes Key=deregistration_delay.timeout_seconds,Value=30

Durante esos 30 segundos el ALB deja de enviar peticiones nuevas a la tarea que se retira pero termina las en curso. Es el equivalente del preStop de 08-01 gestionado por el balanceador.

El certificado y los oyentes:

# Certificado gratuito, validado por DNS y renovado automaticamente
aws acm request-certificate \
  --domain-name ciclourbana.ribalta.example \
  --validation-method DNS --region eu-west-1

# Oyente 443 con TLS
aws elbv2 create-listener --load-balancer-arn arn:aws:elasticloadbalancing:eu-west-1:111122223333:loadbalancer/app/ciclourbana-alb/9z8y7x6w5v4u3t2s \
  --protocol HTTPS --port 443 \
  --certificates CertificateArn=arn:aws:acm:eu-west-1:111122223333:certificate/1234abcd-56ef-78gh-90ij-klmnopqrstuv \
  --ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
  --default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:eu-west-1:111122223333:targetgroup/ciclourbana-tg/1a2b3c4d5e6f7g8h

# Oyente 80 que solo redirige a 443
aws elbv2 create-listener --load-balancer-arn arn:aws:elasticloadbalancing:eu-west-1:111122223333:loadbalancer/app/ciclourbana-alb/9z8y7x6w5v4u3t2s \
  --protocol HTTP --port 80 \
  --default-actions '[{"Type":"redirect","RedirectConfig":{"Protocol":"HTTPS","Port":"443","StatusCode":"HTTP_301"}}]'

Como el ALB termina el TLS y habla HTTP con la tarea, hace falta lo de 08-01 en application-prod.yml:

server:
  forward-headers-strategy: framework

Sin ello, el Location de un 201 Created saldría con http:// y una dirección privada. Y con ello, X-Forwarded-Proto: https llega desde un proxy de confianza —el grupo de seguridad garantiza que solo el ALB alcanza el puerto 8080—, así que es seguro confiar en la cabecera.

Por último, Route 53 con un alias (no un CNAME) hacia el ALB, que resuelve directamente y no cuesta consultas adicionales.

  1. Migraciones de Flyway en AWS

La definición de tarea desactivó Flyway (SPRING_FLYWAY_ENABLED=false). Las migraciones se ejecutan como una tarea puntual de ECS con la misma imagen, antes de actualizar el servicio:

aws ecs run-task \
  --cluster ciclourbana \
  --task-definition ciclourbana-migraciones:1 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-0aa11bb22cc33dd44],securityGroups=[sg-0cc44dd55ee66ff77],assignPublicIp=DISABLED}" \
  --overrides '{"containerOverrides":[{"name":"ciclourbana","command":["java","-Dspring.flyway.enabled=true","-Dspring.main.web-application-type=none","-jar","/app/ciclourbana.jar"]}]}'

web-application-type=none levanta el contexto sin Tomcat: Flyway migra y el proceso termina. La canalización de 08-05 espera a que la tarea acabe y solo continúa si el código de salida es 0.

Tarea puntual antes del despliegue Al arrancar cada tarea
Con varias tareas Ya está migrado cuando arrancan Todas compiten por el bloqueo de Flyway
Si falla El despliegue se detiene; el servicio sigue con la versión anterior Todas las tareas nuevas fallan y ECS las reinicia en bucle
Migración larga Tiempo propio, sin sondas de salud encima Puede agotar el periodo de gracia y provocar reinicios
Visibilidad Un paso con su código de salida Enterrada en el log de arranque
Permisos La tarea de migración necesita DDL; la aplicación, no La aplicación necesita DDL siempre
Complejidad Una definición de tarea más y un paso en la canalización Ninguna

La tarea puntual es la opción correcta en cuanto hay más de una réplica, que es nuestro caso desde desired-count 2. Y con ddl-auto: validate (04-08) intacto: si el esquema no corresponde, la tarea no arranca en lugar de fallar consulta a consulta.

Recordatorio de 08-01: durante el rolling update conviven la versión antigua y la nueva contra el esquema ya migrado, así que cada migración debe ser compatible hacia atrás (expand/contract, 04-08). Es la condición para que revertir la aplicación funcione.

  1. Autoescalado

aws application-autoscaling register-scalable-target \
  --service-namespace ecs \
  --resource-id service/ciclourbana/ciclourbana-web \
  --scalable-dimension ecs:service:DesiredCount \
  --min-capacity 2 --max-capacity 6

aws application-autoscaling put-scaling-policy \
  --service-namespace ecs \
  --resource-id service/ciclourbana/ciclourbana-web \
  --scalable-dimension ecs:service:DesiredCount \
  --policy-name cpu-70 --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration '{
    "TargetValue": 70.0,
    "PredefinedMetricSpecification": {"PredefinedMetricType": "ECSServiceAverageCPUUtilization"},
    "ScaleInCooldown": 300, "ScaleOutCooldown": 60
  }'

El seguimiento de objetivo añade o quita tareas para mantener la CPU media en el 70 %. Los enfriamientos son asimétricos a propósito: crecer rápido (60 s), porque el coste de ir corto es servicio degradado, y decrecer despacio (300 s) para no oscilar ante un pico pasajero. La alternativa ALBRequestCountPerTarget escala por peticiones por tarea y suele ajustar mejor en una API: es una señal más directa de la carga real que la CPU, sobre todo cuando el tiempo se va esperando a la base de datos y la CPU no sube.

Y el límite que hay que respetar antes de escalar (08-01): max-capacity × maximum-pool-size + margen < max_connections. Una db.t4g.micro admite del orden de 80-100 conexiones; con 6 tareas y un pool de 10 son 60, más las migraciones y la administración: entra, pero sin holgura. Subir max-capacity a 12 sin tocar el pool provocaría too many clients justo en el pico de tráfico — el peor momento posible.

  1. Observabilidad: CloudWatch

El driver awslogs de la definición de tarea envía todo lo que la aplicación escribe en stdout a un grupo de CloudWatch Logs. Es el factor 11 de 08-01 con la plataforma haciendo su parte.

aws logs tail /ecs/ciclourbana --follow --since 10m
aws logs tail /ecs/ciclourbana --filter-pattern '"ERROR"' --since 1h

Dos ajustes que conviene hacer desde el principio:

# Retencion: sin esto, los logs se guardan para siempre y se facturan para siempre
aws logs put-retention-policy --log-group-name /ecs/ciclourbana --retention-in-days 30

Y logs estructurados en JSON, para que CloudWatch Logs Insights pueda consultar por campos (filter nivel = "ERROR" and estacionId = 3) en vez de por texto. Con el FiltroTraza con MDC de 03-06, cada línea lleva su identificador de traza y reconstruir una petición completa entre varias tareas es una consulta, no una búsqueda a ojo.

El tema completo —formato, correlación, agregación, retención y coste— es el de 09-05. En métricas, CloudWatch Container Insights aporta CPU, memoria y recuentos de tareas, y las alarmas se definen sobre HTTPCode_Target_5XX_Count, TargetResponseTime o UnHealthyHostCount. Las métricas de negocio de CicloUrbana —alquileres por minuto, estaciones sin bicicletas— llegan en 09-03 con Micrometer.

  1. Camino corto: Elastic Beanstalk

Si el objetivo es solo tener el JAR corriendo en AWS sin aprender ECS, Beanstalk crea por ti EC2, balanceador, autoescalado y despliegues:

eb init ciclourbana --platform "Corretto 21 running on 64bit Amazon Linux 2023" --region eu-west-1
eb create ciclourbana-prod --instance-type t3.small --envvars SPRING_PROFILES_ACTIVE=prod
./mvnw clean package -DskipTests
eb deploy
eb logs && eb open

Tres cosas que hay que saber. El puerto es el 5000, no el 8080: el proxy nginx de la plataforma envía ahí, y se resuelve con SERVER_PORT=5000. Por debajo hay instancias EC2 reales que hay que parchear (Beanstalk lo automatiza con actualizaciones gestionadas, pero el modelo mental es distinto de Fargate). Y .ebextensions/ contiene ficheros YAML que ajustan la plataforma —variables, opciones del balanceador, comprobación de estado—:

# .ebextensions/01-ciclourbana.config
option_settings:
  aws:elasticbeanstalk:application:environment:
    SERVER_PORT: 5000
    SPRING_PROFILES_ACTIVE: prod
    JAVA_TOOL_OPTIONS: "-XX:MaxRAMPercentage=75"
  aws:elasticbeanstalk:environment:process:default:
    HealthCheckPath: /actuator/health/readiness

Frente a Fargate, Beanstalk despliega un JAR en lugar de una imagen, mantiene instancias EC2 por debajo, da menos paridad con el portátil y menos control de red, y no encaja con lo que viene en 08-04 y 08-05. Para CicloUrbana es un buen segundo paso desde Heroku y un mal destino final: conserva la dependencia del artefacto JAR y de una plataforma propietaria, justo lo que la imagen de 07-04 vino a eliminar.

  1. Infraestructura como código

Todo lo anterior se ha hecho con comandos. Funciona una vez; no es sostenible. Los motivos son concretos: nadie recuerda dentro de seis meses por qué un grupo de seguridad tiene esa regla, no hay revisión por pares de un clic, no se puede recrear el entorno de pre idéntico al de prod, y no hay forma de saber qué cambió cuando algo dejó de funcionar.

La respuesta es infraestructura como código: declarar los recursos en ficheros versionados en Git.

# infra/ecs.tf — fragmento ilustrativo con Terraform
resource "aws_ecs_service" "ciclourbana" {
  name            = "ciclourbana-web"
  cluster         = aws_ecs_cluster.principal.id
  task_definition = aws_ecs_task_definition.ciclourbana.arn
  desired_count   = var.replicas          # 1 en pre, 2 en prod
  launch_type     = "FARGATE"

  network_configuration {
    subnets          = aws_subnet.privadas[*].id
    security_groups  = [aws_security_group.aplicacion.id]
    assign_public_ip = false
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.ciclourbana.arn
    container_name   = "ciclourbana"
    container_port   = 8080
  }

  deployment_minimum_healthy_percent = 100
  deployment_maximum_percent         = 200
  health_check_grace_period_seconds  = 90
}
terraform plan     # muestra exactamente qué va a cambiar, antes de cambiarlo
terraform apply
terraform destroy  # elimina TODO lo declarado: la limpieza en un comando

Las tres opciones habituales: CloudFormation (YAML/JSON, solo AWS, sin herramientas externas ni estado que gestionar), CDK (TypeScript, Java o Python, para quien prefiera programar la infraestructura; genera CloudFormation por debajo) y Terraform/OpenTofu (HCL, lo más extendido, multiproveedor y con un ecosistema enorme).

Ese terraform plan es el argumento decisivo: ver el cambio antes de aplicarlo, revisarlo en un pull request y tener el historial en Git. Y terraform destroy convierte la limpieza del apartado siguiente en un solo comando fiable en lugar de una lista de recursos que hay que recordar.

  1. Costes y limpieza

Advertencia destacada: RDS y el ALB se facturan por hora desde que existen, haya tráfico o no. No hay «modo reposo». Un entorno de práctica olvidado un mes son decenas de euros reales.

Recurso Configuración Coste aprox./mes
ECS Fargate 2 tareas × 0,5 vCPU + 1 GB ~30 €
RDS db.t4g.micro Una AZ, 20 GB gp3 ~18 €
RDS db.t4g.micro Multi-AZ Réplica en espera ~36 €
ALB Uno, tráfico bajo ~18 € + LCU
NAT Gateway Uno ~35 € + transferencia
VPC endpoints 4 interfaces ~28 €
ECR 5 GB ~0,50 €
Secrets Manager 2 secretos ~0,80 €
CloudWatch Logs 5 GB con 30 días ~3 €
Total realista de producción ~110-140 €/mes

El NAT Gateway sorprende a todo el mundo: cuesta más que las tareas que sirve. Alternativas: VPC endpoints para los servicios que se usan (más barato y además el tráfico no sale de la red de AWS), o subredes públicas con assignPublicIp=ENABLED solo en entornos de práctica y nunca para la base de datos.

Limpieza obligatoria al terminar la práctica, en este orden exacto:

# 1. Vaciar el servicio y eliminarlo
aws ecs update-service --cluster ciclourbana --service ciclourbana-web --desired-count 0
aws ecs delete-service  --cluster ciclourbana --service ciclourbana-web --force

# 2. Balanceador: primero los oyentes, luego el ALB, luego el target group
aws elbv2 delete-listener     --listener-arn arn:aws:elasticloadbalancing:eu-west-1:111122223333:listener/app/ciclourbana-alb/9z8y7x6w5v4u3t2s/aa11bb22cc33
aws elbv2 delete-load-balancer --load-balancer-arn arn:aws:elasticloadbalancing:eu-west-1:111122223333:loadbalancer/app/ciclourbana-alb/9z8y7x6w5v4u3t2s
aws elbv2 delete-target-group  --target-group-arn  arn:aws:elasticloadbalancing:eu-west-1:111122223333:targetgroup/ciclourbana-tg/1a2b3c4d5e6f7g8h

# 3. RDS: desactivar la proteccion y borrar (con instantanea final si interesa)
aws rds modify-db-instance --db-instance-identifier ciclourbana-prod --no-deletion-protection --apply-immediately
aws rds delete-db-instance --db-instance-identifier ciclourbana-prod --final-db-snapshot-identifier ciclourbana-final

# 4. El resto
aws ecs delete-cluster --cluster ciclourbana
aws ecr delete-repository --repository-name ciclourbana --force
aws secretsmanager delete-secret --secret-id ciclourbana/prod/jwt --force-delete-without-recovery
aws logs delete-log-group --log-group-name /ecs/ciclourbana
aws ec2 delete-nat-gateway --nat-gateway-id nat-0a1b2c3d4e5f60718
# y liberar la IP elastica asociada, borrar VPC endpoints, subredes y VPC

Verificación final —lo que de verdad evita sorpresas—: entra en Billing → Cost Explorer al día siguiente y comprueba que el gasto diario ha caído a cero. Y configura un presupuesto con alerta de 20 $ (aws budgets create-budget) antes de empezar cualquier práctica, no después.

Los recursos que más se olvidan y siguen facturando: NAT Gateway, IPs elásticas sin asociar, instantáneas manuales de RDS, volúmenes EBS huérfanos y grupos de logs sin retención.

  1. Seguridad: las reglas que no se negocian

  • Nunca claves de acceso en el código, en la imagen ni en variables. Los roles IAM dan credenciales temporales y rotadas. Si encuentras un AWS_SECRET_ACCESS_KEY en un repositorio, rótala antes de borrarla: el historial de Git la conserva.
  • Mínimo privilegio, siempre con ARNs concretos. "Resource": "*" es casi siempre un error. Usa IAM Access Analyzer para detectar permisos concedidos y nunca usados.
  • La cuenta raíz no se usa: MFA activado, guardada, y se trabaja con identidades federadas con MFA obligatorio. Y CloudTrail activado desde el primer día: el día que pase algo raro, es la única fuente de verdad sobre quién hizo qué.
  • Cifrado en reposo y en tránsito. --storage-encrypted en RDS (irreversible después), sslmode=require en la URL JDBC, ELBSecurityPolicy-TLS13-1-2-2021-06 en el oyente y IMMUTABLE en ECR.
  • Nada expuesto que no deba estarlo. Solo el ALB en subredes públicas; grupos de seguridad referenciados entre sí, no rangos de direcciones; el puerto 5432 jamás abierto a Internet.
  • Rotación. El secreto del JWT y la contraseña de RDS son rotables desde Secrets Manager; conviene programarlo, y hacerlo obligatoriamente cuando alguien con acceso deja el proyecto.

Errores Comunes y Consejos

Construir la imagen en Apple Silicon y desplegarla en Fargate x86. La tarea muere con exec format error. Usa docker buildx build --platform linux/amd64 o construye en la canalización.

Comprobación de estado apuntando a /. Devuelve 404, el ALB marca la tarea como no sana y ECS entra en un bucle de arranques. Debe ser /actuator/health/readiness.

Sin periodo de gracia. El arranque de la JVM tarda 20-40 segundos; el ALB empieza a comprobar de inmediato y mata la tarea antes de que llegue a estar lista. --health-check-grace-period-seconds 90.

Olvidar la salida a Internet de las subredes privadas. La tarea no puede descargar la imagen de ECR y falla con CannotPullContainerError. Hacen falta VPC endpoints o un NAT Gateway.

Secretos como environment. Quedan legibles en describe-task-definition para cualquiera con permisos de lectura. Usa secrets con ARN. Y RDS accesible públicamente «para probar»: recibe ataques automatizados en minutos y contiene datos personales. Nunca.

Olvidar el NAT Gateway al limpiar. Sigue costando 35 €/mes con cero tráfico.

Consejo: aws ecs describe-services es tu diagnóstico. El campo events explica en texto claro por qué una tarea no arranca: imagen no descargable, salud fallida, sin capacidad, secreto inaccesible.

Consejo: dos entornos, una plantilla. pre y prod deben salir del mismo código de Terraform con distintas variables (replicas, db_instance_class). Es la paridad de 08-01 hecha infraestructura.

Consejo: etiqueta todo con Proyecto=CicloUrbana. Cost Explorer podrá desglosar el gasto por proyecto y encontrar recursos huérfanos será trivial.

Ejercicios

Ejercicio 1

Diseña la red completa de CicloUrbana en eu-west-1: VPC, subredes, tablas de rutas y los tres grupos de seguridad con sus reglas exactas de entrada y salida. Justifica dónde va cada componente (ALB, tareas, RDS) y explica cómo consultarías la base de datos desde tu portátil sin abrir el puerto 5432 a Internet.

Ejercicio 2

Se registra la definición de tarea ciclourbana:1, se crea el servicio con desired-count 2 y ninguna tarea llega a estado RUNNING. aws ecs describe-services muestra, en events, esta secuencia repetida: service ciclourbana-web has started 1 tasks, luego stopped 1 running tasks: Task failed ELB health checks in target-group ciclourbana-tg, y otra vez. Los logs de CloudWatch muestran que la aplicación arranca correctamente y escribe Started CicloUrbanaApplication in 31.4 seconds. Enumera las causas posibles en orden de probabilidad, di cómo distinguir entre ellas y da la corrección de cada una.

Ejercicio 3

CicloUrbana lleva tres meses en ECS Fargate con desired-count 2 y db.t4g.micro. Llega la Semana de la Movilidad de Ribalta y se espera cuadruplicar el tráfico durante cinco días. Diseña el plan completo: autoescalado, tamaño de la base de datos, pool de HikariCP, estrategia de despliegue durante el evento, qué vigilar y cuánto costará de más. Incluye el plan de vuelta a la normalidad.

Soluciones

Solución 1

Red. VPC 10.20.0.0/16. Seis subredes en dos AZ (eu-west-1a y eu-west-1b):

Subred CIDR AZ Ruta por defecto Contiene
publica-a 10.20.0.0/24 a Internet Gateway ALB
publica-b 10.20.1.0/24 b Internet Gateway ALB
privada-app-a 10.20.10.0/24 a NAT o endpoints Tareas Fargate
privada-app-b 10.20.11.0/24 b NAT o endpoints Tareas Fargate
privada-bd-a 10.20.20.0/24 a Ninguna RDS primaria
privada-bd-b 10.20.21.0/24 b Ninguna RDS en espera

Separar las subredes de aplicación de las de base de datos no es imprescindible pero sí buena práctica: su tabla de rutas no tiene salida ninguna, así que aunque alguien comprometiera el motor no podría exfiltrar datos hacia fuera.

Grupos de seguridad, encadenados:

Grupo Entrada Salida
sg-alb 443 y 80 desde 0.0.0.0/0 8080 hacia sg-app
sg-app 8080 desde sg-alb 5432 hacia sg-rds; 443 hacia endpoints/Internet
sg-rds 5432 desde sg-app Ninguna

La clave es que ningún grupo referencia rangos de direcciones para el tráfico interno: se referencian entre sí. Como las tareas de Fargate cambian de IP en cada despliegue, con reglas por CIDR habría que actualizarlas constantemente; con referencias por grupo, no hay nada que mantener.

Acceso desde el portátil, sin abrir nada. La forma correcta es una instancia bastión mínima en subred privada con SSM Session Manager y reenvío de puertos:

aws ssm start-session --target i-0a1b2c3d4e5f60718 \
  --document-name AWS-StartPortForwardingSessionToRemoteHost \
  --parameters '{"host":["ciclourbana-prod.abc123xyz.eu-west-1.rds.amazonaws.com"],"portNumber":["5432"],"localPortNumber":["55432"]}'
# Ahora: psql -h localhost -p 55432 -U consulta_lectura ciclourbana

Ventajas: no se abre ningún puerto de entrada (el agente SSM inicia la conexión saliente), no hay claves SSH que gestionar, y CloudTrail deja constancia de quién abrió la sesión y cuándo. Y una capa más de mínimo privilegio: conectarse con un usuario de PostgreSQL de solo lectura, no con el de la aplicación ni con el maestro.

Solución 2

El síntoma es inequívoco: la aplicación arranca bien pero el ALB no la considera sana. Causas, de más a menos probable:

1. Falta el periodo de gracia. La app tarda 31,4 s en arrancar. Sin --health-check-grace-period-seconds, el ALB empieza a comprobar en cuanto la tarea se registra; con unhealthy-threshold-count 3 e intervalo de 15 s, la declara no sana a los ~45 s de vida de la tarea... pero las comprobaciones fallidas empiezan mucho antes y ECS mata la tarea. Distinguir: los eventos ocurren en los primeros 60 segundos de cada tarea. Corregir: --health-check-grace-period-seconds 90.

2. La ruta de comprobación no responde 200. Si el target group apunta a / (que no existe) o a /actuator/health con un indicador no crítico caído, la respuesta no es 200. Distinguir: lanzar una tarea puntual en la misma subred y hacer curl a la IP privada de la tarea. Corregir: --health-check-path /actuator/health/readiness y comprobar que la cadena de seguridad de 07-01 deja ese endpoint público; si Spring Security lo protege, devolverá 401 y el ALB lo interpretará como fallo.

3. Puerto equivocado. El target group apunta al 8080 y la app escucha en el 8081 porque MANAGEMENT_SERVER_PORT no se ajustó, o el portMappings no coincide. Distinguir: en los logs, la línea Tomcat started on port(s). Corregir: alinear containerPort, portMappings y el puerto real.

4. Grupo de seguridad. Si sg-app no permite el 8080 desde sg-alb, la comprobación no llega y expira. Distinguir: en el target group, el motivo es Request timed out en lugar de un código HTTP. Corregir: la regla de entrada por referencia de grupo.

5. target-type incorrecto. Con awsvpc, el target group debe ser --target-type ip, no instance. Con instance el registro directamente no funciona. Distinguir: no hay destinos registrados en el target group.

Método de diagnóstico general: mirar en la consola del target group la columna «Health status details», que dice literalmente si fue Request timed out (red o puerto), Health checks failed with these codes: [404] (ruta), [401] (seguridad) o [503] (la app responde pero readiness está DOWN, típicamente porque no alcanza RDS).

Solución 3

Autoescalado. Subir el techo y bajar el suelo de reacción:

aws application-autoscaling register-scalable-target ... --min-capacity 4 --max-capacity 12

min-capacity 4 durante el evento evita que el primer pico de la mañana encuentre solo 2 tareas y tenga que escalar bajo presión. Y añadir una política por peticiones (ALBRequestCountPerTarget, objetivo 300) además de la de CPU: en una API que espera a la base de datos, las peticiones son una señal más temprana que la CPU. Con dos políticas, ECS toma el máximo de ambas. Reducir ScaleOutCooldown a 30 s.

Base de datos. db.t4g.micro no aguanta cuatro veces el tráfico: tiene créditos de CPU en modo ráfaga que se agotan y entonces el rendimiento cae en picado, que es el peor modo de fallo posible. Subir a db.t4g.medium o db.m6g.large una semana antes (el cambio requiere una parada breve, que hay que hacer en ventana nocturna, no durante el evento). Verificar --multi-az activo. Y considerar una réplica de lectura si las consultas de disponibilidad de estaciones dominan el tráfico, apuntando a ella las lecturas.

Pool de HikariCP. El cálculo de 08-01 con los números nuevos: 12 tareas × pool + margen < max_connections. Una db.t4g.medium admite unas 340 conexiones. Con pool de 10 son 120 más margen: entra holgadamente. Con db.t4g.micro (unas 85) no entraría y ese es un segundo motivo, independiente del rendimiento, para subir de clase. Dejar maximum-pool-size: 10 y connection-timeout: 3000 para que, si hubiera saturación, las peticiones fallen rápido en lugar de acumularse.

Estrategia de despliegue durante el evento. Congelación de cambios: nada a producción durante los cinco días salvo correcciones críticas. Si hay que desplegar, minimumHealthyPercent=100 y maximumPercent=200 como siempre, fuera de las horas punta (madrugada), con la tarea de migración ejecutada antes y sin migraciones destructivas — solo expand, nunca contract (04-08).

Qué vigilar, con alarmas creadas antes del evento:

Métrica Umbral Por qué
HTTPCode_Target_5XX_Count > 1 % de las peticiones Señal principal de degradación
TargetResponseTime p99 > 1,5 s Latencia percibida
UnHealthyHostCount > 0 durante 2 min Tareas cayendo
CPUUtilization de RDS > 80 % La base de datos es el cuello de botella habitual
DatabaseConnections > 70 % del máximo Aviso temprano de too many clients
CPUCreditBalance (si sigue en t4g) Bajando El fallo silencioso de las instancias en ráfaga
hikaricp.connections.pending > 0 sostenido El pool se queda corto (métricas de 09-03)

Coste adicional estimado: pasar de 2 a una media de ~7 tareas durante 5 días son unos 12-15 € extra; subir RDS de micro a medium durante dos semanas, unos 25-30 €; más tráfico del ALB y logs, unos pocos euros. Total: 40-50 €, cifra perfectamente defendible frente al coste reputacional de que el servicio caiga en la semana estrella de la ciudad.

Vuelta a la normalidad. Bajar min-capacity a 2 y max-capacity a 6; esperar una semana con la instancia grande antes de reducirla, porque el tráfico suele quedarse por encima del punto de partida, y hacerlo en ventana nocturna; retirar la réplica de lectura si se creó; revisar Cost Explorer para confirmar que todo lo temporal ha desaparecido; y escribir la retrospectiva —qué se saturó primero, qué alarma avisó a tiempo y cuál no—, que es lo que hace que el evento del año que viene sea rutina.

Conclusión

CicloUrbana ya no vive en una plataforma prestada: corre sobre una infraestructura propia, en una región europea, con la base de datos donde Internet no llega. Sabes qué servicios de AWS pueden ejecutar una aplicación Spring Boot y por qué el curso elige ECS Fargate + RDS —sin servidores que administrar, destino natural de la imagen de 07-04, red privada de verdad y mucho más simple que Kubernetes para una sola aplicación—, con Elastic Beanstalk documentado como camino corto y Lambda descartada con argumentos.

Manejas los conceptos que sostienen todo lo demás: región y zonas de disponibilidad, VPC con subredes públicas y privadas, grupos de seguridad referenciados entre sí en lugar de rangos de direcciones, roles y políticas IAM con ARNs concretos, ALB, Route 53 y ACM. Has publicado la imagen en ECR con escaneo automático y etiquetas inmutables —lo que hace exacta la reversión de 08-01—, y sabes que Fargate espera linux/amd64. Has creado RDS PostgreSQL 16 cifrado, Multi-AZ, con copias de siete días, ventana de mantenimiento nocturna, protección contra borrado y, sobre todo, sin dirección pública, accesible únicamente desde el grupo de seguridad de la aplicación y consultable desde fuera solo por Session Manager. Los secretos viven en Secrets Manager, entran en el contenedor por el bloque secrets de la definición de tarea —nunca como variables en texto plano— y la aplicación sigue leyendo ${JWT_SECRETO} sin enterarse de que existe AWS.

La definición de tarea reúne todo lo aprendido: CPU y memoria dimensionadas con MaxRAMPercentage, dos roles IAM con responsabilidades distintas, zona horaria de Ribalta, logs a CloudWatch por el driver awslogs y stopTimeout holgado para que quepa el apagado ordenado de 01-05. El servicio mantiene dos tareas repartidas entre zonas, el ALB comprueba /actuator/health/readiness —el momento en que las sondas de 07-01 encuentran su sitio—, termina el TLS con un certificado de ACM que se renueva solo, redirige el puerto 80 al 443 y espera 30 segundos antes de desregistrar una tarea para no producir 502. El despliegue es rolling con minimumHealthyPercent=100, las migraciones de Flyway se ejecutan como tarea puntual antes de actualizar el servicio, y el autoescalado crece rápido y decrece despacio, siempre dentro del límite de conexiones de PostgreSQL.

Y sabes lo que cuesta: unos 110-140 € al mes, con el NAT Gateway costando más que las propias tareas y todo facturado por hora aunque nadie use la aplicación. Por eso tienes la lista exacta de recursos a destruir en su orden correcto, el presupuesto con alerta creado antes de empezar, y las reglas de seguridad que no se negocian —cero claves de acceso, mínimo privilegio con ARNs concretos, cifrado en reposo y en tránsito, CloudTrail y rotación programada—, junto con la nota de fondo de que la consola web no es forma de gestionar producción: infraestructura como código, revisada en un pull request y destruible con un comando.

Queda una pieza del panorama de 08-01 por recorrer. Cuando la red de Ribalta deje de ser una aplicación y sean cinco, con tres equipos desplegando por su cuenta y el ayuntamiento preguntando si esto se puede mover a otro proveedor, la respuesta es un orquestador. La siguiente lección, Desplegando en Kubernetes, lo aborda con honestidad —empezando por advertir que para una sola aplicación como CicloUrbana suele ser complejidad no justificada— y con los manifiestos completos: Deployment, Service, Ingress, ConfigMap, Secret, un Job para las migraciones, HPA para escalar, un chart de Helm para no duplicar nada, y las tres sondas —liveness, readiness y startup— apuntando por fin a los grupos de salud que definimos en 07-01.

Curso de Spring Boot

Módulo 1: Introducción a Spring Boot

Módulo 2: Conceptos Básicos de Spring Boot

Módulo 3: Construyendo Servicios Web RESTful

Módulo 4: Acceso a Datos con Spring Boot

Módulo 5: Seguridad en Spring Boot

Módulo 6: Pruebas en Spring Boot

Módulo 7: Funciones Avanzadas de Spring Boot

Módulo 8: Despliegue de Aplicaciones Spring Boot

Módulo 9: Rendimiento y Monitoreo

Módulo 10: Mejores Prácticas y Consejos

© Copyright 2026. Todos los derechos reservados