AWS Certified CloudOps Engineer - Associate
Fundamentos do CloudFormation
Como um template do CloudFormation descreve infraestrutura: as seções do template, parameters com trava de verdade, funções intrínsecas, o grafo de dependências que o serviço monta para você e os atributos de recurso que decidem o que sobrevive a um delete.
- Distinguir template, stack, logical ID e physical ID
- Identificar as seções do template e explicar qual delas é obrigatória
- Escolher o tipo de parameter e as constraints que pegam entrada ruim antes de qualquer recurso existir
- Selecionar a função intrínseca certa para cada referência e explicar por que Ref e Fn::GetAtt não são intercambiáveis
- Prever a ordem em que o CloudFormation cria os recursos e saber quando DependsOn é obrigatório
- Comparar Fn::ImportValue, Fn::GetStackOutput e outputs de nested stack para passar valores entre stacks
- Manter credenciais fora do template com dynamic references e dizer o que NoEcho não protege
Um time construiu o ambiente de staging na mão: uma VPC, duas subnets, um security group, um load balancer, um Auto Scaling group e uma instância RDS. Levou uma tarde de cliques. Agora produção precisa da mesma coisa, em uma segunda Região, e ninguém consegue reconstruir de cabeça as regras exatas do security group. Alguém abre o console lado a lado com staging e começa a copiar.
Esse é o problema que o CloudFormation elimina. Você descreve a infraestrutura uma vez, em um arquivo, e a AWS constrói. Construa de novo semana que vem em outra conta e o resultado é o mesmo, porque a descrição é a fonte da verdade no lugar da memória de alguém.
Template, stack e os dois tipos de ID
Três palavras carregam quase todo o significado, e confundir elas deixa todo conceito seguinte mais difícil.
Um template é o arquivo. JSON ou YAML, guardado no Git, revisado como código, sem fazer nada sozinho.
Um stack é o que você obtém quando o CloudFormation processa esse template: uma coleção viva de recursos AWS gerenciada como uma unidade só. Atualize a stack e o CloudFormation descobre quais recursos precisam mudar. Apague a stack e, por padrão, tudo dentro dele vai junto.
Dentro do template, cada recurso tem um logical ID, o nome que você escolhe:
Resources:
WebSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Permite HTTPS vindo do load balancer
VpcId: !Ref AppVpc
WebSecurityGroup é o logical ID. Ele existe só dentro deste template e desta stack. Quando o CloudFormation cria o grupo, a AWS atribui um physical ID como sg-0a1b2c3d4e5f. O logical ID é como você se refere ao recurso enquanto escreve; o physical ID é o que existe de fato na conta.
Essa distinção vira peça central mais adiante. Renomeie um logical ID e o CloudFormation não vê uma renomeação, ele vê um recurso apagado e um recurso diferente criado. O physical ID também é o que muda quando um recurso é substituído durante um update, que é a diferença entre um update inofensivo e uma indisponibilidade.
Mais um enquadramento que rende ao longo de todo o tópico: um template é declarativo. Você declara o estado final, não os passos. Você nunca escreve "crie a VPC, espere, depois crie a subnet". Você declara que a subnet pertence à VPC, e o CloudFormation descobre a ordem.
As seções do template
Um template tem dez seções possíveis de primeiro nível. Exatamente uma é obrigatória.
| Seção | Para que serve |
|---|---|
Resources | Obrigatória. Os recursos AWS a criar, cada um com logical ID, Type e Properties |
Parameters | Valores informados na criação ou no update, para um template servir vários ambientes |
Mappings | Tabela de consulta estática lida com Fn::FindInMap, normalmente indexada por Região ou ambiente |
Conditions | Expressões booleanas nomeadas que decidem se um recurso ou property entra |
Outputs | Valores devolvidos depois que a stack sobe, opcionalmente exportados para outras stacks |
Transform | Macros a rodar sobre o template, incluindo AWS::Serverless (SAM) e AWS::LanguageExtensions |
Metadata | Dados extras arbitrários sobre o template, incluindo dicas de interface para o console |
Rules | Valida um parameter ou uma combinação de parameters antes do provisionamento |
AWSTemplateFormatVersion | A versão do formato do template, 2010-09-09 |
Description | Uma descrição em texto do template |
Um template que declara só Resources é válido. Todo o resto ganha seu lugar deixando o template reutilizável, mais seguro ou mais fácil de ler.
A ordem das seções no arquivo não importa para o CloudFormation, e a ordem dos recursos dentro de Resources também não. Isso surpreende quem espera um script.
Parameters: entrada com trava
Um template com valores fixos é um template que você usa uma vez só. Parameters são como um arquivo serve dev, staging e produção.
Parameters:
EnvironmentName:
Type: String
AllowedValues: [dev, staging, prod]
Description: Qual ambiente esta stack representa
AppVpcId:
Type: AWS::EC2::VPC::Id
Description: A VPC onde implantar
LatestAmiId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
São três tipos diferentes de parameter fazendo três trabalhos diferentes, e a diferença importa mais do que a sintaxe sugere.
EnvironmentName é um String comum, trancado por AllowedValues. As constraints (AllowedValues, AllowedPattern, MinLength, MaxLength, MinValue, MaxValue) são checadas antes de qualquer recurso ser tocado, então um erro de digitação falha em segundos em vez de falhar na metade de um deploy de 12 minutos. Adicione ConstraintDescription e a mensagem de erro diz "precisa ser dev, staging ou prod" em vez de jogar uma expressão regular na cara de quem usa.
AppVpcId usa um tipo de parameter específico da AWS. O CloudFormation valida que a VPC realmente existe nesta conta e nesta Região, e o console mostra um dropdown em vez de uma caixa de texto. Existem tipos para availability zones, AMI IDs, IDs de instância, key pairs, security groups, subnets, volumes, VPCs e hosted zones do Route 53, além da forma List<> de cada um.
LatestAmiId usa um tipo de parameter do SSM. Você informa uma chave do Parameter Store e o CloudFormation busca o valor atual na criação ou no update. Essa linha só substitui a tabela inteira de Região para AMI que os templates antigos carregavam, e é a razão de existir o parameter público /aws/service/ami-amazon-linux-latest/.... Repare no que "atual" significa aqui: o valor é resolvido quando a operação da stack roda, então uma stack criada mês passado continua com o AMI ID do mês passado até você atualizar.
Os tipos básicos são String, Number, List<Number> e CommaDelimitedList. Um template pode declarar até 200 parameters.
O mal-entendido que vale matar agora: parameters não são mecanismo de segredo. NoEcho: true mascara um parameter com asteriscos em describe-stacks e describe-stack-events, o que é útil, mas é um filtro de exibição e nada além disso. Ele não mascara o valor na seção Outputs, na seção Metadata nem no atributo Metadata de um recurso, e o valor continua passando pela sua chamada de API e pelos logs do seu CI. Segredos vão em dynamic references, cobertas mais adiante nesta lição.
Funções intrínsecas
Um template precisa se referir a coisas que ainda não existem. A instância precisa do ID do security group, mas o security group não tem ID nenhum até o CloudFormation criar ele. Funções intrínsecas são como você escreve "seja lá o que isso vier a ser".
Ref devolve o identificador principal de um recurso, ou o valor de um parameter.
SubnetId: !Ref PrivateSubnetA # devolve subnet-0abc123
InstanceType: !Ref InstanceType # devolve o valor do parameter
O que Ref devolve depende do recurso e está documentado por tipo. Para AWS::EC2::Instance é o ID da instância. Para AWS::S3::Bucket é o nome do bucket. Para AWS::IAM::Role é o nome da role, não o ARN, e essa é uma falha de uma linha muito comum.
Fn::GetAtt devolve um atributo nomeado em vez do identificador.
RoleArn: !GetAtt AppRole.Arn
DnsName: !GetAtt AppLoadBalancer.DNSName
Leia a fronteira entre os dois uma vez e você para de chutar: Ref te dá o único identificador que o tipo de recurso elegeu; GetAtt te dá qualquer outro atributo publicado, pelo nome. Quando o enunciado pede um ARN e o Ref daquele tipo devolve um nome, a resposta é GetAtt.
Fn::Sub faz substituição de string, e é o substituto legível para chamadas aninhadas de Fn::Join.
BucketName: !Sub "${EnvironmentName}-app-logs-${AWS::AccountId}"
Arn: !Sub "arn:${AWS::Partition}:s3:::${LogBucket}/*"
Aqueles valores ${AWS::...} são pseudo parameters: valores que o CloudFormation fornece sem você declarar. AWS::Region, AWS::AccountId, AWS::StackName, AWS::StackId, AWS::Partition, AWS::URLSuffix e AWS::NoValue. Usar eles em vez de valores fixos é o que torna um template portátil entre contas, Regiões e partições como a GovCloud.
O resto do conjunto de trabalho, resumido:
| Função | Serve para |
|---|---|
Fn::FindInMap | Ler um valor da seção Mappings |
Fn::ImportValue | Ler um valor exportado por outra stack na mesma conta e Região |
Fn::GetStackOutput | Ler qualquer output de stack, inclusive entre Regiões e entre contas |
Fn::Join / Fn::Split | Montar uma string a partir de partes, ou quebrar uma em partes |
Fn::Select | Pegar um item de uma lista pelo índice |
Fn::GetAZs | Obter as availability zones de uma Região como lista |
Fn::Base64 | Codificar user data |
Fn::Cidr | Fatiar blocos CIDR de subnet a partir do CIDR da VPC |
Fn::If, Fn::Equals, Fn::Not, Fn::And, Fn::Or | Lógica de condição |
Fn::ForEach, Fn::Length, Fn::ToJsonString | Laços e auxiliares, disponíveis só com o transform AWS::LanguageExtensions |
Funções intrínsecas não podem ser usadas em qualquer lugar. Você pode usar em properties de recurso, outputs, atributos de metadata, atributos de update policy e conditions. Você não pode usar na seção Parameters, e é por isso que o default de um parameter nunca pode ser calculado.
O grafo de dependências que você não precisou escrever
Aqui é onde "declarativo" deixa de ser uma palavra abstrata.
Resources:
AppVpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
WebSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Camada web
VpcId: !Ref AppVpc
WebInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref LatestAmiId
SecurityGroupIds:
- !Ref WebSecurityGroup
Nada nesse template declara uma ordem. Mas WebSecurityGroup se refere a AppVpc, e WebInstance se refere a WebSecurityGroup, então o CloudFormation monta um grafo dirigido com essas referências e cria os recursos em uma ordem que satisfaz o grafo: VPC, depois security group, depois instância. Recursos sem aresta entre si são criados em paralelo, e é por isso que uma stack de 40 recursos não demora 40 vezes o tempo de uma stack de 1 recurso.
O delete percorre o mesmo grafo ao contrário. É por isso que você não consegue apagar uma stack de VPC enquanto uma instância dela ainda existir, e por isso que uma ordem de delete inesperada quase sempre significa uma referência inesperada.
Às vezes dois recursos precisam de ordem mesmo sem um se referir ao outro. O caso clássico é um Elastic IP em VPC: ele precisa do internet gateway anexado antes de ser alocado, mas nenhuma property dele menciona o attachment. Para esse caso, e só para ele, você declara a aresta na mão:
NatEip:
Type: AWS::EC2::EIP
DependsOn: AttachGateway
Properties:
Domain: vpc
Use DependsOn quando uma referência não consegue expressar a ordem, não como medida geral de segurança. Espalhar ele por todo lado serializa uma stack que poderia subir em paralelo, e esconde as relações reais de quem for ler depois.
Conditions e Mappings: um template, vários ambientes
Produção precisa de banco Multi-AZ e de um bastion host. Dev não precisa de nenhum dos dois, e pagar por ambos é bobagem. Dois mecanismos resolvem isso sem duplicar o template.
Mappings são uma tabela de consulta 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]
Conditions decidem se algo existe ou não:
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"]
Dois detalhes desse último bloco merecem uma pausa. Uma Condition em um recurso controla se o recurso é criado; um Fn::If dentro de uma property controla o valor daquela property. E AWS::NoValue é o pseudo parameter que significa "omita esta property inteira", que é o único jeito de expressar de forma condicional uma property opcional ausente.
A regra de decisão: mappings para valores que diferem, conditions para recursos e properties que existem ou não.
Outputs e a passagem de valores entre stacks
Uma stack que constrói uma VPC não serve para muita coisa se nada mais consegue descobrir os IDs das subnets.
Outputs:
AppVpcId:
Description: VPC criada por esta stack
Value: !Ref AppVpc
Export:
Name: !Sub "${AWS::StackName}-VpcId"
Value deixa o output visível na stack. Adicionar Export publica ele com um nome que precisa ser único por conta e por Região, e é por isso que prefixar com ${AWS::StackName} virou hábito padrão.
Outro stack então consome:
AppSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !ImportValue network-stack-VpcId
Agora existem três jeitos de passar um valor de uma stack para outro, e eles diferem em força e alcance, não em sintaxe.
Fn::ImportValue | Fn::GetStackOutput | Outputs de nested stack | |
|---|---|---|---|
Exige Export no produtor | Sim | Não | Não |
| Mesma conta, mesma Região | Sim | Sim | Sim |
| Entre Regiões | Não | Sim, com o parâmetro Region | Não |
| Entre contas | Não | Sim, com um RoleArn | Não |
| Força da referência | Forte: o produtor não pode ser apagado enquanto for importado | Fraca: resolvida só no momento do deploy | Pertence a stack pai |
| O valor exportado pode mudar em uso? | Não, o export fica travado | Sim, em silêncio | Gerenciado com o pai |
Essa coluna de força é a troca inteira. Fn::ImportValue te dá integridade referencial: o CloudFormation se recusa a apagar a stack exportadora e se recusa a mudar ou remover um export que outra stack importa. Essa proteção é genuinamente valiosa e genuinamente chata, porque significa que você não renomeia o export de uma subnet sem antes desconectar todo consumidor. Fn::GetStackOutput abre mão da proteção para comprar alcance entre Regiões e entre contas, e resolve o valor fresco a cada operação.
Como manter credenciais fora do arquivo
Nunca coloque senha em um template. É fácil concordar com isso e fácil violar sem querer, então use o mecanismo feito para o caso: dynamic references.
AppDatabase:
Type: AWS::RDS::DBInstance
Properties:
MasterUsername: '{{resolve:ssm:/app/db/username}}'
MasterUserPassword: '{{resolve:secretsmanager:prod/app/db:SecretString:password}}'
Existem três padrões:
{{resolve:ssm:parameter-name:version}}para um valor em texto puro do Parameter Store.{{resolve:ssm-secure:parameter-name:version}}para um parameter SecureString.{{resolve:secretsmanager:secret-id:SecretString:json-key:version-stage:version-id}}para um segredo do Secrets Manager.
O CloudFormation resolve a referência quando precisa do valor e nunca guarda ele. Rotacione o segredo e a próxima operação da stack pega o valor novo sem mudar o template.
Os limites são pequenos mas reais: um template aceita até 60 dynamic references, elas não funcionam em metadata do AWS::CloudFormation::Init nem no UserData de EC2, e não são resolvidas antes de um transform rodar. Uma referência terminada em barra invertida simplesmente não resolve.
Atributos de recurso que mudam o raio de impacto
Atributos de recurso ficam ao lado de Type e Properties, e mudam o que o CloudFormation faz com o recurso, não o que o recurso é.
DeletionPolicy decide o que acontece quando o recurso sai da stack.
| Valor | Efeito |
|---|---|
Delete | Apaga o recurso. O padrão para quase todo tipo. |
Retain | Mantém o recurso e tira ele do escopo do CloudFormation. Continua sendo cobrado. |
RetainExceptOnCreate | Se comporta como Retain, exceto que um recurso criado por uma operação que depois faz rollback é apagado. |
Snapshot | Tira um snapshot e depois apaga. Suportado em volumes EBS, clusters e instâncias de RDS, DocumentDB e Neptune, clusters e replication groups do ElastiCache e clusters do Redshift. |
A exceção que pega as pessoas: AWS::RDS::DBCluster, e qualquer AWS::RDS::DBInstance que não especifique DBClusterIdentifier, têm Snapshot como padrão, não Delete. Apague uma stack desses e você fica com um snapshot vivo e uma conta viva.
UpdateReplacePolicy faz o mesmo trabalho para um evento diferente. DeletionPolicy cobre um recurso sendo removido da stack; UpdateReplacePolicy cobre um recurso sendo substituído durante um update, quando o CloudFormation cria um recurso físico novo e apaga o antigo. Definir DeletionPolicy: Retain em um banco e esquecer UpdateReplacePolicy: Retain deixa você protegido contra a exclusão da stack e desprotegido contra a substituição acidental, que é bem mais provável. Defina os dois.
AppDatabase:
Type: AWS::RDS::DBInstance
DeletionPolicy: Snapshot
UpdateReplacePolicy: Snapshot
Properties:
# ...
Metadata anexa dados arbitrários a um recurso. Ele tem um uso operacional que vale guardar: o CloudFormation não trata mudança em deletion policy, update policy, declaração de condition ou declaração de output como um update, então uma edição de template que toca só nisso produz "No updates to be performed". Mudar qualquer valor de metadata dá ao CloudFormation algo para enxergar, e o update segue.
CreationPolicy e UpdatePolicy, os dois atributos que controlam a espera por sinais e os updates em ondas de Auto Scaling, pertencem às operações de stack e aparecem na próxima lição.
Dicas para a prova
Refdevolve o identificador principal do recurso;Fn::GetAttdevolve qualquer outro atributo pelo nome. Se o enunciado precisa de um ARN e oRefdaquele tipo devolve um nome, a resposta éGetAtt.- "O mesmo template precisa funcionar em qualquer Região ou conta" aponta para pseudo parameters e para tipos de parameter da AWS ou do SSM, e descarta AMI IDs e números de conta fixos.
Fn::ImportValueé só mesma conta e mesma Região. Um enunciado que menciona uma segunda Região ou uma segunda conta e pede um output de stack elimina ele e aponta paraFn::GetStackOutput.- "Não é possível apagar a stack, export em uso por outra stack" é comportamento esperado, não defeito. Remova o import primeiro.
NoEchomascara a saída de describe. Qualquer resposta que trate ele como criptografia, ou como proteção para valores que você copiou emOutputs, está errada.- Valores sensíveis em template significam dynamic references (
ssm-secure,secretsmanager), não parameters. - O padrão
Snapshotdo RDS é queridinho da prova. Leia "nenhum DeletionPolicy especificado" mais "RDS" como snapshot, e as mesmas palavras mais "bucket S3" como delete. - Defina
UpdateReplacePolicyjunto comDeletionPolicysempre que houver dado envolvido. Questões sobre um banco que sumiu durante um update de rotina testam exatamente isso. - "No updates to be performed" depois de editar só deletion policy, update policy, condition ou output é esperado. A correção é mudar um valor de
Metadatade recurso. DependsOné para a ordem que nenhuma referência expressa. O attachment do internet gateway e um Elastic IP são o par canônico.
A ideia para levar daqui é que um template é a descrição de um estado final desejado mais um grafo de relações, e o CloudFormation deriva todo o resto disso: a ordem de criação, a ordem de exclusão, o que roda em paralelo e o que precisa mudar quando você edita uma linha.
O que levanta a pergunta que esta lição deixou aberta de propósito. Você já tem um template que cria uma stack certo na primeira vez. A próxima lição trata da segunda vez e de todas as seguintes: como mudar uma stack em produção sem quebrar ela, como ver o que um update vai fazer antes de ele fazer, e o que fazer quando alguém andou editando seus recursos pelo console.
