Ocho módulos han construido una arquitectura completa: seis subredes, dos grupos de autoescalado, cinco funciones Lambda, cuatro colas, un clúster Aurora, dos tablas de DynamoDB, cinco buckets, un balanceador, una distribución de CloudFront, catorce alarmas y un pipeline que despliega sin cortar el servicio. Todo funciona. Y nada de eso existe en ningún fichero: existe porque alguien escribió un comando algún día. Esta lección arregla la asimetría que Marta señaló al cerrar el módulo 8 con la herramienta nativa de AWS para hacerlo: CloudFormation, el servicio que convierte un fichero de texto en recursos reales y mantiene la relación entre ambos.
Aviso de coste. CloudFormation es gratuito cuando gestiona recursos de AWS: pagas los recursos, no las pilas. Solo cobra por recursos de terceros y tipos privados registrados (0,0009 USD por segundo de manejador). La plantilla de red crea dos NAT gateway: unos 0,045 USD/hora cada uno más tráfico, unos 65 USD al mes si la dejas puesta. El apartado de limpieza la borra con un comando. Datos ficticios.
Contenido
- La asimetría entre código e infraestructura
- Qué es la infraestructura como código, y declarativo frente a imperativo
- Anatomía de una plantilla
- Parámetros: tipos, restricciones y valores desde SSM
- Mappings, Conditions y funciones intrínsecas
- Pilas: ciclo de vida, estados y
ROLLBACK_COMPLETE - Políticas de eliminación y protección de terminación
- Conjuntos de cambios y los tres tipos de actualización
- Detección de deriva
- La red de MercadoFresco en una plantilla
- La capa de aplicación que importa las salidas
- Varias pilas: exportaciones frente a SSM
- Pilas anidadas, módulos y recursos personalizados
- Importar recursos existentes a una pila
- StackSets y despliegue desde el pipeline
- Buenas prácticas,
cfn-lintycfn-guard - Límites, coste y limpieza
- Errores comunes y consejos
- Ejercicios
- Conclusión
La asimetría entre código e infraestructura
Marta hace inventario en una tarde y apunta cuatro hechos:
| Hecho | Evidencia | Consecuencia |
|---|---|---|
| Nadie puede recrear el entorno | No hay documento ni script que lo describa | Una región nueva es un proyecto de semanas |
| Preproducción no es igual que producción | Su SG permite 0.0.0.0/0 en el 22 |
Los incidentes no se reproducen a tiempo |
| No hay registro de por qué | La regla de EventBridge no tiene autor ni motivo | Nadie se atreve a borrar nada |
| El pipeline se creó a mano | Un JSON en el portátil de Luis | Lo que gobierna el código no está gobernado |
El tercero es el más caro. En el módulo 5 apareció un grupo de seguridad con una regla hacia un rango de IP que nadie reconocía y nadie borró: podía ser de un proveedor. Ese es el coste real de no tener infraestructura como código: no es que no puedas crearla; es que no puedes cambiarla con confianza.
Qué es la infraestructura como código: declarativo frente a imperativo
Consiste en describir la infraestructura en ficheros de texto versionados en Git y dejar que una herramienta haga coincidir la realidad con la descripción. No es "automatizar la creación": es que el fichero sea la fuente de verdad. De ahí salen cuatro propiedades:
- Reproducibilidad. El mismo fichero produce el mismo resultado en otra región, en otra cuenta o dentro de seis meses.
- Revisión. La infraestructura pasa por una pull request como el código de 08-01: un cambio en un grupo de seguridad se comenta, se aprueba y queda con autor, fecha y motivo.
- Deriva controlada y documentación viva. La herramienta sabe qué debería existir, así que puede compararlo con lo que existe —sin IaC la deriva es invisible por definición— y el fichero no puede quedarse obsoleto respecto a la realidad, porque es lo que la crea.
- Se puede borrar sin miedo. Cuando levantar un entorno cuesta un comando, apagarlo el viernes deja de ser un riesgo y pasa a ser un ahorro. Es la propiedad que convence a los escépticos.
MercadoFresco ha usado hasta ahora el enfoque imperativo: comandos que describen cómo llegar al estado. CloudFormation es declarativo: describes qué quieres y el servicio calcula los pasos.
# Asi se creo la VPC en el modulo 3. Ejecuta este bloque dos veces y tendras DOS VPC.
aws ec2 create-vpc --cidr-block 10.0.0.0/16 --profile mercadofresco-dev
aws ec2 create-subnet --vpc-id vpc-0a1b2c3d --cidr-block 10.0.1.0/24 --availability-zone eu-west-1a
aws ec2 create-internet-gateway
aws ec2 attach-internet-gateway --vpc-id vpc-0a1b2c3d --internet-gateway-id igw-04e5f6a7El script no sabe nada del estado anterior: cada comando crea. Hacerlo idempotente exigiría envolver cada línea en una comprobación, gestionar los identificadores generados y decidir qué hacer si el recurso existe con otra configuración; es decir, reimplementar CloudFormation en Bash.
| Aspecto | Scripts de la CLI (imperativo) | CloudFormation (declarativo) |
|---|---|---|
| Qué describes | Los pasos para llegar al estado | El estado deseado |
| Ejecutar dos veces | Duplica recursos o falla | No hace nada: ya coincide |
| Actualizar / orden de creación | Otro script; esperas manuales | Editas el fichero; el orden se deduce de las dependencias |
| Fallo a medias | Deshacer manual, a veces imposible | Reversión automática de la pila |
| Borrar todo / inventario | Script inverso a mano / lista manual | delete-stack / la pila es el inventario |
| Cambios manuales | Indetectables | Detección de deriva |
Lo imperativo no desaparece: sigue siendo lo correcto para operaciones puntuales (aws s3 cp, reiniciar una instancia, consultar métricas). La regla: si el recurso debe seguir existiendo mañana, va en una plantilla; si es una acción que ocurre una vez, va en un comando.
Anatomía de una plantilla
Un documento YAML o JSON con nueve secciones posibles. Solo Resources es obligatoria.
AWSTemplateFormatVersion: '2010-09-09' # Version del formato. La unica que existe.
Description: Red base de MercadoFresco. # Documentacion. Ponla siempre.
Metadata: { 'AWS::CloudFormation::Interface': {} } # Para herramientas y consola. No despliega nada.
Parameters: # Entradas: hacen la plantilla reutilizable entre entornos.
Entorno: { Type: String, AllowedValues: [ desarrollo, preproduccion, produccion ] }
Mappings: { PorEntorno: { produccion: { NatPorAz: 2 } } } # Tablas de consulta estaticas
Conditions: # Booleanos a partir de parametros; habilitan recursos.
EsProduccion: !Equals [ !Ref Entorno, produccion ]
Transform: AWS::Serverless-2016-10-31 # Macros. SAM es la mas conocida. Opcional.
Resources: # LA UNICA SECCION OBLIGATORIA.
Vpc: { Type: 'AWS::EC2::VPC', Properties: { CidrBlock: 10.0.0.0/16 } }
Outputs: # Valores que la pila publica, para leer o para importar.
IdVpc: { Value: !Ref Vpc }Faltan Rules (validación de combinaciones de parámetros) y Hooks, mucho menos habituales. La estructura de un recurso siempre es la misma, con Type y Properties obligatorios y el resto opcional:
NombreLogico: # Identificador en la plantilla. NO es el nombre del recurso.
Type: AWS::EC2::Subnet # AWS::<servicio>::<recurso>
Condition: EsProduccion # Opcionales: condicion, dependencia explicita y politicas de
DependsOn: [ AdjuntarIgw ] # borrado y de reemplazo, que se explican mas abajo
DeletionPolicy: Retain
Properties: { VpcId: !Ref Vpc, CidrBlock: 10.0.1.0/24 }Primera regla importante: cambiar el nombre lógico de un recurso equivale a borrarlo y crear otro. CloudFormation identifica los recursos por su nombre lógico, no por lo que hacen. Renombrar Vpc a VpcPrincipal en una plantilla desplegada destruye la VPC.
Parámetros: tipos, restricciones y valores desde SSM
Sin parámetros harían falta tres plantillas casi idénticas para los tres entornos.
Parameters:
Entorno: # Sin Default a proposito: obliga a decidirlo cada vez.
Type: String
AllowedValues: [ desarrollo, preproduccion, produccion ]
CidrVpc:
Type: String
Default: 10.0.0.0/16
AllowedPattern: '^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$'
ConstraintDescription: Debe ser un CIDR valido, por ejemplo 10.0.0.0/16.
CapacidadMinima: { Type: Number, Default: 2, MinValue: 1, MaxValue: 10 }
Propietario: { Type: String, Default: marta, MinLength: 2, MaxLength: 32 }
ClaveSsh: { Type: 'AWS::EC2::KeyPair::KeyName' } # Valida que exista ANTES de crear nada
SubredesApp: { Type: 'List<AWS::EC2::Subnet::Id>' } # Lista de subredes existentes
Contrasena: { Type: String, NoEcho: true } # Oculta en consola y eventos. NO cifra.
AmiTienda: # El valor NO es la ruta: CloudFormation resuelve el
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id> # parametro de SSM y usa su contenido
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64Tres cosas separan una plantilla amateur de una profesional:
- Los tipos específicos de AWS validan antes de empezar. Con
Type: Stringpara una clave SSH inexistente, la pila crea media docena de recursos y falla al llegar a la instancia. ConAWS::EC2::KeyPair::KeyNamefalla en el segundo cero, sin crear nada. AWS::SSM::Parameter::Value<...>desacopla la plantilla de los valores. La AMI de la tienda cambia cada mes; la plantilla no debería cambiar por eso. MercadoFresco lo usa con/mercadofresco/produccion/ami-tienda.NoEchono cifra, solo oculta. Las contraseñas no van en parámetros: van en Secrets Manager (04-03) y se referencian con resolución dinámica, que se evalúa en el despliegue sin que el valor entre nunca en la plantilla:
# La misma sintaxis funciona con ssm y ssm-secure para Parameter Store
MasterUserPassword: '{{resolve:secretsmanager:mercadofresco/produccion/rds/mfadmin:SecretString:password}}'Mappings, Conditions y funciones intrínsecas
Mappings es una tabla de consulta estática de dos niveles, útil para variar por región o por entorno; se lee con Fn::FindInMap. Conditions calcula booleanos con Fn::Equals, Fn::And, Fn::Or y Fn::Not.
Mappings:
PorEntorno:
desarrollo: { NatPorAz: 1, Tipo: t3.small, RetencionRegistros: 7 }
preproduccion: { NatPorAz: 1, Tipo: t3.medium, RetencionRegistros: 30 }
produccion: { NatPorAz: 2, Tipo: m6i.large, RetencionRegistros: 365 }
Conditions:
EsProduccion: !Equals [ !Ref Entorno, produccion ]
EsPreproduccion: !Equals [ !Ref Entorno, preproduccion ]
NecesitaAltaDisponibilidad: !Or [ !Condition EsProduccion, !Condition EsPreproduccion ]
CrearSegundoNat: !Equals [ !FindInMap [ PorEntorno, !Ref Entorno, NatPorAz ], 2 ]Se leen con !FindInMap [ PorEntorno, !Ref Entorno, Tipo ]. Un recurso con Condition: CrearSegundoNat solo se crea si la condición es cierta: nat-mercadofresco-b existe en producción y no en desarrollo con la misma plantilla, y eso ahorra unos 32 USD al mes por entorno no productivo. Aviso: úsalas con moderación. Una plantilla con doce condiciones anidadas es ilegible e imposible de razonar cuando falla; si la diferencia entre entornos es grande, hacen falta plantillas distintas, no más condiciones.
Funciones intrínsecas
Cada una tiene forma larga (Fn::Sub) y corta (!Sub). En YAML se usa la corta salvo cuando hay que anidar dos cortas seguidas, que YAML no permite.
| Función | Qué devuelve | Ejemplo en MercadoFresco |
|---|---|---|
!Ref |
Identificador "natural" del recurso o valor del parámetro | !Ref Vpc → vpc-0a1b2c3d |
!GetAtt |
Un atributo del recurso | !GetAtt Alb.DNSName |
!Sub |
Cadena con sustitución de variables | !Sub 'mercadofresco-${Entorno}-registros' |
!Join / !Select / !Split |
Une, extrae el elemento N y divide listas | !Select [ 0, !GetAZs '' ] |
!ImportValue |
Valor exportado por otra pila | !ImportValue mercadofresco-red-IdVpc |
!FindInMap / !If |
Valor de un Mappings / valor condicional |
!If [ EsProduccion, m6i.large, t3.small ] |
!GetAZs / !Cidr |
AZ de la región / subredes calculadas | !Cidr [ 10.0.0.0/16, 6, 8 ] |
Lo que más confusión genera es !Ref, porque devuelve cosas distintas según el tipo: en una VPC o subred, su identificador; en un bucket, el nombre; en una cola de SQS, la URL; en un tema de SNS, el ARN; en un rol de IAM, el nombre; en una Lambda, el nombre. Esa tabla explica el 80 % de los errores tipo Value of property X must be of type String. Cuando necesites un ARN, casi siempre es !GetAtt Recurso.Arn; la sección "Return values" de cada tipo tiene la lista exacta.
# Sustitucion de parametros y pseudoparametros
BucketName: !Sub 'mercadofresco-${Entorno}-registros-${AWS::AccountId}'
# Atributos de otros recursos: la sintaxis con punto tambien vale
Value: !Sub 'https://${Distribucion.DomainName}/catalogo'
# Con mapa de variables locales, cuando hace falta anidar otra funcion
Description: !Sub
- 'Cola de pedidos del entorno ${Ent} en la cuenta ${Cuenta}'
- { Ent: !Ref Entorno, Cuenta: !Ref 'AWS::AccountId' }
# En desarrollo, sin instantanea final; en produccion, con ella: NoValue omite la propiedad
FinalSnapshotIdentifier: !If [ EsProduccion, !Sub 'final-${AWS::StackName}', !Ref 'AWS::NoValue' ]Los pseudoparámetros son variables que AWS proporciona siempre: AWS::AccountId (111122223333), AWS::Region (eu-west-1), AWS::StackName, AWS::StackId, AWS::Partition, AWS::URLSuffix y AWS::NoValue.
Pilas: ciclo de vida, estados y ROLLBACK_COMPLETE
Una pila es una plantilla desplegada: recursos que se crean, actualizan y borran como una unidad. Es la unidad de gestión, de permisos y de deriva.
stateDiagram-v2
[*] --> CREATE_IN_PROGRESS: create-stack
CREATE_IN_PROGRESS --> CREATE_COMPLETE: todo bien
CREATE_IN_PROGRESS --> ROLLBACK_IN_PROGRESS: fallo en la creacion
ROLLBACK_IN_PROGRESS --> ROLLBACK_COMPLETE: recursos deshechos
ROLLBACK_COMPLETE --> [*]: solo se puede BORRAR
CREATE_COMPLETE --> UPDATE_IN_PROGRESS: update-stack
UPDATE_IN_PROGRESS --> UPDATE_COMPLETE: todo bien
UPDATE_IN_PROGRESS --> UPDATE_ROLLBACK_COMPLETE: fallo, vuelve atras y se puede reintentar
CREATE_COMPLETE --> DELETE_IN_PROGRESS: delete-stack
DELETE_IN_PROGRESS --> DELETE_FAILED: un recurso no se deja borrar
El estado que desconcierta el primer día es ROLLBACK_COMPLETE: ocurre cuando falla la creación inicial y CloudFormation deshace lo creado. La pila queda existiendo pero en estado terminal: no se puede actualizar, solo borrar y volver a crear con la plantilla corregida. No confundir con UPDATE_ROLLBACK_COMPLETE, que sí es sano: la pila volvió a su versión anterior y se puede seguir actualizando.
aws cloudformation validate-template --template-body file://red-mercadofresco.yaml # solo sintaxis
aws cloudformation create-stack --stack-name mercadofresco-red-produccion \
--template-body file://red-mercadofresco.yaml \
--parameters ParameterKey=Entorno,ParameterValue=produccion \
--tags Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion Key=Componente,Value=red \
--enable-termination-protection --profile mercadofresco-dev --region eu-west-1
aws cloudformation wait stack-create-complete --stack-name mercadofresco-red-produccion
# El PRIMER CREATE_FAILED es la causa real; los demas son consecuencias
aws cloudformation describe-stack-events --stack-name mercadofresco-red-produccion \
--query 'StackEvents[?ResourceStatus==`CREATE_FAILED`].[LogicalResourceId,ResourceStatusReason]' \
--output tableLas etiquetas de la pila se propagan a todos los recursos que las admitan: es la forma más barata de cumplir el etiquetado obligatorio de MercadoFresco sin repetirlo recurso a recurso (se retoma en 11-02). Para el día a día, deploy crea la pila si no existe y la actualiza si existe, con un conjunto de cambios por debajo:
aws cloudformation deploy --stack-name mercadofresco-red-produccion \
--template-file red-mercadofresco.yaml --parameter-overrides Entorno=produccion \
--no-fail-on-empty-changeset --capabilities CAPABILITY_NAMED_IAM--no-fail-on-empty-changeset evita que el pipeline se ponga en rojo cuando no hay nada que cambiar, el caso más frecuente. --capabilities es obligatorio cuando la plantilla crea recursos de IAM: es una confirmación explícita de que sabes que estás creando permisos (CAPABILITY_IAM, CAPABILITY_NAMED_IAM si les pones nombre, CAPABILITY_AUTO_EXPAND para macros y pilas anidadas).
Políticas de eliminación y protección de terminación
Por omisión, borrar una pila borra sus recursos. Para aurora-mercadofresco-pedidos y mercadofresco-catalogo-fotos eso es inaceptable.
BaseDatosPedidos:
Type: AWS::RDS::DBCluster
DeletionPolicy: Snapshot # Al borrar la pila: instantanea final y luego borra
UpdateReplacePolicy: Retain # Si una actualizacion exige reemplazo: conserva el viejo
BucketFotos:
Type: AWS::S3::Bucket
DeletionPolicy: Retain # Lo deja en pie, huerfano de la pila
UpdateReplacePolicy: Retain
Properties: { BucketName: mercadofresco-catalogo-fotos }| Política | Valores | Qué hace |
|---|---|---|
DeletionPolicy |
Delete (por omisión) |
Borra el recurso al borrar la pila |
Retain / RetainExceptOnCreate |
Lo deja existiendo / igual, pero sí borra en reversión de creación | |
Snapshot |
Instantánea final y borrado (RDS, Redshift, ElastiCache, EBS, Neptune) | |
UpdateReplacePolicy |
Los mismos valores | Se aplica cuando una actualización reemplaza el recurso |
UpdateReplacePolicy es el que la gente olvida y el que más disgustos evita: cambiar el DBClusterIdentifier de Aurora provoca un reemplazo, y sin ella el clúster viejo —con los datos— desaparece en cuanto el nuevo está listo. La protección de terminación es una capa más, a nivel de pila: aws cloudformation update-termination-protection --stack-name mercadofresco-datos-produccion --enable-termination-protection, y con ella activa delete-stack falla directamente. La regla de MercadoFresco: protección de terminación en las tres pilas de producción, y Retain o Snapshot en todo recurso con estado, en cualquier entorno.
Conjuntos de cambios y los tres tipos de actualización
Un conjunto de cambios es una simulación: CloudFormation compara la plantilla nueva con la pila actual y dice qué haría, sin hacerlo. Es la respuesta a la pregunta que nadie podía contestar en el módulo 8: ¿qué va a pasar si aplico esto?
aws cloudformation create-change-set --stack-name mercadofresco-red-produccion \
--change-set-name anadir-endpoint-dynamodb --template-body file://red-mercadofresco.yaml \
--parameters ParameterKey=Entorno,ParameterValue=produccion --capabilities CAPABILITY_IAM
aws cloudformation describe-change-set --stack-name mercadofresco-red-produccion \
--change-set-name anadir-endpoint-dynamodb \
--query 'Changes[].ResourceChange.[Action,LogicalResourceId,ResourceType,Replacement]' --output table
# | Add | EndpointDynamoDb | AWS::EC2::VPCEndpoint | None |
# | Modify | TablaRutasApp | AWS::EC2::RouteTable | False |
# | Modify | SubredDatosA | AWS::EC2::Subnet | True |La última línea es una alarma roja: Replacement: True significa que la subred se borra y se crea otra, con identificador nuevo, y todo lo que dependa de ella se ve afectado. Sin mirar el conjunto de cambios, eso habría ocurrido un viernes a las once.
| Tipo | Qué ocurre | Identificador físico | Ejemplos |
|---|---|---|---|
| Sin interrupción | Se actualiza en caliente | Se conserva | Etiquetas, reglas de un SG, tamaño de un ASG, capacidad de DynamoDB |
| Con interrupción | Se detiene y arranca | Se conserva | InstanceType de EC2, clase de RDS sin Multi-AZ, memoria de una Lambda |
| Con reemplazo | Se crea uno nuevo y se borra el viejo | Cambia | AvailabilityZone o CidrBlock de subred, BucketName, DBClusterIdentifier, KeyName |
La tercera fila es la que duele: un reemplazo en mercadofresco-catalogo-fotos significa bucket nuevo vacío y bucket viejo borrado con las 40.000 fotos dentro, salvo que UpdateReplacePolicy lo impida. La documentación de cada tipo indica, propiedad a propiedad, el "Update requires", y hay que mirarla antes de escribir el cambio, no después.
flowchart LR
A[Editar plantilla] --> B[create-change-set]
B --> C{describe-change-set}
C -->|Replacement False| D[execute-change-set]
C -->|Algun Replacement True| E{Es aceptable?}
E -->|No| F[Otro enfoque o<br/>migrar datos antes]
E -->|Si, con ventana| D
D --> G[UPDATE_COMPLETE o<br/>UPDATE_ROLLBACK_COMPLETE]
Precaución: si alguien modifica la pila entre que lo creas y lo ejecutas, la simulación deja de ser válida; por eso los pipelines lo crean y lo ejecutan en etapas consecutivas, no con horas de por medio.
Detección de deriva
La deriva es la diferencia entre lo que dice la plantilla y lo que hay. Aparece siempre: durante un incidente, a las tres de la mañana, nadie edita una plantilla.
ID=$(aws cloudformation detect-stack-drift --stack-name mercadofresco-red-produccion \
--query StackDriftDetectionId --output text) # la deteccion es asincrona
aws cloudformation describe-stack-drift-detection-status --stack-drift-detection-id "$ID"
aws cloudformation describe-stack-resource-drifts --stack-name mercadofresco-red-produccion \
--stack-resource-drift-status-filters MODIFIED DELETEDLos estados por recurso son IN_SYNC, MODIFIED, DELETED y NOT_CHECKED. Este último importa: no todos los tipos admiten detección de deriva, así que un IN_SYNC de pila no garantiza que nada haya cambiado. Al encontrar deriva hay dos caminos: actualizar la plantilla para reflejar la realidad, si el cambio manual era legítimo, documentando el motivo en la PR; o reaplicar la pila para deshacerlo. Matiz importante: CloudFormation no tiene un "corregir deriva" automático, porque una actualización solo toca lo que cambió en la plantilla; para forzarlo hay que alterar el valor derivado en la plantilla y actualizar. En MercadoFresco, un trabajo programado la ejecuta cada semana sobre las pilas de producción y publica el resultado en alertas-mercadofresco. AWS Config (05-04) tiene además la regla cloudformation-stack-drift-detection-check, que hace esto de forma continua.
La red de MercadoFresco en una plantilla
red-mercadofresco.yaml, en el repositorio mercadofresco-infra, reproduce la red del módulo 3.
AWSTemplateFormatVersion: '2010-09-09'
Description: MercadoFresco - Red base. VPC con seis subredes en dos AZ, IGW, NAT y endpoint de S3.
Parameters:
Entorno: { Type: String, AllowedValues: [ desarrollo, preproduccion, produccion ] }
CidrVpc: { Type: String, Default: 10.0.0.0/16 }
Propietario: { Type: String, Default: marta }
Mappings:
PorEntorno: { desarrollo: { NatPorAz: 1 }, preproduccion: { NatPorAz: 1 }, produccion: { NatPorAz: 2 } }
Conditions:
# Solo produccion tiene dos NAT; en el resto, app-b sale por el NAT de la AZ a.
CrearSegundoNat: !Equals [ !FindInMap [ PorEntorno, !Ref Entorno, NatPorAz ], 2 ]
Resources:
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref CidrVpc
EnableDnsSupport: true # Necesario para el endpoint de S3 y los nombres de RDS
EnableDnsHostnames: true
Tags: [ { Key: Name, Value: vpc-mercadofresco }, { Key: Proyecto, Value: mercadofresco },
{ Key: Entorno, Value: !Ref Entorno }, { Key: Componente, Value: red },
{ Key: Propietario, Value: !Ref Propietario }, { Key: CentroCoste, Value: plataforma } ]
Igw: { Type: 'AWS::EC2::InternetGateway',
Properties: { Tags: [ { Key: Name, Value: igw-mercadofresco } ] } }
AdjuntarIgw: { Type: 'AWS::EC2::VPCGatewayAttachment',
Properties: { VpcId: !Ref Vpc, InternetGatewayId: !Ref Igw } }
# --- Subredes. !Select con !GetAZs evita fijar 'eu-west-1a': la plantilla vale en otra region.
SubredPublicaA:
Type: AWS::EC2::Subnet
Properties: { VpcId: !Ref Vpc, CidrBlock: 10.0.0.0/24, MapPublicIpOnLaunch: true,
AvailabilityZone: !Select [ 0, !GetAZs '' ],
Tags: [ { Key: Name, Value: snet-mercadofresco-publica-a } ] }
SubredPublicaB:
Type: AWS::EC2::Subnet
Properties: { VpcId: !Ref Vpc, CidrBlock: 10.0.1.0/24, MapPublicIpOnLaunch: true,
AvailabilityZone: !Select [ 1, !GetAZs '' ],
Tags: [ { Key: Name, Value: snet-mercadofresco-publica-b } ] }
SubredAppA: { Type: 'AWS::EC2::Subnet', Properties: { VpcId: !Ref Vpc, CidrBlock: 10.0.10.0/24,
AvailabilityZone: !Select [ 0, !GetAZs '' ], Tags: [ { Key: Name, Value: snet-mercadofresco-app-a } ] } }
SubredAppB: { Type: 'AWS::EC2::Subnet', Properties: { VpcId: !Ref Vpc, CidrBlock: 10.0.11.0/24,
AvailabilityZone: !Select [ 1, !GetAZs '' ], Tags: [ { Key: Name, Value: snet-mercadofresco-app-b } ] } }
SubredDatosA: { Type: 'AWS::EC2::Subnet', Properties: { VpcId: !Ref Vpc, CidrBlock: 10.0.20.0/24,
AvailabilityZone: !Select [ 0, !GetAZs '' ], Tags: [ { Key: Name, Value: snet-mercadofresco-datos-a } ] } }
SubredDatosB: { Type: 'AWS::EC2::Subnet', Properties: { VpcId: !Ref Vpc, CidrBlock: 10.0.21.0/24,
AvailabilityZone: !Select [ 1, !GetAZs '' ], Tags: [ { Key: Name, Value: snet-mercadofresco-datos-b } ] } }
# --- NAT: el segundo solo existe en produccion
IpNatA: { Type: 'AWS::EC2::EIP', Properties: { Domain: vpc } }
IpNatB: { Type: 'AWS::EC2::EIP', Condition: CrearSegundoNat, Properties: { Domain: vpc } }
NatA: { Type: 'AWS::EC2::NatGateway', Properties: { AllocationId: !GetAtt IpNatA.AllocationId,
SubnetId: !Ref SubredPublicaA, Tags: [ { Key: Name, Value: nat-mercadofresco-a } ] } }
NatB: { Type: 'AWS::EC2::NatGateway', Condition: CrearSegundoNat,
Properties: { AllocationId: !GetAtt IpNatB.AllocationId, SubnetId: !Ref SubredPublicaB,
Tags: [ { Key: Name, Value: nat-mercadofresco-b } ] } }
# --- Tablas de rutas
TablaRutasPublica: { Type: 'AWS::EC2::RouteTable',
Properties: { VpcId: !Ref Vpc, Tags: [ { Key: Name, Value: rt-mercadofresco-publica } ] } }
RutaInternet:
Type: AWS::EC2::Route
DependsOn: AdjuntarIgw # Dependencia EXPLICITA: no se deduce de las propiedades
Properties: { RouteTableId: !Ref TablaRutasPublica, DestinationCidrBlock: 0.0.0.0/0, GatewayId: !Ref Igw }
AsociarPublicaA: { Type: 'AWS::EC2::SubnetRouteTableAssociation',
Properties: { SubnetId: !Ref SubredPublicaA, RouteTableId: !Ref TablaRutasPublica } }
AsociarPublicaB: { Type: 'AWS::EC2::SubnetRouteTableAssociation',
Properties: { SubnetId: !Ref SubredPublicaB, RouteTableId: !Ref TablaRutasPublica } }
TablaRutasAppA: { Type: 'AWS::EC2::RouteTable',
Properties: { VpcId: !Ref Vpc, Tags: [ { Key: Name, Value: rt-mercadofresco-app-a } ] } }
RutaNatA: { Type: 'AWS::EC2::Route', Properties: { RouteTableId: !Ref TablaRutasAppA,
DestinationCidrBlock: 0.0.0.0/0, NatGatewayId: !Ref NatA } }
AsociarAppA: { Type: 'AWS::EC2::SubnetRouteTableAssociation',
Properties: { SubnetId: !Ref SubredAppA, RouteTableId: !Ref TablaRutasAppA } }
TablaRutasAppB: { Type: 'AWS::EC2::RouteTable',
Properties: { VpcId: !Ref Vpc, Tags: [ { Key: Name, Value: rt-mercadofresco-app-b } ] } }
RutaNatB: # Con segundo NAT sale por el suyo; si no, comparte el de la AZ a
Type: AWS::EC2::Route
Properties: { RouteTableId: !Ref TablaRutasAppB, DestinationCidrBlock: 0.0.0.0/0,
NatGatewayId: !If [ CrearSegundoNat, !Ref NatB, !Ref NatA ] }
AsociarAppB: { Type: 'AWS::EC2::SubnetRouteTableAssociation',
Properties: { SubnetId: !Ref SubredAppB, RouteTableId: !Ref TablaRutasAppB } }
# Las subredes de datos NO tienen ruta a 0.0.0.0/0: sin salida a internet, por diseno (03-01).
TablaRutasDatos: { Type: 'AWS::EC2::RouteTable',
Properties: { VpcId: !Ref Vpc, Tags: [ { Key: Name, Value: rt-mercadofresco-datos } ] } }
AsociarDatosA: { Type: 'AWS::EC2::SubnetRouteTableAssociation',
Properties: { SubnetId: !Ref SubredDatosA, RouteTableId: !Ref TablaRutasDatos } }
AsociarDatosB: { Type: 'AWS::EC2::SubnetRouteTableAssociation',
Properties: { SubnetId: !Ref SubredDatosB, RouteTableId: !Ref TablaRutasDatos } }
EndpointS3: # De tipo Gateway: no cuesta nada y ahorra el trafico por NAT de las copias a S3
Type: AWS::EC2::VPCEndpoint
Properties: { VpcId: !Ref Vpc, VpcEndpointType: Gateway,
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.s3',
RouteTableIds: [ !Ref TablaRutasAppA, !Ref TablaRutasAppB, !Ref TablaRutasDatos ] }
Outputs:
IdVpc: { Value: !Ref Vpc, Export: { Name: !Sub '${AWS::StackName}-IdVpc' } }
SubredesPublicas: { Value: !Join [ ',', [ !Ref SubredPublicaA, !Ref SubredPublicaB ] ],
Export: { Name: !Sub '${AWS::StackName}-SubredesPublicas' } }
SubredesApp: { Value: !Join [ ',', [ !Ref SubredAppA, !Ref SubredAppB ] ],
Export: { Name: !Sub '${AWS::StackName}-SubredesApp' } }
SubredesDatos: { Value: !Join [ ',', [ !Ref SubredDatosA, !Ref SubredDatosB ] ],
Export: { Name: !Sub '${AWS::StackName}-SubredesDatos' } }Cuatro comentarios. El orden de creación no está en ninguna parte: CloudFormation deduce que SubredPublicaA necesita Vpc porque la referencia con !Ref, y crea en paralelo lo independiente; el único DependsOn es la única dependencia que no se deduce de las propiedades. !GetAZs evita fijar eu-west-1a. !If con !Ref NatB degrada en entornos pequeños sin duplicar la plantilla. Y las salidas se exportan con el prefijo ${AWS::StackName}, de modo que producción y preproducción no chocan: los nombres de exportación son únicos por región.
cfn-lint red-mercadofresco.yaml
aws cloudformation deploy --stack-name mercadofresco-red-produccion \
--template-file red-mercadofresco.yaml --parameter-overrides Entorno=produccion Propietario=marta \
--tags Proyecto=mercadofresco Entorno=produccion Componente=red Propietario=marta \
CentroCoste=plataforma --profile mercadofresco-dev --region eu-west-1Siete minutos después —los NAT tardan unos dos minutos cada uno— la red completa existe. Y esta vez existe también en un fichero, con historial en Git.
La capa de aplicación que importa las salidas
aplicacion-mercadofresco.yaml monta el ALB, el grupo de destino y el ASG encima, sin conocer un solo identificador:
Parameters:
Entorno: { Type: String, AllowedValues: [ desarrollo, preproduccion, produccion ] }
PilaRed: { Type: String, Default: mercadofresco-red-produccion }
AmiTienda: { Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>',
Default: /mercadofresco/produccion/ami-tienda }
Mappings:
PorEntorno: { desarrollo: { Tipo: t3.small, Min: 1, Max: 2 },
preproduccion: { Tipo: t3.medium, Min: 2, Max: 3 }, produccion: { Tipo: m6i.large, Min: 2, Max: 4 } }
Resources:
SgAlb: { Type: 'AWS::EC2::SecurityGroup', Properties: {
GroupDescription: Entrada HTTPS publica hacia el ALB,
VpcId: !ImportValue { 'Fn::Sub': '${PilaRed}-IdVpc' },
SecurityGroupIngress: [ { IpProtocol: tcp, FromPort: 443, ToPort: 443, CidrIp: 0.0.0.0/0 } ] } }
SgTienda: { Type: 'AWS::EC2::SecurityGroup', Properties: { # Origen por grupo de seguridad y
GroupDescription: Trafico del ALB hacia la tienda, # no por CIDR: el patron de 03-02
VpcId: !ImportValue { 'Fn::Sub': '${PilaRed}-IdVpc' },
SecurityGroupIngress: [ { IpProtocol: tcp, FromPort: 8080, ToPort: 8080,
SourceSecurityGroupId: !Ref SgAlb } ] } }
Alb:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties: { Name: !Sub 'alb-mercadofresco-tienda-${Entorno}', Scheme: internet-facing,
Type: application, SecurityGroups: [ !Ref SgAlb ],
Subnets: !Split [ ',', !ImportValue { 'Fn::Sub': '${PilaRed}-SubredesPublicas' } ] }
GrupoDestino:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties: { Name: !Sub 'tg-mercadofresco-tienda-${Entorno}', Port: 8080, Protocol: HTTP,
VpcId: !ImportValue { 'Fn::Sub': '${PilaRed}-IdVpc' },
HealthCheckPath: /salud, HealthCheckIntervalSeconds: 15,
HealthyThresholdCount: 2, UnhealthyThresholdCount: 3,
TargetGroupAttributes: [ { Key: deregistration_delay.timeout_seconds, Value: '30' } ] }
PlantillaLanzamiento:
Type: AWS::EC2::LaunchTemplate
Properties: { LaunchTemplateName: !Sub 'lt-mercadofresco-tienda-${Entorno}',
LaunchTemplateData: { ImageId: !Ref AmiTienda, SecurityGroupIds: [ !Ref SgTienda ],
InstanceType: !FindInMap [ PorEntorno, !Ref Entorno, Tipo ],
IamInstanceProfile: { Name: rol-mercadofresco-tienda },
MetadataOptions: { HttpTokens: required } } } # IMDSv2 obligatorio (04-01)
Asg:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
AutoScalingGroupName: !Sub 'asg-mercadofresco-tienda-${Entorno}'
MinSize: !FindInMap [ PorEntorno, !Ref Entorno, Min ]
MaxSize: !FindInMap [ PorEntorno, !Ref Entorno, Max ]
HealthCheckType: ELB
HealthCheckGracePeriod: 120
VPCZoneIdentifier: !Split [ ',', !ImportValue { 'Fn::Sub': '${PilaRed}-SubredesApp' } ]
TargetGroupARNs: [ !Ref GrupoDestino ]
LaunchTemplate: { LaunchTemplateId: !Ref PlantillaLanzamiento,
Version: !GetAtt PlantillaLanzamiento.LatestVersionNumber }
UpdatePolicy: # Como sustituir las instancias cuando cambia la plantilla de lanzamiento
AutoScalingRollingUpdate: { MinInstancesInService: 2, MaxBatchSize: 1, PauseTime: PT5M }
Outputs:
DnsAlb: { Value: !GetAtt Alb.DNSName }Dos detalles. YAML no admite dos etiquetas cortas seguidas, así que !ImportValue con !Sub dentro obliga a escribir la interna en forma larga ('Fn::Sub'); el error que produce olvidarlo, unsupported structure, no ayuda nada. Y UpdatePolicy decide qué pasa cuando cambia la plantilla de lanzamiento: sin ella, CloudFormation actualiza el ASG y no toca las instancias existentes, así que el cambio no se aplica hasta el siguiente escalado. Es el despliegue por renovación de 08-03 aplicado a la infraestructura.
Varias pilas: exportaciones frente a SSM
Una pila con 300 recursos tarda 40 minutos, revierte entera ante cualquier fallo y no la puede tocar una persona sin coordinarse con las otras dos. La partición sigue dos criterios: por ciclo de vida (lo que cambia junto va junto) y por propiedad (cada pila tiene un dueño claro).
| Pila | Contenido | Frecuencia | Propietario |
|---|---|---|---|
mercadofresco-red-<entorno> |
VPC, subredes, NAT, rutas, endpoints | Muy baja | Marta |
mercadofresco-datos-<entorno> |
Aurora, DynamoDB, ElastiCache, buckets | Baja | Marta |
mercadofresco-integracion-<entorno> |
Colas, DLQ, temas, bus, reglas, Step Functions | Media | Luis |
mercadofresco-aplicacion-<entorno> |
ALB, grupos de destino, ASG, Lambdas | Alta | Luis |
mercadofresco-observabilidad-<entorno> |
Alarmas, paneles, filtros de métricas | Media | Marta |
| Criterio | Exportaciones e Fn::ImportValue |
Parámetros de SSM |
|---|---|---|
| Acoplamiento | Fuerte: no se puede cambiar ni borrar una exportación en uso | Débil: es solo un valor leído |
| Ámbito | Misma cuenta y misma región | Cualquiera: cruza cuentas y regiones |
| Actualizar el valor / orden de borrado | Imposible mientras se importe; borrado impuesto | Libre; el orden queda a la disciplina |
| Legibilidad | Alta: la dependencia es explícita | Menor: hay que saber qué ruta lee cada pila |
| Riesgo típico | Quedarse sin poder cambiar la red | Que alguien cambie el valor y nadie se entere |
La primera fila es la clave: una exportación en uso es un cerrojo. Eso es bueno —evita romper cosas— y es malo —bloquea evoluciones—. La regla de MercadoFresco: exportaciones para lo estructural que no va a cambiar (VPC y subredes), y parámetros de SSM para todo lo demás y siempre que el valor cruce cuentas o regiones (ARN de colas, nombres de tablas, alias de claves).
ParametroArnColaPedidos: # Publica desde la pila de integracion
Type: AWS::SSM::Parameter
Properties: { Type: String, Value: !GetAtt ColaPedidos.Arn,
Name: !Sub '/mercadofresco/${Entorno}/integracion/arn-cola-pedidos' }
# Y la pila de aplicacion lo consume como parametro:
# ArnColaPedidos: { Type: 'AWS::SSM::Parameter::Value<String>',
# Default: /mercadofresco/produccion/integracion/arn-cola-pedidos }Pilas anidadas, módulos y recursos personalizados
Las pilas anidadas incluyen una plantilla dentro de otra como si fuera un recurso; la hija debe estar en S3 (mercadofresco-artefactos sirve) y sus salidas se leen con !GetAtt PilaSubredes.Outputs.SubredesApp.
PilaSubredes:
Type: AWS::CloudFormation::Stack
Properties: { Parameters: { IdVpc: !Ref Vpc, Entorno: !Ref Entorno },
TemplateURL: https://s3.eu-west-1.amazonaws.com/mercadofresco-artefactos/plantillas/subredes.yaml }| Enfoque | Cuándo | Inconvenientes |
|---|---|---|
| Pilas separadas | Ciclos de vida y propietarios distintos | Hay que coordinar el orden |
| Pilas anidadas | Un mismo ciclo de vida con partes repetitivas | Depuración difícil; subir a S3; CAPABILITY_AUTO_EXPAND |
| Módulos | Un patrón idéntico en muchas plantillas | Hay que registrarlos por cuenta y región; poco extendidos |
Antes o después aparece algo que CloudFormation no sabe crear: un usuario en una herramienta externa, una carga inicial de datos. Un recurso personalizado delega en una Lambda: CloudFormation le envía un evento con RequestType y una URL prefirmada, y la función debe responder a esa URL.
import json, urllib.request
def responder(evento, contexto, estado, datos=None, motivo=""):
"""Responder a la URL prefirmada. Si no se llama, la pila se queda una hora colgada."""
cuerpo = json.dumps({"Status": estado, # SUCCESS o FAILED
"Reason": motivo or f"Ver registros: {contexto.log_stream_name}",
"PhysicalResourceId": evento.get("PhysicalResourceId", contexto.log_stream_name),
"StackId": evento["StackId"], "RequestId": evento["RequestId"],
"LogicalResourceId": evento["LogicalResourceId"], "Data": datos or {}}).encode()
peticion = urllib.request.Request(evento["ResponseURL"], data=cuerpo, method="PUT")
peticion.add_header("content-type", "")
urllib.request.urlopen(peticion)
def handler(evento, contexto):
try:
if evento["RequestType"] == "Delete":
return responder(evento, contexto, "SUCCESS") # Nada que deshacer
cargar_categorias(evento["ResourceProperties"]["Tabla"])
responder(evento, contexto, "SUCCESS", {"Categorias": "12"})
except Exception as error: # Capturar SIEMPRE: una excepcion sin
responder(evento, contexto, "FAILED", motivo=str(error)) # responder cuelga la pila 1 horaLos proveedores de recursos son la alternativa formal: se registra un tipo nuevo, por ejemplo MercadoFresco::Facturacion::Cliente, con su esquema JSON y sus manejadores, y a partir de ahí se usa como cualquier otro recurso, con deriva y validación incluidas. Cuesta más y se paga por segundo de manejador, pero el registro público ya trae tipos de terceros —Datadog, MongoDB Atlas, GitHub— que se activan en un clic.
Importar recursos existentes a una pila
Esta es la operación que MercadoFresco necesita de verdad, porque su infraestructura ya existe. La alternativa ingenua —crear pilas nuevas y borrar lo viejo— implicaría migrar datos y una ventana de corte. La importación adopta los recursos existentes sin tocarlos. Cuatro pasos:
- Escribir la plantilla que describe exactamente lo que existe. Aquí no se improvisa: si dice
/healthy el grupo de destino real tiene/salud, la importación pasa y el siguienteupdate-stackcambia la realidad sin avisar. - Añadir
DeletionPolicy: Retaina todos los recursos a importar —es obligatorio— y preparar el fichero de identificadores, que empareja cada nombre lógico con el recurso real. - Crear y ejecutar un conjunto de cambios de tipo
IMPORT.
[
{ "ResourceType": "AWS::EC2::VPC", "LogicalResourceId": "Vpc",
"ResourceIdentifier": { "VpcId": "vpc-0a1b2c3d4e5f6a7b8" } },
{ "ResourceType": "AWS::EC2::Subnet", "LogicalResourceId": "SubredAppA",
"ResourceIdentifier": { "SubnetId": "subnet-0c1d2e3f4a5b6c7d8" } },
{ "ResourceType": "AWS::SQS::Queue", "LogicalResourceId": "ColaPedidos", "ResourceIdentifier": {
"QueueUrl": "https://sqs.eu-west-1.amazonaws.com/111122223333/cola-mercadofresco-pedidos" } }
]aws cloudformation create-change-set --stack-name mercadofresco-red-produccion \
--change-set-name importar-red-existente --change-set-type IMPORT \
--resources-to-import file://recursos-a-importar.json \
--template-body file://red-mercadofresco.yaml \
--parameters ParameterKey=Entorno,ParameterValue=produccion
aws cloudformation execute-change-set --stack-name mercadofresco-red-produccion \
--change-set-name importar-red-existente
aws cloudformation detect-stack-drift --stack-name mercadofresco-red-produccion # PASO OBLIGATORIOLa importación no comprueba que la plantilla coincida con la realidad: solo que los recursos existan y sean del tipo declarado. La deriva inmediatamente después es lo que revela las diferencias. Marta encuentra tres:
| Recurso | Deriva | Decisión |
|---|---|---|
SubredAppB |
Etiqueta Propietario ausente |
Corregir la realidad: aplicar la plantilla |
TablaRutasDatos |
Ruta 0.0.0.0/0 hacia el NAT que nadie recordaba |
Investigar y quitar: los datos no salen |
EndpointS3 |
Política más restrictiva que la plantilla | Corregir la plantilla: la realidad era la buena |
El tercer caso explica por qué el orden importa: primero importar y detectar, después corregir. Escribir la plantilla "como debería ser" y aplicarla directamente habría relajado una política de seguridad sin que nadie se enterara. MercadoFresco importa por capas, empezando por la red —la más estable— y dejando la de datos para el final, con ventana acordada y copia reciente de Aurora.
StackSets y despliegue desde el pipeline
Un StackSet despliega la misma plantilla en un conjunto de cuentas y regiones desde una única operación. Hoy MercadoFresco tiene una cuenta, así que su utilidad es limitada; en 09-04, con cuatro, será la pieza que garantice que toda cuenta nueva nace con la línea base puesta: trail, grabador de Config, alarmas de facturación y roles.
aws cloudformation create-stack-set --stack-set-name ss-mercadofresco-linea-base \
--template-body file://linea-base.yaml --permission-model SERVICE_MANAGED \
--auto-deployment Enabled=true --capabilities CAPABILITY_NAMED_IAMSELF_MANAGED exige crear a mano dos roles de IAM; SERVICE_MANAGED se apoya en Organizations y no requiere nada. Con --auto-deployment Enabled=true, una cuenta añadida a una OU destino recibe la pila sola. Y la conclusión práctica del módulo 8: si la infraestructura es código, se despliega desde el pipeline. CodePipeline tiene acción nativa con cuatro modos; el patrón de MercadoFresco separa simulación de ejecución con una aprobación en medio.
flowchart TB
A[Fuente: mercadofresco-infra] --> B[Construccion:<br/>cfn-lint + cfn-guard]
B --> C[CHANGE_SET_REPLACE]
C --> D[Publicar el resumen<br/>del conjunto de cambios]
D --> E{Aprobacion de Marta}
E -->|Rechaza| G[Fin: nada se ha tocado]
E -->|Aprueba| F[CHANGE_SET_EXECUTE] --> H[Verificar deriva y salidas]
- name: InfraestructuraProduccion
actions:
- name: PrepararCambio
actionTypeId: { category: Deploy, owner: AWS, provider: CloudFormation, version: '1' }
runOrder: 1
configuration:
ActionMode: CHANGE_SET_REPLACE
StackName: mercadofresco-red-produccion
ChangeSetName: cambio-desde-pipeline
TemplatePath: 'Infra::plantillas/red-mercadofresco.yaml'
TemplateConfiguration: 'Infra::parametros/produccion.json'
RoleArn: arn:aws:iam::111122223333:role/rol-cloudformation-mercadofresco
Capabilities: CAPABILITY_NAMED_IAM
- { name: AprobacionMarta, runOrder: 2,
actionTypeId: { category: Approval, owner: AWS, provider: Manual, version: '1' } }
- name: EjecutarCambio
actionTypeId: { category: Deploy, owner: AWS, provider: CloudFormation, version: '1' }
runOrder: 3
configuration: { ActionMode: CHANGE_SET_EXECUTE, StackName: mercadofresco-red-produccion,
ChangeSetName: cambio-desde-pipeline }RoleArn es clave: CloudFormation asume ese rol para crear los recursos, no los permisos de quien lanza el pipeline, así que Luis puede disparar un despliegue de infraestructura sin tener él permiso para crear VPC. Y TemplateConfiguration apunta a un JSON por entorno en el repositorio con Parameters y Tags: con eso, la diferencia entre preproducción y producción deja de ser un misterio y pasa a ser un fichero que se compara con diff en diez segundos.
Buenas prácticas, cfn-lint y cfn-guard
Seis reglas en el README de mercadofresco-infra:
- Plantillas pequeñas y con una responsabilidad. Más de 400 líneas u 80 recursos: se parte, por ciclo de vida.
- Parámetros mínimos. Cada parámetro es una decisión que alguien puede tomar mal. Lo que no varía entre entornos es una constante, no un parámetro.
- Nada de valores mágicos, y nombres físicos solo cuando hacen falta. Ni AMI a mano, ni CIDR repetidos, ni ARN a pelo:
!Sub,Mappingsy SSM cubren todos los casos. Y fijarBucketNameoGroupNameimpide actualizaciones sin interrupción y bloquea desplegar dos veces la misma plantilla en una cuenta. - Todo pasa por PR y por validación automática. Ningún
deploymanual contra producción; una emergencia obliga a abrir la PR de reconciliación el mismo día.
pip install cfn-lint && cfn-lint plantillas/*.yaml
# E3002 Invalid Property Resources/GrupoDestino/Properties/HealthCheckPeriod
# W2001 Parameter Propietario not used
cfn-guard validate --data plantillas/ --rules reglas/mercadofresco.guardlet buckets = Resources.*[ Type == 'AWS::S3::Bucket' ]
rule buckets_cifrados when %buckets !empty {
%buckets.Properties.BucketEncryption exists
<<Todo bucket de MercadoFresco debe declarar cifrado. Ver 04-02.>>
}
let sgs = Resources.*[ Type == 'AWS::EC2::SecurityGroup' ]
rule sin_ssh_abierto when %sgs !empty {
%sgs.Properties.SecurityGroupIngress[*] { when FromPort == 22 { CidrIp != '0.0.0.0/0' } }
}Esa segunda regla es la que habría impedido la diferencia entre preproducción y producción del principio de la lección. Ambas herramientas se ejecutan en build-mercadofresco-integracion (08-02) y fallan la construcción: no son avisos.
Límites, coste y limpieza
| Límite | Valor | Qué hacer al llegar |
|---|---|---|
| Recursos por pila / pilas por cuenta y región | 500 / 2.000 | Partir en varias pilas o anidar; consolidar |
| Parámetros / Salidas | 200 / 200 | Agrupar valores en SSM |
| Plantilla local / en S3 / anidamiento | 51.200 bytes / 1 MB / 5 niveles | --template-url; partir; replantear |
CloudFormation no cobra por gestionar recursos de AWS; cobra 0,0009 USD por segundo de manejador en recursos de terceros y tipos privados, con 1.000 operaciones mensuales gratuitas. El coste real es siempre el de los recursos creados.
# Borrar en orden inverso a las dependencias: primero los consumidores
aws cloudformation delete-stack --stack-name mercadofresco-aplicacion-pruebas
aws cloudformation wait stack-delete-complete --stack-name mercadofresco-aplicacion-pruebas
aws cloudformation delete-stack --stack-name mercadofresco-red-pruebas
aws cloudformation wait stack-delete-complete --stack-name mercadofresco-red-pruebas
# Las IP elasticas huerfanas se cobran
aws ec2 describe-addresses --query 'Addresses[?AssociationId==null].[PublicIp,AllocationId]' --output tableSi el borrado queda en DELETE_FAILED, la causa casi siempre es un bucket con objetos dentro o una interfaz de red creada a mano en la subred; --retain-resources permite terminar el borrado dejando fuera esos recursos concretos.
Errores Comunes y Consejos
Error: renombrar un recurso lógico y perder el recurso. Cambiar Vpc por VpcPrincipal no renombra: borra la VPC y crea otra. Consejo: los nombres lógicos son inmutables en la práctica; si hay que renombrar, se hace en dos pasos con DeletionPolicy: Retain más una importación posterior.
Error: desplegar sin mirar el conjunto de cambios. Es fusionar una PR sin leer el diff. Consejo: que el pipeline lo genere y lo publique antes de la aprobación, prestando atención a la columna Replacement. Error: no entender ROLLBACK_COMPLETE. Es un estado terminal de una creación fallida. Consejo: bórrala y vuelve a crearla, pero mira los eventos antes de borrar, filtrando CREATE_FAILED: el primero es la causa, los demás son consecuencias.
Error: contraseñas en parámetros con NoEcho creyendo que están seguras, o exportaciones para todo. Con veinte exportaciones cruzadas la red se vuelve inmodificable. Consejo: {{resolve:secretsmanager:...}} o {{resolve:ssm-secure:...}} para los secretos, exportaciones solo para lo estructural y SSM para el resto.
Error: creer que la importación valida la plantilla. No lo hace: detección de deriva justo después de cada importación, sin excepción.
Consejo: usa --role-arn en todos los despliegues y una política de pila en los recursos con datos. Lo primero es la diferencia entre "cualquiera que pueda desplegar puede crear cualquier cosa" y "solo la plantilla revisada crea lo que declara"; lo segundo, con set-stack-policy, deniega Update:Replace y Update:Delete sobre recursos concretos, un cinturón para el tirante de UpdateReplacePolicy.
Consejo: mira la plantilla desplegada, no solo la del repositorio. get-template --template-stage Processed la devuelve con macros y transformaciones ya aplicadas; es lo que hay que revisar cuando el resultado no coincide con lo que creías escribir.
Consejo: Description en la plantilla, en cada parámetro y en cada salida. Es la documentación viva del principio, y cuesta treinta segundos.
Ejercicios
Ejercicio 1: la plantilla de integración
Escribe integracion-mercadofresco.yaml: la cola cola-mercadofresco-pedidos con su DLQ mercadofresco-pedidos-fallidos y 5 intentos, el tema mercadofresco-pedido-confirmado, la suscripción de la cola al tema y la publicación del ARN de la cola en Parameter Store. Requisitos: cifrado con alias/mercadofresco-datos, retención de 14 días en la DLQ y 4 en la cola, sufijo de entorno en los nombres, etiquetado obligatorio completo y DeletionPolicy adecuada en la DLQ. Indica qué tipo de actualización provocaría cambiar VisibilityTimeout y cuál cambiar QueueName.
Ejercicio 2: adoptar el ALB existente
alb-mercadofresco-tienda y tg-mercadofresco-tienda llevan ocho meses en producción, creados a mano. Marta quiere adoptarlos en mercadofresco-aplicacion-produccion sin cortar el servicio. Describe el procedimiento: qué recopilar antes y con qué comandos, qué debe llevar la plantilla obligatoriamente, cómo se construye el fichero de identificadores, qué comandos se ejecutan y en qué orden, y qué se hace inmediatamente después. Explica qué ocurriría si la plantilla declarase HealthCheckIntervalSeconds: 30 cuando el grupo de destino real tiene 15, y en qué momento exacto se manifestaría.
Ejercicio 3: partir el monolito de plantilla
Un compañero ha escrito una plantilla de 780 líneas con 96 recursos: VPC y subredes, Aurora, dos tablas de DynamoDB, ElastiCache, ALB, ASG, cuatro colas, bus de eventos, doce alarmas y tres buckets. Tarda 41 minutos y cualquier fallo revierte todo; la semana pasada, un error en una alarma revirtió también un cambio de subred correcto. Propón la partición: qué pilas, con qué contenido y con qué criterio; para cada frontera, si usarías exportación o SSM y por qué; el orden de despliegue y el de borrado; y qué habría pasado la semana pasada con tu partición.
Soluciones
Solución 1
Resources:
ColaPedidosFallidos:
Type: AWS::SQS::Queue
DeletionPolicy: Retain # La DLQ guarda mensajes sin procesar: nunca se borra con la pila
UpdateReplacePolicy: Retain
Properties:
QueueName: !Sub 'mercadofresco-pedidos-fallidos-${Entorno}'
MessageRetentionPeriod: 1209600 # 14 dias, el maximo
KmsMasterKeyId: alias/mercadofresco-datos
Tags: &etiquetas # Ancla YAML: CloudFormation admite alias y evita repetir
- { Key: Proyecto, Value: mercadofresco }
- { Key: Entorno, Value: !Ref Entorno }
- { Key: Componente, Value: integracion }
- { Key: Propietario, Value: luis }
- { Key: CentroCoste, Value: plataforma }
ColaPedidos:
Type: AWS::SQS::Queue
Properties: { QueueName: !Sub 'cola-mercadofresco-pedidos-${Entorno}',
MessageRetentionPeriod: 345600, VisibilityTimeout: 180, # 4 dias de retencion
KmsMasterKeyId: alias/mercadofresco-datos, Tags: *etiquetas,
RedrivePolicy: { deadLetterTargetArn: !GetAtt ColaPedidosFallidos.Arn, maxReceiveCount: 5 } }
TemaPedidoConfirmado:
Type: AWS::SNS::Topic
Properties: { TopicName: !Sub 'mercadofresco-pedido-confirmado-${Entorno}',
KmsMasterKeyId: alias/mercadofresco-datos, Tags: *etiquetas }
SuscripcionColaAlTema:
Type: AWS::SNS::Subscription
Properties: { TopicArn: !Ref TemaPedidoConfirmado, # !Ref de un tema devuelve el ARN
Protocol: sqs, Endpoint: !GetAtt ColaPedidos.Arn, RawMessageDelivery: true }
PoliticaColaParaSns:
Type: AWS::SQS::QueuePolicy
Properties:
Queues: [ !Ref ColaPedidos ] # !Ref de una cola devuelve la URL: es lo que espera aqui
PolicyDocument:
Version: '2012-10-17'
Statement: [ { Effect: Allow, Action: 'sqs:SendMessage', Resource: !GetAtt ColaPedidos.Arn,
Principal: { Service: sns.amazonaws.com },
Condition: { ArnEquals: { 'aws:SourceArn': !Ref TemaPedidoConfirmado } } } ]
ParametroArnCola:
Type: AWS::SSM::Parameter
Properties: { Type: String, Value: !GetAtt ColaPedidos.Arn,
Name: !Sub '/mercadofresco/${Entorno}/integracion/arn-cola-pedidos' }Cambiar VisibilityTimeout es sin interrupción: SQS lo aplica en caliente y la URL no cambia. Cambiar QueueName es con reemplazo: SQS no permite renombrar, así que se crea una cola nueva y se borra la vieja con los mensajes dentro. Es un ejemplo perfecto de por qué fijar nombres físicos tiene coste y de por qué la DLQ lleva Retain. Y un detalle fácil de olvidar: sin la política de cola, SNS no puede entregar y los mensajes se pierden en silencio; la condición aws:SourceArn evita que cualquier otro tema de la cuenta escriba en ella, el mínimo privilegio de 04-01 aplicado a recursos.
Solución 2
Antes: leer la configuración real, nunca escribirla de memoria.
aws elbv2 describe-load-balancers --names alb-mercadofresco-tienda > alb-real.json
aws elbv2 describe-target-groups --names tg-mercadofresco-tienda > tg-real.json
aws elbv2 describe-listeners --load-balancer-arn "$ARN_ALB" > listeners-real.json
aws elbv2 describe-target-group-attributes --target-group-arn "$ARN_TG" > tg-attrs.jsonLa plantilla se escribe a partir de esos ficheros, propiedad por propiedad, incluidos los atributos poco evidentes: deregistration_delay.timeout_seconds, stickiness.enabled, la política TLS del listener y el certificado de ACM. Obligatorio: DeletionPolicy: Retain en el ALB, el grupo de destino y el listener; sin ella la importación se rechaza, y conviene añadir UpdateReplacePolicy: Retain. El fichero de identificadores empareja LogicalResourceId con LoadBalancerArn y TargetGroupArn, usando el ARN completo (arn:aws:elasticloadbalancing:eu-west-1:111122223333:loadbalancer/app/alb-mercadofresco-tienda/a1b2c3).
Orden: cfn-lint; create-change-set con --change-set-type IMPORT; describe-change-set para confirmar que todas las acciones son Import y ninguna es Add o Modify —una sola Modify significa que la plantilla no coincide con la realidad y hay que corregirla antes—; execute-change-set; wait stack-import-complete. Inmediatamente después: detección de deriva, resolviendo caso a caso si manda la plantilla o la realidad. El caso de 30 frente a 15. La importación pasa sin quejarse: solo verifica existencia y tipo. La deriva lo marcaría como MODIFIED. Y si nadie mira, el problema se manifiesta en el siguiente update-stack cualquiera, aunque no tenga nada que ver con el grupo de destino: CloudFormation aplica la plantilla completa y el intervalo pasa a 30 segundos. En la práctica, el ALB tarda el doble en detectar una instancia caída —de 30 a 60 segundos con dos comprobaciones fallidas—, justo el tipo de degradación silenciosa que nadie relaciona con un despliegue de otra cosa. Ese es el argumento entero a favor del paso de deriva.
Solución 3
Cinco pilas, las mismas de la tabla de la lección: red (casi inmutable, todo depende de ella), datos (estado, borrado catastrófico, requiere protección), integracion (sin estado duradero, propietario Luis), aplicacion (lo que más cambia) y observabilidad (cambia con los incidentes, riesgo nulo). Fronteras y orden. Red → todas: exportación, porque los identificadores de VPC y subredes son estructurales, no van a cambiar, y queremos el cerrojo: nadie debe poder borrar la red mientras haya algo encima. Datos → aplicación (endpoint de Aurora, nombres de tablas): SSM, porque cambian con más facilidad de la que parece —una migración a Serverless v2, un endpoint de lectura— y una exportación bloquearía justamente eso. Integración → aplicación: SSM, además porque en 09-04 estas pilas van a vivir en cuentas distintas, donde las exportaciones no funcionan. Todas → observabilidad: SSM; la pila que menos importa y más cambia no debe poder bloquear a nadie.
El orden de despliegue es red → datos e integración en paralelo → aplicación → observabilidad, y el de borrado, el inverso exacto: las exportaciones lo imponen solas en el caso de la red, y en el resto hay que respetarlo por disciplina porque SSM no impone nada.
La semana pasada. El error en la alarma habría fallado únicamente mercadofresco-observabilidad, que despliega en menos de dos minutos y no tiene recursos críticos; el cambio de subred, en mercadofresco-red, ya estaría aplicado y no se habría revertido. El ámbito del fallo pasa de 96 recursos a 12, y los 41 minutos pasan a cuatro despliegues de entre 2 y 12 minutos, algunos en paralelo. Un matiz honesto: la partición no es gratis. Introduce orden de despliegue, obliga a mantener las fronteras y hace que un cambio que toca dos capas necesite dos pilas. La regla que lo justifica es la misma de 08-05 con los despliegues: el ámbito de un fallo debe ser proporcional al riesgo del cambio, y mezclar una alarma con una subred viola esa regla.
Conclusión
La infraestructura de MercadoFresco está por fin en ficheros. red-mercadofresco.yaml describe la VPC, las seis subredes, el gateway de internet, los NAT —dos en producción y uno en el resto, con la misma plantilla— y el endpoint de S3, con historial en Git; aplicacion-mercadofresco.yaml monta encima el ALB, el grupo de destino y el grupo de autoescalado sin conocer un solo identificador, importando las salidas de la primera. Y tienes la anatomía de una plantilla, con parámetros que validan antes de empezar gracias a los tipos específicos de AWS, valores que llegan desde Parameter Store sin tocar la plantilla y secretos resueltos con {{resolve:secretsmanager:...}} en lugar del falso amigo que es NoEcho. Tienes las funciones intrínsecas y la tabla que evita el error más frecuente: que !Ref devuelve la URL de una cola, el ARN de un tema y el nombre de un rol, y que casi siempre lo que buscas es !GetAtt Recurso.Arn. Y tienes el ciclo de vida de una pila, con el estado que desconcierta el primer día: ROLLBACK_COMPLETE, que solo se puede borrar.
Lo que de verdad cambia la forma de trabajar son tres mecanismos. Los conjuntos de cambios, que contestan por escrito a la pregunta que en el módulo 8 nadie podía contestar —¿qué va a pasar si aplico esto?— y que muestran la columna Replacement que separa un cambio rutinario de la destrucción de una subred. Las políticas de eliminación y de reemplazo más la protección de terminación, lo único que se interpone entre un comando y los datos de Aurora. Y la detección de deriva, que convierte los cambios manuales de invisibles en un informe semanal. Con la importación de recursos existentes, MercadoFresco no recrea nada: adopta lo que ya funciona, capa a capa, empezando por la red, y ejecuta la deriva justo después —el paso que nadie debe saltarse, porque la importación comprueba que los recursos existan, no que la plantilla los describa bien—. Las cinco pilas por entorno quedan repartidas por ciclo de vida y por propiedad, con exportaciones para lo estructural y SSM para lo que puede cambiar o cruzar cuentas, sabiendo que una exportación en uso es un cerrojo. Y la infraestructura pasa por el pipeline de 08-04 con el mismo rigor que el código: cfn-lint y cfn-guard que fallan la construcción, un conjunto de cambios publicado, la aprobación de Marta y la ejecución con RoleArn, para que quien dispara no necesite los permisos que se ejercen.
Queda una limitación que se nota en cuanto la plantilla crece. La red describe seis subredes casi idénticas repetidas a mano: no hay bucles, no hay funciones, no hay forma de escribir "crea una subred por cada AZ" ni de encapsular "la red estándar de MercadoFresco" en algo reutilizable. Las condiciones sirven para dos casos y se vuelven ilegibles a partir de cinco. Y no hay manera de escribir una prueba que verifique que ninguna plantilla abre el puerto 22 al mundo: cfn-guard ayuda, pero es otro lenguaje más que aprender.
En 09-02, «AWS CDK», esa misma infraestructura se escribe en TypeScript y en Python, con bucles, condicionales, tipos, constructos reutilizables y pruebas unitarias de verdad, y el resultado sigue siendo una plantilla de CloudFormation desplegada como una pila. Todo lo de esta lección sigue siendo cierto por debajo: el CDK no sustituye a CloudFormation, lo escribe por ti.
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
