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.

Intermediário 30 minutos 7 Objetivos de aprendizado
  1. Distinguir template, stack, logical ID e physical ID
  2. Identificar as seções do template e explicar qual delas é obrigatória
  3. Escolher o tipo de parameter e as constraints que pegam entrada ruim antes de qualquer recurso existir
  4. 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
  5. Prever a ordem em que o CloudFormation cria os recursos e saber quando DependsOn é obrigatório
  6. Comparar Fn::ImportValue, Fn::GetStackOutput e outputs de nested stack para passar valores entre stacks
  7. 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çãoPara que serve
ResourcesObrigatória. Os recursos AWS a criar, cada um com logical ID, Type e Properties
ParametersValores informados na criação ou no update, para um template servir vários ambientes
MappingsTabela de consulta estática lida com Fn::FindInMap, normalmente indexada por Região ou ambiente
ConditionsExpressões booleanas nomeadas que decidem se um recurso ou property entra
OutputsValores devolvidos depois que a stack sobe, opcionalmente exportados para outras stacks
TransformMacros a rodar sobre o template, incluindo AWS::Serverless (SAM) e AWS::LanguageExtensions
MetadataDados extras arbitrários sobre o template, incluindo dicas de interface para o console
RulesValida um parameter ou uma combinação de parameters antes do provisionamento
AWSTemplateFormatVersionA versão do formato do template, 2010-09-09
DescriptionUma 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çãoServe para
Fn::FindInMapLer um valor da seção Mappings
Fn::ImportValueLer um valor exportado por outra stack na mesma conta e Região
Fn::GetStackOutputLer qualquer output de stack, inclusive entre Regiões e entre contas
Fn::Join / Fn::SplitMontar uma string a partir de partes, ou quebrar uma em partes
Fn::SelectPegar um item de uma lista pelo índice
Fn::GetAZsObter as availability zones de uma Região como lista
Fn::Base64Codificar user data
Fn::CidrFatiar blocos CIDR de subnet a partir do CIDR da VPC
Fn::If, Fn::Equals, Fn::Not, Fn::And, Fn::OrLógica de condição
Fn::ForEach, Fn::Length, Fn::ToJsonStringLaç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::ImportValueFn::GetStackOutputOutputs de nested stack
Exige Export no produtorSimNãoNão
Mesma conta, mesma RegiãoSimSimSim
Entre RegiõesNãoSim, com o parâmetro RegionNão
Entre contasNãoSim, com um RoleArnNão
Força da referênciaForte: o produtor não pode ser apagado enquanto for importadoFraca: resolvida só no momento do deployPertence a stack pai
O valor exportado pode mudar em uso?Não, o export fica travadoSim, em silêncioGerenciado 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.

ValorEfeito
DeleteApaga o recurso. O padrão para quase todo tipo.
RetainMantém o recurso e tira ele do escopo do CloudFormation. Continua sendo cobrado.
RetainExceptOnCreateSe comporta como Retain, exceto que um recurso criado por uma operação que depois faz rollback é apagado.
SnapshotTira 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

  • Ref devolve o identificador principal do recurso; Fn::GetAtt devolve qualquer outro atributo pelo nome. Se o enunciado precisa de um ARN e o Ref daquele 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 para Fn::GetStackOutput.
  • "Não é possível apagar a stack, export em uso por outra stack" é comportamento esperado, não defeito. Remova o import primeiro.
  • NoEcho mascara a saída de describe. Qualquer resposta que trate ele como criptografia, ou como proteção para valores que você copiou em Outputs, está errada.
  • Valores sensíveis em template significam dynamic references (ssm-secure, secretsmanager), não parameters.
  • O padrão Snapshot do RDS é queridinho da prova. Leia "nenhum DeletionPolicy especificado" mais "RDS" como snapshot, e as mesmas palavras mais "bucket S3" como delete.
  • Defina UpdateReplacePolicy junto com DeletionPolicy sempre 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 Metadata de 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.