AWS Certified CloudOps Engineer - Associate
Fundamentos de CloudFormation
Cómo un template de CloudFormation describe infraestructura: las secciones del template, parámetros con guardarraíles reales, funciones intrínsecas, el grafo de dependencias que CloudFormation arma solo, y los atributos de recurso que deciden qué sobrevive a un delete.
- Distinguir entre template, stack, logical ID y physical ID
- Identificar las secciones de un template y explicar cuál es la única obligatoria
- Elegir el tipo de parámetro y las constraints correctas para atajar una entrada inválida antes de crear cualquier recurso
- Seleccionar la función intrínseca correcta para cada referencia, y explicar por qué Ref y Fn::GetAtt no son intercambiables
- Predecir el orden en que CloudFormation crea los recursos, y saber cuándo hace falta DependsOn
- Comparar Fn::ImportValue, Fn::GetStackOutput y los outputs de nested stacks para compartir valores entre stacks
- Mantener las credenciales fuera del template con dynamic references, y enunciar qué NO protege NoEcho
Un equipo armó staging a mano: una VPC, dos subnets, un security group, un load balancer, un grupo de Auto Scaling y una instancia RDS. Le tomó una tarde de clics. Ahora producción necesita lo mismo, en una segunda región, y nadie puede reconstruir de memoria las reglas exactas del security group. Alguien abre la consola al lado de staging y empieza a copiar.
Ese es el problema que resuelve CloudFormation. Describes la infraestructura una vez, en un archivo, y AWS la construye. La vuelves a construir la semana que viene en otra cuenta y obtienes el mismo resultado, porque la fuente de verdad es la descripción y no la memoria de alguien.
Template, stack y los dos tipos de ID
Tres palabras cargan casi todo el significado, y confundirlas vuelve más difícil cada concepto posterior.
Un template es el archivo. JSON o YAML, guardado en Git, revisado como código, sin hacer nada por sí solo.
Un stack es lo que obtienes cuando CloudFormation procesa ese template: una colección viva de recursos AWS administrada como una unidad. Actualizas el stack y CloudFormation calcula qué recursos hay que cambiar. Borras el stack y, por defecto, todo lo que contiene se va.
Dentro del template, cada recurso tiene un logical ID, el nombre que tú eliges:
Resources:
WebSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Permitir HTTPS desde el load balancer
VpcId: !Ref AppVpc
WebSecurityGroup es el logical ID. Existe solo dentro de este template y de este stack. Cuando CloudFormation crea el grupo, AWS le asigna un physical ID como sg-0a1b2c3d4e5f. El logical ID es cómo te refieres al recurso mientras escribes; el physical ID es lo que realmente existe en la cuenta.
Esa distinción se vuelve crítica más adelante. Renombra un logical ID y CloudFormation no ve un renombre, ve un recurso borrado y otro recurso creado. El physical ID también es lo que cambia cuando un recurso se reemplaza durante un update, y esa es la diferencia entre un update inofensivo y una caída.
Un encuadre más que paga durante todo el tema: un template es declarativo. Enuncias el estado final, no los pasos. Nunca escribes "crea la VPC, espera, después crea la subnet". Enuncias que la subnet pertenece a la VPC, y CloudFormation deduce el orden.
Las secciones del template
Un template tiene diez secciones posibles de primer nivel. Exactamente una es obligatoria.
| Sección | Para qué sirve |
|---|---|
Resources | Obligatoria. Los recursos AWS a crear, cada uno con logical ID, Type y Properties |
Parameters | Valores que entregas al crear o actualizar, para que un template sirva a muchos ambientes |
Mappings | Una tabla de búsqueda estática que se lee con Fn::FindInMap, casi siempre indexada por región o ambiente |
Conditions | Expresiones booleanas con nombre que deciden si un recurso o una property se incluye |
Outputs | Valores que se devuelven una vez construido el stack, opcionalmente exportados para otros stacks |
Transform | Macros que corren sobre el template, incluidos AWS::Serverless (SAM) y AWS::LanguageExtensions |
Metadata | Datos extra arbitrarios sobre el template, incluidas pistas de UI para la consola |
Rules | Valida un parámetro o una combinación de parámetros antes de aprovisionar |
AWSTemplateFormatVersion | La versión del formato de template, 2010-09-09 |
Description | Una descripción en texto del template |
Un template que solo declara Resources es válido. Todo lo demás se gana su lugar volviendo el template reusable, más seguro o más legible.
El orden de las secciones en el archivo no le importa a CloudFormation, y el orden de los recursos dentro de Resources tampoco. Eso sorprende a quien espera un script.
Parameters: entrada con guardarraíles
Un template con valores fijos es un template que puedes usar una sola vez. Los parámetros son cómo un mismo archivo sirve a dev, staging y producción.
Parameters:
EnvironmentName:
Type: String
AllowedValues: [dev, staging, prod]
Description: Qué ambiente representa este stack
AppVpcId:
Type: AWS::EC2::VPC::Id
Description: La VPC donde desplegar
LatestAmiId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
Ahí hay tres tipos de parámetro haciendo tres trabajos distintos, y la diferencia importa más de lo que sugiere la sintaxis.
EnvironmentName es un String común, acotado con AllowedValues. Las constraints (AllowedValues, AllowedPattern, MinLength, MaxLength, MinValue, MaxValue) se validan antes de tocar ningún recurso, así que un typo falla en segundos en vez de fallar a la mitad de un despliegue de 12 minutos. Agrega ConstraintDescription y el mensaje de error dice "debe ser dev, staging o prod" en vez de tirarle una expresión regular al usuario.
AppVpcId usa un tipo de parámetro específico de AWS. CloudFormation valida que la VPC exista de verdad en esta cuenta y región, y la consola muestra un desplegable en vez de una caja de texto. Hay tipos para availability zones, AMI IDs, instance IDs, key pairs, security groups, subnets, volúmenes, VPCs y hosted zones de Route 53, más la variante List<> de cada uno.
LatestAmiId usa un tipo de parámetro de SSM. Le pasas una clave de Parameter Store y CloudFormation trae el valor actual al crear o actualizar. Esa línea reemplaza la tabla entera de región a AMI que arrastraban los templates viejos, y es la razón por la que existe el parámetro público de AWS /aws/service/ami-amazon-linux-latest/.... Ojo con qué significa "actual" aquí: el valor se resuelve cuando corre la operación, así que un stack creado el mes pasado conserva el AMI ID del mes pasado hasta que lo actualices.
Los tipos básicos son String, Number, List<Number> y CommaDelimitedList. Un template puede declarar hasta 200 parámetros.
La confusión que conviene matar ya: los parámetros no son un mecanismo de secretos. NoEcho: true enmascara un parámetro con asteriscos en describe-stacks y describe-stack-events, lo cual es útil, pero es un filtro de visualización y nada más. No enmascara el valor en la sección Outputs, ni en la sección Metadata, ni en el atributo Metadata de un recurso, y el valor igual viaja dentro de tu llamada a la API y de los logs de tu CI. Los secretos van en una dynamic reference, que se cubre más adelante en esta lección.
Funciones intrínsecas
Un template tiene que referirse a cosas que todavía no existen. La instancia necesita el ID del security group, pero el security group no tiene ID hasta que CloudFormation lo crea. Las funciones intrínsecas son cómo escribes "lo que sea que eso resulte ser".
Ref devuelve el identificador primario de un recurso, o el valor de un parámetro.
SubnetId: !Ref PrivateSubnetA # devuelve subnet-0abc123
InstanceType: !Ref InstanceType # devuelve el valor del parámetro
Lo que devuelve Ref depende del recurso y está documentado por tipo. Para AWS::EC2::Instance es el instance ID. Para AWS::S3::Bucket es el nombre del bucket. Para AWS::IAM::Role es el nombre del rol, no el ARN, y esa es una falla de una línea muy común.
Fn::GetAtt devuelve un atributo con nombre en vez del identificador.
RoleArn: !GetAtt AppRole.Arn
DnsName: !GetAtt AppLoadBalancer.DNSName
Lee el límite entre las dos una vez y dejas de adivinar: Ref te da el único identificador que ese tipo de recurso nomina; GetAtt te da cualquier otro atributo publicado, por nombre. Cuando un enunciado pide un ARN y el Ref de ese recurso devuelve un nombre, la respuesta es GetAtt.
Fn::Sub hace sustitución de cadenas, y es el reemplazo legible de los Fn::Join anidados.
BucketName: !Sub "${EnvironmentName}-app-logs-${AWS::AccountId}"
Arn: !Sub "arn:${AWS::Partition}:s3:::${LogBucket}/*"
Esos valores ${AWS::...} son pseudo parámetros: valores que CloudFormation entrega sin que los declares. AWS::Region, AWS::AccountId, AWS::StackName, AWS::StackId, AWS::Partition, AWS::URLSuffix y AWS::NoValue. Usarlos en vez de valores fijos es lo que vuelve portable un template entre cuentas, regiones y particiones como GovCloud.
El resto del conjunto de trabajo, en corto:
| Función | Para qué |
|---|---|
Fn::FindInMap | Leer un valor de la sección Mappings |
Fn::ImportValue | Leer un valor exportado por otro stack en la misma cuenta y región |
Fn::GetStackOutput | Leer cualquier output de stack, incluso entre regiones y entre cuentas |
Fn::Join / Fn::Split | Armar una cadena desde partes, o partir una |
Fn::Select | Tomar un elemento de una lista por índice |
Fn::GetAZs | Obtener las availability zones de una región como lista |
Fn::Base64 | Codificar user data |
Fn::Cidr | Cortar bloques CIDR de subnet a partir del CIDR de la VPC |
Fn::If, Fn::Equals, Fn::Not, Fn::And, Fn::Or | Lógica de conditions |
Fn::ForEach, Fn::Length, Fn::ToJsonString | Bucles y helpers, disponibles solo con el transform AWS::LanguageExtensions |
Las funciones intrínsecas no se pueden usar en cualquier lado. Puedes usarlas en properties de recursos, en outputs, en atributos metadata, en atributos update policy y en conditions. No puedes usarlas en la sección Parameters, y por eso el default de un parámetro nunca puede ser calculado.
El grafo de dependencias que no tuviste que escribir
Aquí es donde "declarativo" deja de ser una palabra abstracta.
Resources:
AppVpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
WebSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Capa web
VpcId: !Ref AppVpc
WebInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref LatestAmiId
SecurityGroupIds:
- !Ref WebSecurityGroup
Nada en ese template enuncia un orden. Pero WebSecurityGroup se refiere a AppVpc, y WebInstance se refiere a WebSecurityGroup, así que CloudFormation arma un grafo dirigido con esas referencias y crea los recursos en un orden que lo satisface: VPC, después security group, después instancia. Los recursos sin arista entre ellos se crean en paralelo, y por eso un stack de 40 recursos no tarda 40 veces lo que uno de 1 recurso.
El borrado recorre el mismo grafo al revés. Por eso no puedes borrar un stack de VPC mientras siga existiendo una instancia suya, y por eso un orden de borrado inesperado casi siempre significa una referencia inesperada.
A veces dos recursos deben ir en cierto orden aunque ninguno se refiera al otro. El caso clásico es una Elastic IP dentro de una VPC: necesita el internet gateway adjunto antes de poder asignarse, pero ninguna de sus properties menciona el attachment. Para eso, y solo para eso, declaras la arista tú:
NatEip:
Type: AWS::EC2::EIP
DependsOn: AttachGateway
Properties:
Domain: vpc
Usa DependsOn cuando ninguna referencia puede expresar el orden, no como medida general de seguridad. Repartirlo por todos lados serializa un stack que podía desplegarse en paralelo, y esconde las relaciones reales al próximo que lea el archivo.
Conditions y Mappings: un template, muchos ambientes
Producción necesita una base de datos Multi-AZ y un bastion host. Dev no necesita ninguno de los dos, y pagarlos igual es absurdo. Dos mecanismos resuelven esto sin bifurcar el template.
Los Mappings son una tabla de búsqueda estática:
Mappings:
EnvConfig:
dev:
InstanceType: t3.small
MinSize: 1
prod:
InstanceType: m6i.large
MinSize: 3
Resources:
AppGroup:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
MinSize: !FindInMap [EnvConfig, !Ref EnvironmentName, MinSize]
Las Conditions deciden si algo existe o no:
Conditions:
IsProduction: !Equals [!Ref EnvironmentName, prod]
Resources:
BastionHost:
Type: AWS::EC2::Instance
Condition: IsProduction
Properties:
# ...
AppDatabase:
Type: AWS::RDS::DBInstance
Properties:
MultiAZ: !If [IsProduction, true, false]
DBSnapshotIdentifier: !If [IsProduction, !Ref SnapshotId, !Ref "AWS::NoValue"]
Dos detalles de ese último bloque merecen una pausa. Una Condition sobre un recurso controla si el recurso se crea; un Fn::If dentro de una property controla el valor de esa property. Y AWS::NoValue es el pseudo parámetro que significa "omite esta property por completo", que es la única forma de expresar condicionalmente una property opcional ausente.
La regla de decisión: mappings para valores que cambian, conditions para recursos y properties que existen o no existen.
Outputs, y compartir valores entre stacks
Un stack que construye una VPC sirve de poco si nada más puede encontrar los IDs de las subnets.
Outputs:
AppVpcId:
Description: VPC creada por este stack
Value: !Ref AppVpc
Export:
Name: !Sub "${AWS::StackName}-VpcId"
Value hace visible el output en el stack. Agregar Export lo publica bajo un nombre que debe ser único por cuenta y región, y por eso el hábito estándar es prefijarlo con ${AWS::StackName}.
Otro stack lo consume:
AppSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !ImportValue network-stack-VpcId
Ahora hay tres formas de pasar un valor de un stack a otro, y difieren en fuerza y alcance, no en sintaxis.
Fn::ImportValue | Fn::GetStackOutput | Outputs de nested stack | |
|---|---|---|---|
Exige un Export en el productor | Sí | No | No |
| Misma cuenta, misma región | Sí | Sí | Sí |
| Entre regiones | No | Sí, con el parámetro Region | No |
| Entre cuentas | No | Sí, con un RoleArn | No |
| Fuerza de la referencia | Fuerte: el productor no se puede borrar mientras se importe | Débil: se resuelve solo al desplegar | La maneja el stack padre |
| ¿Puede cambiar el valor exportado mientras se usa? | No, el export queda bloqueado | Sí, en silencio | Se administra con el padre |
Esa columna de fuerza es todo el trade. Fn::ImportValue te da integridad referencial: CloudFormation se niega a borrar el stack que exporta, y se niega a cambiar o quitar un export que otro stack importa. Esa protección es genuinamente valiosa y genuinamente molesta, porque significa que no puedes renombrar el export de una subnet sin desconectar antes a cada consumidor. Fn::GetStackOutput renuncia a la protección para comprar alcance entre regiones y entre cuentas, y resuelve el valor de nuevo en cada operación.
Mantener las credenciales fuera del archivo
Nunca pongas un password en un template. Es fácil estar de acuerdo con eso y fácil violarlo sin querer, así que usa el mecanismo hecho para el caso: las dynamic references.
AppDatabase:
Type: AWS::RDS::DBInstance
Properties:
MasterUsername: '{{resolve:ssm:/app/db/username}}'
MasterUserPassword: '{{resolve:secretsmanager:prod/app/db:SecretString:password}}'
Existen tres patrones:
{{resolve:ssm:nombre-parametro:version}}para un valor de Parameter Store en texto plano.{{resolve:ssm-secure:nombre-parametro:version}}para un parámetro SecureString.{{resolve:secretsmanager:id-secreto:SecretString:clave-json:version-stage:version-id}}para un secreto de Secrets Manager.
CloudFormation resuelve la referencia cuando necesita el valor y nunca lo guarda. Rotas el secreto y la siguiente operación del stack toma el valor nuevo sin cambiar el template.
Los límites son chicos pero reales: un template admite hasta 60 dynamic references, no funcionan dentro del metadata de AWS::CloudFormation::Init ni en el UserData de EC2, y no se resuelven antes de que corra un transform. Una referencia que termina en barra invertida no resuelve nunca.
Atributos de recurso que cambian el radio de daño
Los atributos de recurso van al lado de Type y Properties, y cambian lo que CloudFormation le hace al recurso en vez de cambiar lo que el recurso es.
DeletionPolicy decide qué pasa cuando el recurso sale del stack.
| Valor | Efecto |
|---|---|
Delete | Borra el recurso. El default de casi todos los tipos. |
Retain | Conserva el recurso y lo saca del alcance de CloudFormation. Sigue facturando. |
RetainExceptOnCreate | Se comporta como Retain, salvo que un recurso creado por una operación que después hace rollback sí se borra. |
Snapshot | Toma un snapshot y después borra. Soportado en volúmenes EBS, clusters e instancias de RDS, DocumentDB y Neptune, clusters y replication groups de ElastiCache, y clusters de Redshift. |
La excepción que atrapa a la gente: AWS::RDS::DBCluster, y cualquier AWS::RDS::DBInstance que no especifique DBClusterIdentifier, usan Snapshot por defecto, no Delete. Borras ese stack y te quedan un snapshot vivo y una factura viva.
UpdateReplacePolicy hace el mismo trabajo para otro evento. DeletionPolicy cubre que el recurso salga del stack; UpdateReplacePolicy cubre que el recurso se reemplace durante un update, donde CloudFormation crea un recurso físico nuevo y borra el viejo. Poner DeletionPolicy: Retain en una base de datos y olvidar UpdateReplacePolicy: Retain te deja protegido contra un borrado de stack y desprotegido contra el reemplazo accidental, que es mucho más probable. Pon los dos.
AppDatabase:
Type: AWS::RDS::DBInstance
DeletionPolicy: Snapshot
UpdateReplacePolicy: Snapshot
Properties:
# ...
Metadata adjunta datos arbitrarios a un recurso. Tiene un uso operativo que vale la pena recordar: CloudFormation no considera un update a los cambios en una deletion policy, una update policy, una condition o la declaración de un output, así que una edición del template que solo toca eso produce "No updates to be performed". Cambiar cualquier valor de metadata le da a CloudFormation algo que ver, y el update procede.
CreationPolicy y UpdatePolicy, los dos atributos que controlan la espera de señales y los updates rodados de Auto Scaling, pertenecen a las operaciones de stack y aparecen en la próxima lección.
Consejos para el examen
Refdevuelve el identificador primario del recurso;Fn::GetAttdevuelve cualquier otro atributo por nombre. Si el enunciado necesita un ARN y elRefde ese tipo devuelve un nombre, la respuesta esGetAtt.- "El mismo template debe funcionar en cualquier región o cuenta" apunta a pseudo parámetros y a tipos de parámetro de AWS o de SSM, y descarta AMI IDs y números de cuenta escritos a mano.
Fn::ImportValuees solo misma cuenta, misma región. Un enunciado que menciona una segunda región o una segunda cuenta y pide un output de stack lo descarta y apunta aFn::GetStackOutput.- "No se puede borrar el stack, el export lo usa otro stack" es el comportamiento esperado, no un bug. Primero quitas el import.
NoEchoenmascara la salida de describe. Cualquier respuesta que lo trate como cifrado, o como protección para valores que copiaste aOutputs, está mal.- Valores sensibles en un template significan dynamic references (
ssm-secure,secretsmanager), no parámetros. - El default
Snapshotde RDS es un favorito. Lee "sin DeletionPolicy especificado" más "RDS" como snapshot, y esas mismas palabras más "bucket de S3" como delete. - Pon
UpdateReplacePolicyjunto aDeletionPolicysiempre que haya datos de por medio. Las preguntas sobre una base que desapareció durante un update de rutina evalúan exactamente esto. - "No updates to be performed" después de editar solo una deletion policy, update policy, condition u output es lo esperado. El arreglo es cambiar un valor de
Metadataen un recurso. DependsOnes para el orden que ninguna referencia expresa. El attachment del internet gateway y una Elastic IP son el par canónico.
La idea que conviene llevarse es que un template es una descripción de un estado final deseado más un grafo de relaciones, y CloudFormation deriva todo lo demás de ahí: el orden de creación, el orden de borrado, qué corre en paralelo y qué tiene que cambiar cuando editas una línea.
Lo que deja abierta la pregunta que esta lección esquivó a propósito. Ya tienes un template que crea un stack bien la primera vez. La próxima lección trata de la segunda vez y de todas las siguientes: cómo cambiar un stack en marcha sin romperlo, cómo ver qué hará un update antes de que lo haga, y qué hacer cuando alguien estuvo editando tus recursos desde la consola.
