La lección anterior terminó con una limitación concreta: red-mercadofresco.yaml describe seis subredes casi idénticas escritas a mano, una detrás de otra, porque YAML no tiene bucles. Tampoco tiene funciones, ni tipos, ni forma de encapsular «la red estándar de MercadoFresco» en algo que se reutilice en tres entornos. Y no hay manera de escribir una prueba que verifique que ninguna plantilla abre el puerto 22 al mundo. El AWS Cloud Development Kit resuelve las cuatro cosas de una sola forma: dejando que la infraestructura se escriba en un lenguaje de programación de verdad.
Lo importante desde el primer minuto es que el CDK no sustituye a CloudFormation. Todo lo de 09-01 —pilas, conjuntos de cambios, deriva, DeletionPolicy, ROLLBACK_COMPLETE— sigue siendo exactamente igual de cierto. El CDK escribe las plantillas por ti.
Aviso de coste. El CDK es gratuito; pagas los recursos que crea.
cdk bootstrapcrea un bucket de S3, un repositorio de ECR y cinco roles de IAM por cuenta y región: céntimos al mes mientras estén vacíos. Los ejemplos de esta lección despliegan una VPC con NAT (unos 0,045 USD/hora cada uno) y un ALB (unos 0,025 USD/hora): si sigues los ejercicios, destruye las pilas al terminar con el comando del apartado de limpieza. Datos ficticios.
Contenido
- Qué es el CDK y su relación con CloudFormation
- Por qué compensa, y cuándo CloudFormation puro sigue siendo mejor
- Instalación,
cdk inity estructura del proyecto - App, Stack, Construct y los tres niveles de constructos
- La red de MercadoFresco en CDK
- Un constructo propio:
MercadoFrescoVpc - Las capas de aplicación e integración en Python
- Activos: el código de las Lambdas dentro de la pila
- El ciclo de vida:
synth,diff,deploy,destroyybootstrap - Contexto, entornos explícitos y una pila por entorno
- Aspectos: etiquetado obligatorio y reglas de auditoría
- Pruebas de infraestructura
- Recursos con estado,
RemovalPolicyy el peligro decdk destroy - CDK Pipelines: el pipeline que se despliega a sí mismo
- Terraform, CDK for Terraform y la comparación honesta
- Coste y limpieza
- Errores comunes y consejos
- Ejercicios
- Conclusión
Qué es el CDK y su relación con CloudFormation
El CDK es una biblioteca de clases —disponible en TypeScript, JavaScript, Python, Java, C# y Go— con la que se describe la infraestructura como objetos. Cuando ejecutas cdk synth, esos objetos se sintetizan: producen una plantilla de CloudFormation, que después se despliega como una pila normal.
flowchart LR
A[Codigo TypeScript<br/>o Python] -->|cdk synth| B[Plantilla<br/>CloudFormation]
B --> C[cdk.out/<br/>artefacto de nube]
C -->|cdk deploy| D[Conjunto de cambios<br/>en CloudFormation]
D --> E[Pila desplegada:<br/>recursos reales]
E -->|cdk diff| A
De ese flujo salen tres consecuencias prácticas que conviene interiorizar antes de escribir una línea:
- El artefacto de despliegue sigue siendo una plantilla. Puedes leerla, versionarla, revisarla y desplegarla sin el CDK.
cdk.out/contiene el resultado exacto de la síntesis, ycdk synth > plantilla.yamlte lo deja en un fichero. - Los errores de CloudFormation siguen ocurriendo. Un
ROLLBACK_COMPLETEen una pila del CDK se diagnostica y se resuelve como en 09-01: mirando los eventos de la pila. - El código no se ejecuta en AWS. Se ejecuta en tu portátil o en CodeBuild, y su única salida es JSON. No hay nada «vivo» del CDK en la cuenta salvo lo que crea el bootstrap.
Por qué compensa, y cuándo CloudFormation puro sigue siendo mejor
Lo que aporta un lenguaje de programación no es sintaxis más bonita: son cinco capacidades que YAML no tiene. Bucles, para las seis subredes. Condicionales de verdad, evaluados en la síntesis y no en el despliegue, lo que hace que la plantilla generada sea más simple, no más compleja. Tipos, que convierten un error de propiedad en un fallo de compilación en el editor, no en un CREATE_FAILED a los ocho minutos. Reutilización, encapsulando patrones en clases con nombre. Y pruebas, ejecutables en el pipeline como las del código de aplicación.
Pero el CDK no es la respuesta correcta en todos los casos, y conviene decirlo sin adornos:
| Criterio | CloudFormation puro | AWS CDK |
|---|---|---|
| Equipo sin cultura de programación | Mejor: un YAML lo lee cualquiera | Peor: hay que saber TypeScript o Python |
| Infraestructura estable, que cambia dos veces al año | Mejor: nada que mantener | Peor: dependencias que actualizar cada trimestre |
| Auditoría externa que revisa la infraestructura | Mejor: el artefacto es lo que se revisa | Peor: hay que auditar código y plantilla generada |
| Mucha repetición (subredes, entornos, cuentas) | Peor: copiar y pegar | Mejor: bucles y constructos |
| Necesidad de pruebas automáticas | Peor: cfn-guard y poco más |
Mejor: pruebas unitarias reales |
| Abstracciones propias compartidas entre equipos | Peor: módulos poco extendidos | Mejor: una biblioteca versionada |
| Curva de entrada | Mejor: se aprende en una tarde | Peor: una semana hasta ser productivo |
| Depuración de un fallo raro | Peor y mejor a la vez: solo hay una capa | Peor: dos capas, código y plantilla |
MercadoFresco elige CDK por dos razones concretas de su situación: tiene tres entornos que deben ser idénticos y un equipo de dos personas que ya programan a diario. Si el equipo fuera de sistemas puros y la infraestructura llevara dos años sin tocarse, la respuesta correcta sería quedarse en YAML.
Instalación, cdk init y estructura del proyecto
El CDK se instala con npm, incluso para proyectos en Python: la herramienta de línea de comandos es Node.
npm install -g aws-cdk # herramienta de linea de comandos
cdk --version # 2.x
mkdir infra-cdk && cd infra-cdk
cdk init app --language typescript # o --language pythoncdk init genera un proyecto ejecutable. Lo importante de su estructura:
| Fichero | Qué es |
|---|---|
bin/infra-cdk.ts |
Punto de entrada: instancia la App y las pilas |
lib/*-stack.ts |
Las pilas: aquí vive la infraestructura |
test/*.test.ts |
Pruebas con Jest, ya configuradas |
cdk.json |
Configuración: comando de ejecución, contexto y banderas de compatibilidad |
cdk.out/ |
Salida de la síntesis. No se versiona |
package.json |
Dependencias: aws-cdk-lib y constructs |
Una advertencia sobre cdk.json: la sección context incluye banderas de funcionalidad (@aws-cdk/aws-*:featureFlag) que cambian el comportamiento por omisión de los constructos. Se generan con la versión del CDK que creó el proyecto y no deben tocarse a mano: modificarlas puede cambiar el ARN o el nombre de recursos ya desplegados, con reemplazo incluido.
En el repositorio mercadofresco-infra, el proyecto vive en infra-cdk/ junto a las plantillas de 09-01, que se mantienen mientras dura la migración.
App, Stack, Construct y los tres niveles de constructos
Tres conceptos, y todo lo demás se deduce de ellos:
- App: la aplicación CDK completa, raíz del árbol. Una App contiene pilas.
- Stack: la unidad de despliegue, que corresponde 1:1 con una pila de CloudFormation. Los límites de 09-01 —500 recursos— siguen aplicando.
- Construct: cualquier nodo del árbol. Un recurso, un grupo de recursos o una pila entera son constructos. Todos reciben los mismos tres argumentos:
scope(el padre),id(único entre hermanos) yprops.
El id es el equivalente del nombre lógico de 09-01, y hereda su regla más importante: cambiar el id de un constructo desplegado destruye y recrea el recurso. El CDK compone los identificadores lógicos concatenando la ruta del árbol y añadiendo un sufijo, así que mover un constructo de sitio también lo recrea.
Los constructos tienen tres niveles de abstracción, y el mismo bucket ilustra la diferencia:
// NIVEL 1 (L1): calco directo del recurso de CloudFormation. Prefijo Cfn.
// No pone nada por defecto: si no lo declaras, no existe.
new s3.CfnBucket(this, 'FotosL1', {
bucketName: 'mercadofresco-catalogo-fotos',
bucketEncryption: { serverSideEncryptionConfiguration: [
{ serverSideEncryptionByDefault: { sseAlgorithm: 'aws:kms' } } ] },
versioningConfiguration: { status: 'Enabled' },
});
// NIVEL 2 (L2): API con valores por defecto sensatos, tipos y metodos de conveniencia.
const fotos = new s3.Bucket(this, 'FotosL2', {
encryption: s3.BucketEncryption.KMS,
encryptionKey: claveDatos,
versioned: true,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, // por defecto ya lo bloquea
});
fotos.grantRead(rolMiniaturas); // genera la politica de IAM minima necesaria
// NIVEL 3 (L3): patron completo. Varios recursos coordinados con una sola llamada.
new patterns.ApplicationLoadBalancedFargateService(this, 'Tienda', { /* ... */ });| Nivel | Qué es | Cuándo usarlo |
|---|---|---|
L1 (Cfn*) |
Traducción automática del esquema de CloudFormation | Un recurso o propiedad que el L2 aún no soporta |
| L2 | API cuidada, valores por defecto seguros, métodos grant*, metric* |
El 90 % del tiempo |
| L3 | Patrones que combinan varios recursos | Prototipos y arquitecturas estándar |
Dos avisos. Del L2 siempre puedes bajar al L1 con recurso.node.defaultChild as s3.CfnBucket para tocar una propiedad que la API no expone; es la válvula de escape que evita quedarse bloqueado. Y los L3 son cómodos y opacos: crean más recursos de los que esperas y su configuración fina cuesta más que escribirla a mano, así que MercadoFresco los usa para prototipar y luego baja a L2.
Otra pieza que ahorra mucho tiempo son los métodos grant*: cola.grantConsumeMessages(rol) genera la política de IAM correcta —incluidos los permisos sobre la clave de KMS— sin que nadie escriba un JSON. Es, en la práctica, el mínimo privilegio de 04-01 por defecto.
La red de MercadoFresco en CDK
Las 190 líneas de YAML de 09-01 se convierten en esto:
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';
export class RedStack extends cdk.Stack {
public readonly vpc: ec2.Vpc;
constructor(scope: Construct, id: string, props: cdk.StackProps & { entorno: string }) {
super(scope, id, props);
this.vpc = new ec2.Vpc(this, 'Vpc', {
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
maxAzs: 2, // eu-west-1a y eu-west-1b
// Un NAT por AZ en produccion; uno solo en el resto. La condicion se evalua AQUI,
// en la sintesis, asi que la plantilla generada no lleva ningun Fn::If.
natGateways: props.entorno === 'produccion' ? 2 : 1,
subnetConfiguration: [
{ name: 'publica', subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24 },
{ name: 'app', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, cidrMask: 24 },
{ name: 'datos', subnetType: ec2.SubnetType.PRIVATE_ISOLATED, cidrMask: 24 },
],
gatewayEndpoints: {
S3: { service: ec2.GatewayVpcEndpointAwsService.S3 },
},
});
}
}Veintiocho líneas frente a ciento noventa, y el resultado sintetizado es equivalente: la misma VPC, seis subredes (tres configuraciones × dos AZ), el gateway de internet, los NAT, cuatro tablas de rutas con sus asociaciones y el endpoint de S3. Lo que ha desaparecido no es la infraestructura, es la repetición mecánica.
Merece la pena detenerse en tres detalles:
subnetConfigurationes el bucle. Declaras tres tipos de subred y el CDK crea una por AZ, calculando los CIDR automáticamente a partir del bloque de la VPC y decidrMask. Añadir una tercera AZ es cambiarmaxAzs: 2por3.SubnetTypecodifica la intención, no la implementación:PRIVATE_WITH_EGRESSsignifica «privada con salida por NAT» yPRIVATE_ISOLATED, «sin salida a internet». Las subredes de datos de MercadoFresco son exactamente lo segundo, y el CDK no les crea ruta a0.0.0.0/0porque el tipo lo prohíbe.- El condicional del NAT se resuelve en la síntesis. En 09-01 hacía falta un
Mappings, unaConditiony unFn::If; aquí es un operador ternario de TypeScript y la plantilla resultante no tiene ninguna condición. Esa es la diferencia entre evaluar en tiempo de síntesis y evaluar en tiempo de despliegue, y es la razón por la que las plantillas generadas suelen ser más simples que las escritas a mano.
Un matiz honesto: esa concisión tiene un precio. El L2 de ec2.Vpc toma decisiones por ti —el reparto de CIDR, los nombres, el número de tablas de rutas— y para saber exactamente qué ha creado hay que mirar la plantilla sintetizada. Por eso cdk synth no es un comando de depuración ocasional: es parte del flujo normal.
Un constructo propio: MercadoFrescoVpc
La reutilización de verdad llega al encapsular el patrón en una clase con las decisiones de MercadoFresco ya tomadas:
export interface MercadoFrescoVpcProps {
readonly entorno: 'desarrollo' | 'preproduccion' | 'produccion';
readonly cidr?: string;
}
export class MercadoFrescoVpc extends Construct {
public readonly vpc: ec2.Vpc;
public readonly sgTienda: ec2.SecurityGroup;
constructor(scope: Construct, id: string, props: MercadoFrescoVpcProps) {
super(scope, id);
const esProduccion = props.entorno === 'produccion';
this.vpc = new ec2.Vpc(this, 'Vpc', {
ipAddresses: ec2.IpAddresses.cidr(props.cidr ?? '10.0.0.0/16'),
maxAzs: 2,
natGateways: esProduccion ? 2 : 1,
subnetConfiguration: [
{ name: 'publica', subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24 },
{ name: 'app', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, cidrMask: 24 },
{ name: 'datos', subnetType: ec2.SubnetType.PRIVATE_ISOLATED, cidrMask: 24 },
],
gatewayEndpoints: { S3: { service: ec2.GatewayVpcEndpointAwsService.S3 } },
// Los registros de flujo son obligatorios en produccion (05-03) y caros en desarrollo.
flowLogs: esProduccion ? { registro: { trafficType: ec2.FlowLogTrafficType.ALL } } : {},
});
this.sgTienda = new ec2.SecurityGroup(this, 'SgTienda', {
vpc: this.vpc,
description: 'Instancias de la tienda de MercadoFresco',
allowAllOutbound: true,
});
}
}Usarlo en los tres entornos es una línea por entorno, y cualquier mejora del patrón llega a los tres a la vez: si mañana se decide activar los registros de flujo también en preproducción, se cambia en un sitio. Eso es lo que en 09-01 no existía, porque un Mappings con tres columnas no es una abstracción: es una tabla.
La convención de MercadoFresco es que los constructos propios exponen lo que los consumidores necesitan y nada más —aquí, vpc y sgTienda—, para poder cambiar el interior sin romper a nadie. Es encapsulación aplicada a infraestructura, y es exactamente el motivo por el que compensa un lenguaje de programación.
Las capas de aplicación e integración en Python
El CDK es multilenguaje de verdad: las bibliotecas se generan con jsii desde la misma fuente en TypeScript, así que la API es idéntica salvo por el estilo de nombres. Luis prefiere Python para la capa de aplicación:
from aws_cdk import Stack, Duration, aws_ec2 as ec2, aws_autoscaling as asg
from aws_cdk import aws_elasticloadbalancingv2 as elb, aws_ssm as ssm
from constructs import Construct
class AplicacionStack(Stack):
def __init__(self, scope: Construct, id: str, *, vpc: ec2.IVpc, entorno: str, **kwargs):
super().__init__(scope, id, **kwargs)
tamanos = {"desarrollo": ("t3.small", 1, 2),
"preproduccion": ("t3.medium", 2, 3),
"produccion": ("m6i.large", 2, 4)}
tipo, minimo, maximo = tamanos[entorno]
self.alb = elb.ApplicationLoadBalancer(
self, "Alb", vpc=vpc, internet_facing=True,
load_balancer_name=f"alb-mercadofresco-tienda-{entorno}")
grupo = asg.AutoScalingGroup(
self, "Tienda", vpc=vpc,
instance_type=ec2.InstanceType(tipo),
machine_image=ec2.MachineImage.from_ssm_parameter(
f"/mercadofresco/{entorno}/ami-tienda"),
min_capacity=minimo, max_capacity=maximo,
vpc_subnets=ec2.SubnetSelection(subnet_group_name="app"),
health_check=asg.HealthCheck.elb(grace=Duration.seconds(120)))
# Escalado por peticiones: el pico de los viernes son 900 pedidos/hora
grupo.scale_on_request_count("PorPeticiones", target_requests_per_minute=600)
oyente = self.alb.add_listener("Https", port=443, open=True)
oyente.add_targets("Tienda", port=8080, targets=[grupo],
health_check=elb.HealthCheck(path="/salud",
interval=Duration.seconds(15)))
ssm.StringParameter(self, "DnsAlb",
parameter_name=f"/mercadofresco/{entorno}/alb/dns",
string_value=self.alb.load_balancer_dns_name)Aquí ocurren cosas que en YAML costaban decenas de líneas. add_listener y add_targets crean el oyente, el grupo de destino, el registro del ASG y las reglas del grupo de seguridad necesarias para que el ALB llegue a las instancias: el SourceSecurityGroupId de 09-01 lo deduce el CDK del grafo de objetos. Y scale_on_request_count crea la política de escalado con su alarma de CloudWatch asociada.
La capa de integración es igual de compacta, y muestra el patrón de 07-05 con la cola de mensajes fallidos:
from aws_cdk import aws_sqs as sqs, aws_sns as sns, aws_sns_subscriptions as subs
fallidos = sqs.Queue(self, "PedidosFallidos",
queue_name=f"mercadofresco-pedidos-fallidos-{entorno}",
retention_period=Duration.days(14),
encryption=sqs.QueueEncryption.KMS, encryption_master_key=clave)
pedidos = sqs.Queue(self, "Pedidos",
queue_name=f"cola-mercadofresco-pedidos-{entorno}",
visibility_timeout=Duration.seconds(180),
encryption=sqs.QueueEncryption.KMS, encryption_master_key=clave,
dead_letter_queue=sqs.DeadLetterQueue(max_receive_count=5, queue=fallidos))
tema = sns.Topic(self, "PedidoConfirmado",
topic_name=f"mercadofresco-pedido-confirmado-{entorno}", master_key=clave)
tema.add_subscription(subs.SqsSubscription(pedidos, raw_message_delivery=True))add_subscription crea la suscripción y la política de la cola que permite a SNS escribir en ella, con la condición aws:SourceArn incluida. Es el bloque de veinte líneas de la solución del ejercicio de 09-01, reducido a una llamada y sin posibilidad de olvidarlo.
Activos: el código de las Lambdas dentro de la pila
Hay una cosa que CloudFormation no sabe hacer sola y que el CDK resuelve de forma casi invisible: subir el código. Una plantilla puede declarar una función Lambda, pero su código tiene que estar ya en un bucket, con una clave que alguien ha tenido que rellenar a mano o desde un script. En 09-01 eso quedaba fuera de la plantilla y lo hacía el pipeline.
Los activos (assets) del CDK cierran ese hueco: durante cdk deploy, la herramienta empaqueta el directorio indicado, calcula su huella, lo sube al bucket del bootstrap y sustituye la referencia en la plantilla.
from aws_cdk import aws_lambda as lambda_, aws_lambda_event_sources as fuentes
miniaturas = lambda_.Function(
self, "GenerarMiniaturas",
function_name=f"mercadofresco-generar-miniaturas-{entorno}",
runtime=lambda_.Runtime.PYTHON_3_12,
handler="index.handler",
code=lambda_.Code.from_asset("lambdas/miniaturas"), # el directorio se empaqueta y se sube
memory_size=1024,
timeout=Duration.seconds(30),
environment={"BUCKET_FOTOS": fotos.bucket_name})
fotos.grant_read_write(miniaturas) # politica de IAM + permisos sobre la clave KMS
reservar = lambda_.Function(self, "ReservarStock", ...)
reservar.add_event_source(fuentes.SqsEventSource(pedidos, batch_size=10))Tres consecuencias prácticas. La primera: la huella del contenido forma parte del nombre del activo, así que cambiar el código cambia el activo y cdk diff lo detecta; si no cambia, el despliegue no toca la función. La segunda: add_event_source crea el mapeo de origen de eventos y los permisos de consumo sobre la cola, incluida la clave de KMS. Y la tercera, que es una advertencia: los activos se acumulan en el bucket del bootstrap y nadie los borra, así que la regla de ciclo de vida del apartado de limpieza no es opcional a los seis meses.
Un matiz sobre el reparto de responsabilidades. Que el CDK pueda desplegar el código no significa que deba hacerlo en producción: MercadoFresco mantiene la separación de 08-05 —el artefacto de la tienda se construye una vez y lo despliega CodeDeploy— y usa activos solo para las Lambdas, cuyo código es pequeño y va acoplado a su infraestructura. Mezclar el ciclo de vida del código de aplicación con el de la infraestructura vuelve a juntar lo que el módulo 8 separó.
El ciclo de vida: synth, diff, deploy, destroy y bootstrap
cdk bootstrap aws://111122223333/eu-west-1 # una vez por cuenta y region
cdk ls # lista las pilas de la App
cdk synth MercadoFrescoRedProduccion # sintetiza y muestra la plantilla
cdk diff MercadoFrescoRedProduccion # compara con lo desplegado
cdk deploy MercadoFrescoRedProduccion # despliega (crea un conjunto de cambios)
cdk deploy --all --require-approval any-change
cdk destroy MercadoFrescoRedDesarrollo # borra la pila| Comando | Qué hace de verdad |
|---|---|
synth |
Ejecuta tu código y escribe el artefacto en cdk.out/. No toca AWS |
diff |
Sintetiza y compara con la pila desplegada; equivale a un conjunto de cambios legible |
deploy |
Sube el artefacto al bucket del bootstrap y ejecuta el despliegue en CloudFormation |
destroy |
delete-stack, con las mismas reglas de DeletionPolicy de 09-01 |
watch |
Redespliega en caliente al guardar; solo para desarrollo, nunca en producción |
cdk bootstrap merece una explicación, porque es la parte que más desconcierta. Crea una pila llamada CDKToolkit con: un bucket de S3 para los artefactos (plantillas grandes, código de Lambda, activos), un repositorio de ECR para imágenes de contenedor, y cinco roles de IAM —de despliegue, de publicación de activos, de búsqueda, de ejecución de CloudFormation y de imágenes—. Sin ella, cdk deploy falla con un mensaje explícito.
Hay tres cosas que saber sobre el bootstrap: es por cuenta y por región, así que desplegar en us-east-1 para un certificado de CloudFront exige bootstrapearla también; la versión importa, y una versión antigua provoca fallos crípticos que se resuelven volviendo a ejecutarlo; y los roles que crea son potentes por omisión, con AdministratorAccess en el rol de ejecución de CloudFormation, lo que en una cuenta compartida es una decisión que hay que tomar conscientemente —--cloudformation-execution-policies permite acotarlo—.
cdk diff es el equivalente cotidiano del conjunto de cambios de 09-01, y su salida marca con [-], [+] y [~] lo que se borra, se añade y se modifica, señalando además los cambios que provocan reemplazo. La regla de MercadoFresco es la misma que allí: ningún despliegue sin haber leído el diff.
Contexto, entornos explícitos y una pila por entorno
Aquí se ataca de frente el problema que aparece en cada incidente: que preproducción no es igual que producción. La solución del CDK es hacer que las tres pilas salgan del mismo código, con un fichero de configuración por entorno como única diferencia.
// config/entornos.ts — la unica fuente de diferencias entre entornos
export interface ConfigEntorno {
readonly nombre: 'desarrollo' | 'preproduccion' | 'produccion';
readonly cuenta: string;
readonly region: string;
readonly cidr: string;
readonly retencionRegistrosDias: number;
readonly protegerRecursos: boolean;
}
export const ENTORNOS: Record<string, ConfigEntorno> = {
desarrollo: { nombre: 'desarrollo', cuenta: '111122223333', region: 'eu-west-1',
cidr: '10.2.0.0/16', retencionRegistrosDias: 7, protegerRecursos: false },
preproduccion: { nombre: 'preproduccion', cuenta: '111122223333', region: 'eu-west-1',
cidr: '10.1.0.0/16', retencionRegistrosDias: 30, protegerRecursos: true },
produccion: { nombre: 'produccion', cuenta: '111122223333', region: 'eu-west-1',
cidr: '10.0.0.0/16', retencionRegistrosDias: 365, protegerRecursos: true },
};// bin/infra-cdk.ts — el punto de entrada instancia las pilas de los tres entornos
const app = new cdk.App();
for (const config of Object.values(ENTORNOS)) {
const sufijo = config.nombre.charAt(0).toUpperCase() + config.nombre.slice(1);
const env = { account: config.cuenta, region: config.region }; // ENTORNO EXPLICITO
const red = new RedStack(app, `MercadoFrescoRed${sufijo}`, { env, config });
new AplicacionStack(app, `MercadoFrescoAplicacion${sufijo}`, { env, config, vpc: red.vpc });
new IntegracionStack(app, `MercadoFrescoIntegracion${sufijo}`, { env, config });
}Quedan nueve pilas con nombres predecibles: MercadoFrescoRedProduccion, MercadoFrescoAplicacionPreproduccion, y así. Y la diferencia entre entornos ya no es un misterio arqueológico: es un fichero de veinte líneas que se lee en diez segundos. La primera vez que se generó, Marta descubrió tres divergencias que llevaban meses ahí: preproducción tenía un solo NAT declarado como si fuera producción, la retención de registros era de 7 días en los tres entornos y el CIDR de desarrollo se solapaba con el de preproducción.
flowchart TB
App[App CDK<br/>bin/infra-cdk.ts] --> C[config/entornos.ts]
App --> D[Desarrollo]
App --> P[Preproduccion]
App --> R[Produccion]
D --> D1[MercadoFrescoRedDesarrollo]
D --> D2[MercadoFrescoIntegracionDesarrollo]
D --> D3[MercadoFrescoAplicacionDesarrollo]
R --> R1[MercadoFrescoRedProduccion]
R --> R2[MercadoFrescoIntegracionProduccion]
R --> R3[MercadoFrescoAplicacionProduccion]
R1 -->|vpc| R3
Dos conceptos importantes de este bloque:
Entornos explícitos. Si omites env, la pila es agnóstica de entorno y no puede usar información real de la cuenta: Vpc.fromLookup, el número de AZ o los certificados existentes. El CDK entonces genera plantillas con dos AZ ficticias y sorpresas al desplegar. Declara siempre env con cuenta y región literales.
Contexto y cdk.context.json. Las búsquedas (fromLookup) consultan la cuenta durante la síntesis y cachean el resultado en cdk.context.json, que sí se versiona: garantiza que la síntesis sea reproducible y que el pipeline no dependa de consultas en vivo. Cuando la realidad cambia, se refresca con cdk context --clear. La regla práctica: preferir referencias explícitas a búsquedas, porque una búsqueda es una dependencia oculta sobre el estado de la cuenta.
El paso de valores entre pilas es más natural que en 09-01: pasar red.vpc a AplicacionStack hace que el CDK cree automáticamente la exportación y la importación de CloudFormation. Es cómodo y tiene la misma trampa que allí —una exportación en uso es un cerrojo— con un agravante: el CDK la crea sin que la veas. Si dos pilas van a evolucionar por separado, sigue siendo mejor publicar en Parameter Store y leer con StringParameter.valueForStringParameter.
Aspectos: etiquetado obligatorio y reglas de auditoría
Un aspecto es un visitante que recorre todo el árbol de constructos y actúa sobre cada nodo. Sirve para dos cosas que en YAML eran imposibles: aplicar algo a todos los recursos de golpe y auditar reglas sobre lo que se va a desplegar.
El etiquetado obligatorio de MercadoFresco es trivial con Tags, que internamente es un aspecto:
cdk.Tags.of(app).add('Proyecto', 'mercadofresco');
cdk.Tags.of(red).add('Entorno', config.nombre);
cdk.Tags.of(red).add('Componente', 'red');
cdk.Tags.of(red).add('Propietario', 'marta');
cdk.Tags.of(red).add('CentroCoste', 'plataforma');Y una regla de auditoría propia es una clase de veinte líneas:
import { IAspect, Annotations } from 'aws-cdk-lib';
import { IConstruct } from 'constructs';
/** Prohibe buckets sin cifrar y grupos de seguridad con el puerto 22 abierto al mundo. */
export class ReglasMercadoFresco implements IAspect {
public visit(nodo: IConstruct): void {
if (nodo instanceof s3.CfnBucket && !nodo.bucketEncryption) {
// addError hace que 'cdk synth' FALLE. addWarning solo avisa.
Annotations.of(nodo).addError('Todo bucket debe declarar cifrado (04-02).');
}
if (nodo instanceof ec2.CfnSecurityGroupIngress &&
nodo.fromPort === 22 && nodo.cidrIp === '0.0.0.0/0') {
Annotations.of(nodo).addError('Prohibido abrir el puerto 22 al mundo.');
}
}
}
cdk.Aspects.of(app).add(new ReglasMercadoFresco());Tres detalles que marcan la diferencia entre que esto funcione y que dé falsos negativos. Los aspectos visitan constructos L1, así que hay que comprobar CfnBucket y no Bucket: los L2 crean L1 por debajo y es ahí donde están las propiedades reales. addError detiene la síntesis, lo que convierte la regla en una puerta de calidad de verdad y no en un aviso que nadie lee. Y los aspectos se ejecutan después de que el árbol esté construido pero antes de sintetizar, por lo que también pueden modificar recursos —añadir cifrado en lugar de fallar—, aunque MercadoFresco prefiere fallar: una infraestructura que se corrige sola esconde el error en lugar de arreglarlo.
Esto es lo que en 09-01 hacía cfn-guard con un lenguaje aparte. Aquí es el mismo lenguaje, con el mismo editor, el mismo depurador y las mismas pruebas.
Pruebas de infraestructura
La pirámide de pruebas de 08-05 tenía un hueco evidente: la infraestructura no se probaba. Con el CDK sí, porque una plantilla sintetizada es un objeto JSON contra el que se pueden hacer aserciones.
import { App } from 'aws-cdk-lib';
import { Template, Match } from 'aws-cdk-lib/assertions';
describe('RedStack de produccion', () => {
const app = new App();
const pila = new RedStack(app, 'Prueba', { env: { account: '111122223333',
region: 'eu-west-1' },
config: ENTORNOS.produccion });
const plantilla = Template.fromStack(pila);
test('crea seis subredes', () => {
plantilla.resourceCountIs('AWS::EC2::Subnet', 6);
});
test('produccion tiene dos NAT, uno por AZ', () => {
plantilla.resourceCountIs('AWS::EC2::NatGateway', 2);
});
test('las subredes de datos no tienen salida a internet', () => {
// Ninguna ruta hacia un NAT puede colgar de la tabla de rutas de datos
plantilla.hasResourceProperties('AWS::EC2::Subnet', Match.objectLike({
Tags: Match.arrayWith([Match.objectLike({ Value: Match.stringLikeRegexp('.*datos.*') })]),
}));
});
test('ningun grupo de seguridad abre el 22 al mundo', () => {
const sgs = plantilla.findResources('AWS::EC2::SecurityGroup');
for (const sg of Object.values(sgs)) {
const reglas = sg.Properties?.SecurityGroupIngress ?? [];
expect(reglas.filter((r: any) => r.FromPort === 22 && r.CidrIp === '0.0.0.0/0')).toHaveLength(0);
}
});
test('la plantilla no cambia sin querer', () => {
expect(plantilla.toJSON()).toMatchSnapshot(); // prueba instantanea
});
});Hay dos familias de pruebas y sirven para cosas distintas:
| Tipo | Qué comprueba | Ventaja | Riesgo |
|---|---|---|---|
Aserciones (hasResourceProperties, resourceCountIs) |
Invariantes concretos que te importan | Expresan intención; sobreviven a refactorizaciones | Solo detectan lo que has escrito |
Instantáneas (toMatchSnapshot) |
Que la plantilla no cambie sin querer | Detectan cualquier cambio, incluidos los de la biblioteca | Se actualizan con -u sin mirar, y dejan de servir |
MercadoFresco usa las dos con una regla estricta: las aserciones cubren lo que no puede fallar nunca —seis subredes, dos NAT en producción, sin SSH abierto, cifrado en todos los buckets— y las instantáneas actúan de red de seguridad. Y una norma de revisión: una instantánea actualizada en una PR obliga a explicar el diff en la descripción. Sin esa norma, jest -u se convierte en un reflejo y la prueba deja de valer nada.
La ganancia real se ve al actualizar aws-cdk-lib: la instantánea muestra exactamente qué ha cambiado en las plantillas generadas antes de desplegar nada. En una actualización menor de la biblioteca, MercadoFresco descubrió así un cambio de comportamiento por omisión en el cifrado de una cola que nadie habría notado.
Recursos con estado, RemovalPolicy y el peligro de cdk destroy
RemovalPolicy del CDK es la DeletionPolicy de 09-01, con una diferencia peligrosa: los constructos L2 eligen un valor por omisión, y no siempre el que esperas.
tabla = dynamodb.Table(self, "Carritos",
table_name=f"mercadofresco-carritos-{entorno}",
partition_key=dynamodb.Attribute(name="idCarrito", type=dynamodb.AttributeType.STRING),
billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
removal_policy=RemovalPolicy.RETAIN if config.proteger else RemovalPolicy.DESTROY,
point_in_time_recovery=True)
cluster = rds.DatabaseCluster(self, "Pedidos",
engine=rds.DatabaseClusterEngine.aurora_postgres(
version=rds.AuroraPostgresEngineVersion.VER_15_4),
removal_policy=RemovalPolicy.SNAPSHOT, # instantanea final antes de borrar
deletion_protection=True, # y ademas, proteccion de borrado en RDS
storage_encrypted=True)| Recurso | RemovalPolicy por omisión en el L2 |
Qué significa |
|---|---|---|
s3.Bucket |
RETAIN |
El bucket sobrevive a cdk destroy |
dynamodb.Table |
RETAIN |
La tabla sobrevive |
rds.DatabaseCluster |
SNAPSHOT |
Instantánea final y borrado |
logs.LogGroup |
RETAIN |
Los registros sobreviven |
sqs.Queue, sns.Topic, ec2.Vpc |
DESTROY |
Se borran sin más |
Y aquí está el peligro real, que en MercadoFresco es concreto porque los tres entornos comparten la cuenta 111122223333: cdk destroy --all no pregunta a qué entorno pertenece cada pila. Un --all lanzado desde el directorio equivocado, o un cdk destroy MercadoFrescoRedProduccion con un autocompletado desafortunado, toca producción. Tres medidas, en orden de eficacia:
deletion_protectionytermination_protectionen las pilas de producción, que hacen que el borrado falle en el servicio, no en la buena voluntad de quien escribe el comando.cdk.StackaceptaterminationProtection: true.- Nunca
--allen un terminal humano. Los despliegues de producción salen del pipeline; en local se nombra la pila entera. - Cuentas separadas, que es la solución de verdad y llega en 09-04. Mientras tanto, las dos anteriores son parches conscientes.
Un último matiz: RemovalPolicy.RETAIN deja el recurso huérfano, y el siguiente cdk deploy intentará crear otro con el mismo nombre físico y fallará. Es la contrapartida de la seguridad, y la razón por la que los nombres físicos fijos —table_name, bucket_name— tienen coste, igual que en 09-01.
CDK Pipelines: el pipeline que se despliega a sí mismo
Queda la última pieza de la asimetría del módulo 8: el pipeline se creó a mano. pipelines.CodePipeline resuelve eso con una propiedad llamada autoactualización: el pipeline se define en el mismo repositorio que la infraestructura y, en cada ejecución, su primera etapa se actualiza a sí mismo antes de desplegar nada.
const pipeline = new pipelines.CodePipeline(this, 'Pipeline', {
pipelineName: 'pipeline-mercadofresco-infra',
synth: new pipelines.ShellStep('Synth', {
input: pipelines.CodePipelineSource.connection('mercadofresco/mercadofresco-infra', 'main', {
connectionArn: 'arn:aws:codeconnections:eu-west-1:111122223333:connection/conn-mercadofresco-github',
}),
commands: ['npm ci', 'npm run build', 'npm test', 'npx cdk synth'],
}),
});
// Una etapa por entorno. La aprobacion manual solo antes de produccion.
pipeline.addStage(new EtapaMercadoFresco(this, 'Desarrollo', { config: ENTORNOS.desarrollo }));
pipeline.addStage(new EtapaMercadoFresco(this, 'Preproduccion', { config: ENTORNOS.preproduccion }));
pipeline.addStage(new EtapaMercadoFresco(this, 'Produccion', { config: ENTORNOS.produccion }), {
pre: [new pipelines.ManualApprovalStep('AprobacionMarta')],
post: [new pipelines.ShellStep('Humo', { commands: ['./pruebas/humo.sh produccion'] })],
});Un Stage es un grupo de pilas que se despliegan juntas —red, datos, integración, aplicación y observabilidad de un entorno— y el CDK deduce el orden de despliegue del grafo de dependencias entre ellas. npm test ejecuta las pruebas de la sección anterior, así que una plantilla que viole una regla no llega a desplegarse.
Con esto, MercadoFresco cierra el círculo del módulo 8: el pipeline-mercadofresco-tienda despliega la aplicación y el pipeline-mercadofresco-infra despliega la infraestructura y a sí mismo. Añadir una etapa nueva es una PR, no un clic en la consola.
Dos advertencias. La primera: el pipeline autoactualizable es potente y peligroso —quien pueda fusionar en main puede cambiar el propio pipeline—, así que la protección de rama de 08-01 y la revisión obligatoria dejan de ser buenas prácticas y pasan a ser un control de seguridad. La segunda: la etapa de síntesis necesita los permisos de búsqueda, así que cdk.context.json versionado no es opcional si quieres síntesis reproducibles.
Terraform, CDK for Terraform y la comparación honesta
Terraform, de HashiCorp, es la alternativa multiproveedor más extendida: lenguaje declarativo propio (HCL), estado en un fichero que hay que almacenar y bloquear, y proveedores para AWS, Azure, GCP, Datadog o GitHub. CDK for Terraform (CDKTF) permite escribir configuración de Terraform con TypeScript o Python, igual que el CDK con CloudFormation.
| Criterio | CloudFormation | AWS CDK | Terraform |
|---|---|---|---|
| Lenguaje | YAML/JSON | TypeScript, Python, Java, C#, Go | HCL (o TS/Python con CDKTF) |
| Estado | Gestionado por AWS | Gestionado por AWS | Fichero propio: hay que alojarlo y bloquearlo |
| Multiproveedor | No | No | Sí, es su razón de ser |
| Vista previa | Conjunto de cambios | cdk diff |
terraform plan, más detallado |
| Recursos nuevos de AWS | Con retraso ocasional | El del L1, más el L2 después | Rápido, a veces antes |
| Reversión automática | Sí, la pila revierte sola | Sí, es CloudFormation | No: se queda a medias y se corrige |
| Deriva | Detección nativa | La de CloudFormation | plan la muestra en cada ejecución |
| Coste operativo | Ninguno | Ninguno | Gestionar el estado (o pagar HCP Terraform) |
| Comunidad de módulos | Escasa | Creciente (Construct Hub) | Enorme: el registro de módulos |
La recomendación es sencilla y no depende de gustos. Si todo está en AWS, CloudFormation o CDK, porque el estado gestionado y la reversión automática son ventajas reales que se pagan caras en Terraform. Si hay varios proveedores —AWS más Cloudflare, más GitHub, más Datadog— Terraform gana, y el coste de mantener el estado se justifica. MercadoFresco está entero en AWS y ya tiene el pipeline montado sobre servicios nativos: cambiar sería trabajo sin retorno.
Coste y limpieza
El CDK es gratuito; el bootstrap deja un bucket, un repositorio de ECR y cinco roles cuyo coste es despreciable mientras estén vacíos. Conviene, eso sí, poner una regla de ciclo de vida en el bucket de activos, que crece con cada despliegue y no se limpia solo.
cdk destroy MercadoFrescoAplicacionDesarrollo MercadoFrescoRedDesarrollo # en este orden
cdk ls # comprobar que ya no estan
aws ec2 describe-addresses --query 'Addresses[?AssociationId==null]' --output tableEl orden importa por la misma razón que en 09-01: primero los consumidores, después la red. Si una pila no se deja borrar, la causa suele ser una exportación en uso o un bucket con objetos, y se diagnostica en los eventos de CloudFormation, no en el CDK.
Errores Comunes y Consejos
Error: cambiar el id de un constructo desplegado. Renombrar 'Vpc' por 'VpcPrincipal' recrea el recurso, igual que en 09-01. Consejo: los id son inmutables en la práctica; si de verdad hay que reorganizar, cdk diff avisa —léelo—, y overrideLogicalId permite conservar el identificador anterior.
Error: desplegar sin cdk diff. El CDK hace tan fácil desplegar que la gente se salta la vista previa. Consejo: cdk diff en cada PR, publicado como comentario, y --require-approval any-change en producción.
Error: pilas sin env explícito. Producen plantillas con dos AZ ficticias y fromLookup no funciona. Consejo: declara env con cuenta y región literales en todas las pilas.
Error: cdk destroy --all en la cuenta compartida. Es el riesgo concreto de MercadoFresco hasta 09-04. Consejo: terminationProtection: true en las pilas de producción, nombrar siempre la pila, y nunca --all fuera de un entorno de pruebas.
Error: actualizar instantáneas con -u sin mirar. Convierte la prueba en decoración. Consejo: exigir en la revisión que toda instantánea actualizada venga con la explicación del diff.
Error: creer que el L2 hace lo que tú harías. ec2.Vpc toma docenas de decisiones por ti. Consejo: cdk synth y leer la plantilla la primera vez que uses un constructo nuevo; es la única forma de saber qué has pedido de verdad.
Consejo: fija la versión de aws-cdk-lib y actualízala a propósito. Con las pruebas instantáneas, cada actualización te enseña exactamente qué cambia en las plantillas antes de tocar nada.
Consejo: usa los métodos grant* en lugar de escribir políticas. Generan el mínimo privilegio real, incluidos los permisos sobre KMS que casi todo el mundo olvida.
Consejo: no metas lógica de negocio en el código de infraestructura. El CDK invita a hacerlo por ser un lenguaje completo. Una pila que lee una base de datos durante la síntesis es una pila que no se puede sintetizar en el pipeline.
Ejercicios
Ejercicio 1: el constructo de cola con DLQ
Escribe un constructo propio ColaMercadoFresco (TypeScript o Python) que encapsule el patrón de 07-05: una cola principal con su cola de mensajes fallidos, cifrado con alias/mercadofresco-datos, maxReceiveCount configurable con valor por omisión de 5, retención de 14 días en la DLQ, y una alarma de CloudWatch que salte cuando la DLQ tenga mensajes. Debe exponer la cola principal, la DLQ y un método concederConsumo(rol). Úsalo para crear las cuatro colas de MercadoFresco. Indica qué RemovalPolicy pones en cada cola y por qué.
Ejercicio 2: las pruebas que faltan
Escribe cuatro pruebas para RedStack y AplicacionStack que verifiquen invariantes que a MercadoFresco le importan de verdad: (a) que en producción hay dos NAT y en desarrollo uno; (b) que ninguna subred de datos tiene ruta hacia un NAT; (c) que el ALB solo acepta HTTPS; (d) que todos los recursos etiquetables llevan las cinco etiquetas obligatorias. Para cada una, di si la resolverías con aserción o con instantánea y por qué, y qué haría falta además de la prueba para que la regla se cumpla siempre.
Ejercicio 3: migrar sin recrear
MercadoFresco tiene ya la pila mercadofresco-red-produccion desplegada con la plantilla YAML de 09-01, con la VPC y las seis subredes en producción. Marta quiere pasar esa pila a CDK sin recrear nada y sin cortar el servicio. Describe la estrategia completa: qué opciones hay, cuál eliges y por qué, qué comandos ejecutas, cómo compruebas que la migración no va a cambiar nada, y qué haces si cdk diff muestra diferencias. Explica también qué riesgo concreto tiene la opción que descartas.
Soluciones
Solución 1
export interface ColaMercadoFrescoProps {
readonly nombre: string;
readonly entorno: string;
readonly clave: kms.IKey;
readonly maxIntentos?: number;
readonly tiempoVisibilidad?: cdk.Duration;
readonly temaAlertas: sns.ITopic;
}
export class ColaMercadoFresco extends Construct {
public readonly cola: sqs.Queue;
public readonly fallidos: sqs.Queue;
constructor(scope: Construct, id: string, props: ColaMercadoFrescoProps) {
super(scope, id);
this.fallidos = new sqs.Queue(this, 'Fallidos', {
queueName: `mercadofresco-${props.nombre}-fallidos-${props.entorno}`,
retentionPeriod: cdk.Duration.days(14),
encryption: sqs.QueueEncryption.KMS,
encryptionMasterKey: props.clave,
removalPolicy: cdk.RemovalPolicy.RETAIN, // guarda mensajes sin procesar: nunca se borra
});
this.cola = new sqs.Queue(this, 'Principal', {
queueName: `cola-mercadofresco-${props.nombre}-${props.entorno}`,
visibilityTimeout: props.tiempoVisibilidad ?? cdk.Duration.seconds(180),
encryption: sqs.QueueEncryption.KMS,
encryptionMasterKey: props.clave,
deadLetterQueue: { queue: this.fallidos, maxReceiveCount: props.maxIntentos ?? 5 },
removalPolicy: props.entorno === 'produccion'
? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY,
});
// Cualquier mensaje en la DLQ es un incidente: umbral 0, una evaluacion.
this.fallidos.metricApproximateNumberOfMessagesVisible()
.createAlarm(this, 'AlarmaDlq', {
alarmName: `mercadofresco-${props.nombre}-fallidos`,
threshold: 0, evaluationPeriods: 1,
comparisonOperator: cw.ComparisonOperator.GREATER_THAN_THRESHOLD,
treatMissingData: cw.TreatMissingData.NOT_BREACHING,
})
.addAlarmAction(new actions.SnsAction(props.temaAlertas));
}
public concederConsumo(rol: iam.IGrantable): void {
this.cola.grantConsumeMessages(rol); // incluye los permisos sobre la clave KMS
}
}for (const nombre of ['pedidos', 'correo', 'almacen', 'analitica']) {
new ColaMercadoFresco(this, `Cola${nombre}`, { nombre, entorno: config.nombre,
clave, temaAlertas: alertas });
}Las políticas de borrado. La DLQ siempre RETAIN, en todos los entornos: contiene mensajes que no se procesaron y que alguien tendrá que examinar; borrarla al destruir una pila destruye la evidencia de un incidente. La cola principal, RETAIN en producción y DESTROY en el resto: en producción puede haber mensajes en vuelo cuya pérdida sería pérdida de pedidos, mientras que en desarrollo un residuo que impide recrear la pila cuesta más de lo que protege.
Y un detalle que se nota al usarlo cuatro veces: el bucle de cuatro líneas sustituye a ciento veinte líneas de YAML con doce recursos, y garantiza que las cuatro colas tengan exactamente la misma configuración —cifrado, reintentos, alarma—, que es justo lo que hoy no se cumple en MercadoFresco.
Solución 2
test('(a) el numero de NAT depende del entorno', () => {
const prod = Template.fromStack(new RedStack(new App(), 'P', { env, config: ENTORNOS.produccion }));
const dev = Template.fromStack(new RedStack(new App(), 'D', { env, config: ENTORNOS.desarrollo }));
prod.resourceCountIs('AWS::EC2::NatGateway', 2);
dev.resourceCountIs('AWS::EC2::NatGateway', 1);
});
test('(c) el ALB solo acepta HTTPS', () => {
plantillaApp.hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener',
Match.objectLike({ Port: 443, Protocol: 'HTTPS' }));
const oyentes = plantillaApp.findResources('AWS::ElasticLoadBalancingV2::Listener');
expect(Object.values(oyentes).filter((o: any) => o.Properties.Port === 80)).toHaveLength(0);
});(a) Aserción. Es un invariante de negocio con dos valores concretos, y la prueba lo expresa mejor que cualquier instantánea. Además sobrevive a refactorizaciones: si mañana se reorganizan los constructos, la prueba sigue valiendo.
(b) Aserción, pero indirecta. Comprobar «ninguna subred de datos tiene ruta a un NAT» requiere seguir la relación entre RouteTable, Route y SubnetRouteTableAssociation en la plantilla, que es tedioso. Lo pragmático es afirmar que no existe ninguna AWS::EC2::Route con NatGatewayId cuya tabla esté asociada a una subred datos, y apoyarse en que el CDK garantiza la propiedad por construcción al usar PRIVATE_ISOLATED. La prueba vale como red de seguridad ante un cambio de tipo de subred.
(c) Aserción, con la parte negativa incluida. Comprobar que existe un oyente 443 no basta: lo que importa es que no exista uno en el 80 sin redirección. Las pruebas que solo verifican presencia dejan pasar precisamente los errores por adición, que son los que ocurren.
(d) Ni aserción ni instantánea: un aspecto. Una prueba que recorra todos los recursos comprobando etiquetas es frágil, porque muchos tipos no las admiten y habría que mantener la lista de excepciones. Lo correcto es un aspecto que falle la síntesis si falta una etiqueta obligatoria, más una prueba de que el aspecto está registrado en la App. Se prueba el mecanismo, no cada instancia.
Qué hace falta además de las pruebas. Nada de esto sirve si las pruebas no bloquean: tienen que ejecutarse en npm test dentro de la etapa de síntesis de CDK Pipelines, y la construcción debe fallar, no avisar. Es la misma conclusión de 08-02, aplicada a la infraestructura: una comprobación que no bloquea es documentación.
Solución 3
Hay dos opciones y no son equivalentes.
La primera es recrear la red en CDK y migrar: desplegar una VPC nueva junto a la vieja, mover las instancias, la base de datos y el ALB, y borrar la antigua. Es limpia conceptualmente y es la que se descarta, porque implica mover Aurora y el ALB entre VPC: cambio de endpoints, ventana de corte, riesgo sobre los datos de clientes y, sobre todo, un beneficio nulo —la red resultante es idéntica—. Todo el riesgo, ninguna ganancia.
La segunda, la elegida, es hacer que el CDK adopte la pila existente. La clave es que una pila de CloudFormation no sabe si su plantilla la escribió una persona o el CDK: solo compara identificadores lógicos. Si la plantilla sintetizada es equivalente, la actualización no toca ningún recurso.
El procedimiento. Se escribe la pila CDK con el mismo nombre de pila, mercadofresco-red-produccion, y se fuerzan los identificadores lógicos para que coincidan con los de la plantilla YAML, usando overrideLogicalId recurso a recurso:
const cfnVpc = this.vpc.node.defaultChild as ec2.CfnVPC;
cfnVpc.overrideLogicalId('Vpc'); // el nombre logico exacto de red-mercadofresco.yamlLa comprobación, que es el corazón del ejercicio:
cdk synth mercadofresco-red-produccion > /tmp/cdk.yaml
aws cloudformation get-template --stack-name mercadofresco-red-produccion \
--template-stage Processed --query TemplateBody > /tmp/actual.json
cdk diff mercadofresco-red-produccion # el veredicto: tiene que salir vaciocdk diff vacío es la condición de aceptación. Mientras muestre algo, no se despliega.
Si muestra diferencias, hay tres casos. Si son solo metadatos del CDK (CDKMetadata, la versión del analizador) se aceptan: no afectan a ningún recurso. Si son propiedades cosméticas —una descripción, una etiqueta— se decide caso a caso y se documenta. Y si hay algún [~] con reemplazo o cualquier [-], se para: significa que la plantilla generada no es equivalente y hay que ajustar el código hasta que lo sea. Un [-] sobre una subred en producción es exactamente el desastre que esta migración existe para evitar.
La red de seguridad, en dos pasos. Antes de nada, --enable-termination-protection en la pila y DeletionPolicy: Retain en los recursos críticos, para que ni siquiera un error grave destruya nada. Y el ensayo completo primero en desarrollo, luego en preproducción y solo entonces en producción, que es el mismo orden de promoción de 08-04 aplicado a la infraestructura.
Conclusión
La misma infraestructura, en un lenguaje de programación. Las 190 líneas de YAML de la red se han convertido en 28 líneas de TypeScript que sintetizan una plantilla equivalente, y las seis subredes casi idénticas han desaparecido dentro de un subnetConfiguration de tres entradas que el CDK multiplica por AZ. Lo que no ha cambiado es lo que hay debajo: el CDK no sustituye a CloudFormation, escribe las plantillas por ti, y los conjuntos de cambios, la deriva, ROLLBACK_COMPLETE y las políticas de borrado de 09-01 siguen gobernando lo que ocurre de verdad.
Tienes los tres niveles de constructos —el L1 que calca CloudFormation, el L2 con valores por defecto seguros y métodos grant* que generan el mínimo privilegio, y el L3 cómodo y opaco— con la válvula de escape de node.defaultChild para bajar de nivel cuando la API se queda corta. Tienes el constructo propio MercadoFrescoVpc, que es la diferencia entre una tabla de tres columnas y una abstracción de verdad: una mejora del patrón llega a los tres entornos a la vez. Y tienes el ciclo de vida completo, con cdk bootstrap explicado —el bucket, el repositorio de ECR y los cinco roles, por cuenta y región, con permisos de administración que hay que acotar conscientemente— y con la regla que se hereda de 09-01: ningún despliegue sin haber leído el diff.
Los tres entornos salen ahora del mismo código, y la diferencia entre preproducción y producción ha dejado de ser arqueología: es config/entornos.ts, veinte líneas legibles en diez segundos, que al escribirse por primera vez destapó tres divergencias que llevaban meses ahí. Con entornos explícitos en todas las pilas —sin env no hay búsquedas y sí AZ ficticias— y cdk.context.json versionado para que la síntesis sea reproducible en el pipeline.
Y la infraestructura entra por fin en la pirámide de pruebas de 08-05. Los aspectos aplican el etiquetado obligatorio de golpe y auditan reglas —buckets sin cifrar, el puerto 22 abierto— con addError, que detiene la síntesis en lugar de avisar; visitando siempre constructos L1, que es donde están las propiedades reales. Las aserciones cubren lo que no puede fallar nunca y las instantáneas actúan de red de seguridad, con la norma de revisión que las mantiene vivas: una instantánea actualizada obliga a explicar el diff. Más CDK Pipelines, que cierra la asimetría del módulo 8 al desplegar la infraestructura y actualizarse a sí mismo —y que convierte la protección de rama de 08-01 en un control de seguridad, porque quien fusiona en main cambia el pipeline—.
Queda un riesgo señalado tres veces y no resuelto: cdk destroy apuntando a producción. La protección de terminación y la disciplina de nombrar siempre la pila son parches conscientes, no soluciones, porque el problema de fondo es que los tres entornos comparten la cuenta 111122223333. Mientras eso siga siendo cierto, un comando mal escrito, una variable de entorno olvidada o un --all en el directorio equivocado pueden alcanzar la tienda en producción. Y el radio de explosión no es lo único: las cuotas se comparten, un despliegue de desarrollo puede agotar el límite de direcciones IP elásticas de producción, la factura no se separa de verdad y ninguna política de IAM aísla del todo a alguien que ya tiene permisos amplios en la cuenta.
En 09-03, «AWS Elastic Beanstalk», damos un paso atrás para mirar la alternativa gestionada: qué hace por ti una plataforma como servicio, cuándo habría sido la decisión correcta para MercadoFresco —y por qué hoy ya no lo es—. Después, 09-04, «AWS Organizations», ataca el problema de fondo: separar de verdad los entornos en cuentas distintas, con políticas de control de servicios, facturación consolidada y una línea base que se despliega sola en cada cuenta nueva.
Curso de AWS
Módulo 1: Introducción a AWS
- ¿Qué es AWS?
- Configuración de tu cuenta de AWS
- Infraestructura global de AWS
- Consola de administración de AWS
- AWS CLI y SDKs
Módulo 2: Servicios principales de AWS
Módulo 3: Redes y entrega de contenido
- Amazon VPC
- Grupos de seguridad y listas de control de acceso
- Elastic Load Balancing
- Amazon CloudFront
- Route 53
Módulo 4: Seguridad e identidad
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager y Parameter Store
- AWS Shield
- AWS WAF
Módulo 5: Monitorización y gestión
- Amazon CloudWatch
- AWS X-Ray y trazabilidad distribuida
- AWS CloudTrail
- AWS Config
- AWS Trusted Advisor
Módulo 6: Bases de datos
- Cómo elegir la base de datos adecuada
- Amazon DynamoDB
- Amazon Aurora
- Amazon Redshift
- Amazon ElastiCache
Módulo 7: Integración de aplicaciones
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Patrones de integración: idempotencia, reintentos y colas de mensajes fallidos
