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
- Panorama: qué servicio de AWS ejecuta una aplicación Spring Boot
- Conceptos base imprescindibles
- Preparar la imagen: ECR
- La base de datos: RDS PostgreSQL
- Secretos: Secrets Manager y Parameter Store
- Desplegar en ECS Fargate
- El balanceador, la salud y el TLS
- Migraciones de Flyway en AWS
- Autoescalado
- Observabilidad: CloudWatch
- Camino corto: Elastic Beanstalk
- Infraestructura como código
- Costes y limpieza
- Seguridad: las reglas que no se negocian
- Errores Comunes y Consejos
- Ejercicios
- 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.
- 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.
- 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.0Dos 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).
- 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-protectionPará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 CIDRReferenciar 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».
- 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.
- 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).
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: 512ymemory: 1024son 0,5 vCPU y 1 GB. Fargate solo admite combinaciones concretas (0,5 vCPU admite 1, 2, 3 o 4 GB). ConMaxRAMPercentage=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.
secretsfrente aenvironment: el valor nunca aparece en la definición. El sufijo:password::extrae un campo concreto del JSON del secreto de RDS, que contieneusernameypassword.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/readinesscon 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 entreSIGTERMySIGKILL. Debe superar eltimeout-per-shutdown-phasedel 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 2reparte las tareas entre las dos AZ: si una zona cae, el servicio sigue.assignPublicIp=DISABLEDmantiene 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 90da 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=200define 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.
- 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=200Este 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=30Durante 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:
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.
- 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.
- 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.
- 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 1hDos 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 30Y 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.
- 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 openTres 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/readinessFrente 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.
- 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 comandoLas 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.
- 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 VPCVerificació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.
- 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_KEYen 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-encrypteden RDS (irreversible después),sslmode=requireen la URL JDBC,ELBSecurityPolicy-TLS13-1-2-2021-06en el oyente yIMMUTABLEen 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 ciclourbanaVentajas: 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:
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
- ¿Qué es Spring Boot?
- Configuración de tu Entorno de Desarrollo
- Creando tu Primera Aplicación Spring Boot
- Entendiendo la Estructura del Proyecto
- El Arranque y el Ciclo de Vida de la Aplicación
Módulo 2: Conceptos Básicos de Spring Boot
- Anotaciones de Spring Boot
- Inyección de Dependencias en Spring Boot
- Ámbito y Ciclo de Vida de los Beans
- Configuración de Spring Boot
- Propiedades de Spring Boot
- Autoconfiguración y Starters por Dentro
Módulo 3: Construyendo Servicios Web RESTful
- Introducción a los Servicios Web RESTful
- Creando Controladores REST
- Manejo de Métodos HTTP
- Validación de Datos de Entrada
- DTOs y Mapeo entre Capas
- Manejo de Excepciones en REST
- Documentar la API con OpenAPI
Módulo 4: Acceso a Datos con Spring Boot
- Introducción a Spring Data JPA
- Configuración de Fuentes de Datos
- Creación de Entidades JPA
- Relaciones entre Entidades
- Uso de Repositorios de Spring Data
- Métodos de Consulta en Spring Data JPA
- Transacciones y Gestión de la Persistencia
- Migraciones de Esquema con Flyway
Módulo 5: Seguridad en Spring Boot
- Introducción a Spring Security
- Configuración de Spring Security
- Autenticación y Autorización de Usuarios
- Implementación de Autenticación JWT
- Seguridad a Nivel de Método y Endurecimiento de la API
Módulo 6: Pruebas en Spring Boot
- Introducción a las Pruebas
- Pruebas Unitarias con JUnit
- Simulación con Mockito
- Pruebas de Integración
- Pruebas con Testcontainers
Módulo 7: Funciones Avanzadas de Spring Boot
- Spring Boot Actuator
- Perfiles de Spring Boot
- Tareas Programadas y Ejecución Asíncrona
- Spring Boot con Docker
- Spring Boot y Microservicios
- Comunicación entre Servicios y Tolerancia a Fallos
Módulo 8: Despliegue de Aplicaciones Spring Boot
- Introducción al Despliegue
- Desplegando en Heroku
- Desplegando en AWS
- Desplegando en Kubernetes
- Integración y Entrega Continua
Módulo 9: Rendimiento y Monitoreo
- Ajuste de Rendimiento
- Caché con Spring Cache
- Monitoreo con Spring Boot Actuator
- Uso de Prometheus y Grafana
- Gestión de Registros y Logs
- Trazabilidad Distribuida
