luizmachado.dev

PT EN

Session 002 — CloudFormation: stacks, templates, parameters, outputs, Ref/GetAtt

Estimated duration: 60 minutes
Prerequisites: session-001 — Advanced AWS CLI (SSO, profiles, assume-role)


Objective

By the end, you will be able to write a complete YAML template with typed Parameters, Resources referencing others with Ref and Fn::GetAtt, Outputs exported across stacks, and deploy via aws cloudformation deploy with changesets.


Context

[FACT] CloudFormation is AWS's native IaC service since 2011. It operates on a declarative model: you describe the desired state of the infrastructure in a template, and CloudFormation calculates the delta between the stack's current state and the desired one, executing the necessary operations in the correct order.

[CONSENSUS] Even if you use CDK (which comes in upcoming sessions), understanding raw CloudFormation is essential — CDK generates CloudFormation as its output artifact, and any deploy debugging happens at the CloudFormation level. Engineers who skip this layer are blind when something goes wrong during cdk deploy.

The central distinction in this session is: a template is the document, a stack is the running instance. The same template can create multiple stacks in different regions or accounts.


Key concepts

1. Template anatomy

[FACT] A CloudFormation template has 10 possible sections. Only Resources is required.

AWSTemplateFormatVersion: "2010-09-09"   # fixo, nunca muda
Description: "O que essa stack faz"

Metadata: {}        # dados para ferramentas externas (ex: Console UI hints)
Parameters: {}      # inputs do usuário ao criar/atualizar a stack
Rules: {}           # validações de parâmetros (cross-parameter constraints)
Mappings: {}        # lookup tables estáticas (ex: AMI por região)
Conditions: {}      # flags booleanas baseadas em parâmetros
Transform: {}       # macros (ex: SAM usa AWS::Serverless-2016-10-31)
Resources: {}       # OBRIGATÓRIO — recursos a criar
Outputs: {}         # valores a exportar ou exibir

CloudFormation processing order:

1. Parameters   → resolve inputs
2. Rules        → valida combinações de parâmetros
3. Mappings     → disponibiliza lookup tables
4. Conditions   → avalia flags booleanas
5. Resources    → determina ordem de criação pelo grafo de dependências
6. Outputs      → resolve referências após criação dos recursos

This order matters because Ref and Fn::GetAtt in Outputs only work after the resources exist.


2. Parameters — typing, constraints, and best practices

Parameters receive values at runtime. Unlike environment variables, they are validated by CloudFormation before any resource is created.

Available types:

Parameters:

  # Tipos primitivos
  Env:
    Type: String
    AllowedValues: [dev, staging, prod]
    Default: dev

  InstanceType:
    Type: String
    AllowedPattern: "^(t3|m5|c5)\\.(micro|small|medium|large)$"
    ConstraintDescription: "Apenas instâncias t3, m5 ou c5"

  Port:
    Type: Number
    MinValue: 1024
    MaxValue: 65535

  # Tipos AWS — CloudFormation valida a existência do recurso na conta
  VpcId:
    Type: AWS::EC2::VPC::Id           # dropdown no console, validado

  SubnetIds:
    Type: List<AWS::EC2::Subnet::Id>  # lista de subnets

  AmiId:
    Type: AWS::EC2::Image::Id

  # SSM Parameter Store — resolve o valor em runtime
  LatestAmiId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64

[CONSENSUS] Using AWS::SSM::Parameter::Value<AWS::EC2::Image::Id> for AMIs is the recommended practice — the template always uses the latest AMI without needing to be updated. The downside is that the deploy can change without any template modification, which complicates drift tracking.

Referencing parameters:

Resources:
  MyInstance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType   # !Ref retorna o valor do parâmetro
      ImageId: !Ref LatestAmiId

3. Resources — the heart of the template

Each resource has: a logical ID (name in the template), Type, and Properties.

Resources:

  # Logical ID: nome interno — referenciado por Ref/GetAtt
  # Convenção: PascalCase descritivo (AppBucket, WebServerSG, etc.)
  AppBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain             # Snapshot | Retain | Delete (padrão)
    UpdateReplacePolicy: Retain        # comportamento ao substituir o recurso
    DependsOn: LogGroup                # força ordem explícita (use só quando necessário)
    Properties:
      BucketName: !Sub "app-${Env}-${AWS::AccountId}"
      VersioningConfiguration:
        Status: Enabled
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256

  WebServerSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "Web server security group"
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0

  WebServer:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      ImageId: !Ref LatestAmiId
      SecurityGroupIds:
        - !Ref WebServerSG          # Ref num recurso retorna o ID físico
      Tags:
        - Key: Name
          Value: !Sub "web-${Env}"

DeletionPolicy and UpdateReplacePolicy — critical difference:

DeletionPolicy       → o que acontece quando a STACK é deletada
UpdateReplacePolicy  → o que acontece quando o recurso precisa ser SUBSTITUÍDO
                       (ex: mudar o nome de um S3 bucket obriga substituição)

Valores:
  Delete    → recurso é deletado (padrão para a maioria)
  Retain    → recurso permanece na conta, desassociado da stack
  Snapshot  → tira snapshot antes de deletar (RDS, EBS, ElastiCache)

[FACT] DeletionPolicy: Retain on an S3 bucket does not prevent CloudFormation from trying to delete the bucket when deleting the stack — it only makes CloudFormation "forget" the resource instead of deleting it. If the bucket has objects and does not have Retain, the stack delete will fail with BucketNotEmpty.


4. Ref and Fn::GetAtt — what each one returns

This is the biggest source of confusion in CloudFormation. Ref and Fn::GetAtt return different things depending on the resource type.

Ref — returns the resource's primary identifier:

# Cada tipo de recurso tem um "Ref value" diferente:
!Ref AppBucket        # → nome do bucket (ex: "app-prod-123456789")
!Ref WebServerSG      # → Group ID (ex: "sg-0abc123")
!Ref WebServer        # → Instance ID (ex: "i-0abc123")
!Ref MyQueue          # → URL da fila (ex: "https://sqs.us-east-1...")
!Ref MyTable          # → nome da tabela DynamoDB
!Ref MyFunction       # → nome da função Lambda
!Ref MyRole           # → nome da role IAM (NÃO o ARN)

[FACT] To know what Ref returns for a specific type, check the resource documentation under "Return values > Ref". There is no universal rule — each service defines what its "primary identifier" is.

Fn::GetAtt — returns specific attributes:

# Sintaxe longa
Fn::GetAtt:
  - AppBucket
  - Arn

# Sintaxe curta (YAML)
!GetAtt AppBucket.Arn           # → arn:aws:s3:::app-prod-123456789
!GetAtt AppBucket.DomainName    # → app-prod-123456789.s3.amazonaws.com
!GetAtt WebServerSG.GroupId     # → sg-0abc123 (mesmo que Ref aqui)
!GetAtt WebServer.PrivateIp     # → 10.0.1.50
!GetAtt WebServer.PublicIp      # → 3.x.x.x (se tiver IP público)
!GetAtt MyRole.Arn              # → arn:aws:iam::123:role/MyRole
!GetAtt MyFunction.Arn          # → arn:aws:lambda:us-east-1:123:function:name

Diagram of the difference:

Recurso: AWS::S3::Bucket (logical ID: AppBucket)
         │
         ├── Ref           → nome do bucket   "app-prod-123456789012"
         ├── GetAtt .Arn   → ARN completo      "arn:aws:s3:::app-prod-..."
         └── GetAtt .DomainName → endpoint     "app-prod-....s3.amazonaws.com"

Recurso: AWS::IAM::Role (logical ID: MyRole)
         │
         ├── Ref           → nome da role      "MyRole-XXXXXXXXXXX"
         └── GetAtt .Arn   → ARN completo      "arn:aws:iam::123:role/MyRole-..."
         (para IAM Roles você quase sempre quer o ARN → use GetAtt, não Ref)

Most commonly used string functions alongside Ref/GetAtt:

# Sub — interpolação de strings (mais legível que Join)
!Sub "arn:aws:s3:::${AppBucket}/*"
!Sub "https://${WebServer.PublicIp}:443"
!Sub "${AWS::StackName}-${AWS::Region}-logs"   # pseudo-parâmetros

# Join — une lista com delimitador
!Join [",", [!Ref SubnetA, !Ref SubnetB]]

# Select — pega elemento de lista por índice
!Select [0, !GetAZs ""]   # primeira AZ da região atual

# Pseudo-parâmetros sempre disponíveis (sem declarar em Parameters):
# AWS::AccountId, AWS::Region, AWS::StackName, AWS::StackId, AWS::NoValue

5. Outputs and cross-stack references

Outputs serve two distinct purposes: displaying useful values after deploy, and exporting values for other stacks to consume.

Output structure:

Outputs:

  BucketName:
    Description: "Nome do bucket de aplicação"
    Value: !Ref AppBucket          # valor exibido no console e retornado pela API

  BucketArn:
    Description: "ARN do bucket"
    Value: !GetAtt AppBucket.Arn
    Export:
      Name: !Sub "${AWS::StackName}-BucketArn"   # nome único na região/conta

Consuming an export in another stack:

# Stack B importa o valor exportado pela Stack A
Resources:
  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Environment:
        Variables:
          BUCKET_ARN: !ImportValue "minha-stack-infra-BucketArn"

Critical Export/ImportValue restrictions — [FACT]:

1. Nomes de export são únicos por região/conta — não existem namespaces
2. Você NÃO pode deletar uma stack que tem outputs exportados
   enquanto outra stack estiver usando esses exports via ImportValue
3. Você NÃO pode modificar ou remover um output exportado
   enquanto ele estiver sendo importado
4. Exports NÃO funcionam cross-region — só cross-stack na mesma região
5. O nome do Export NÃO pode usar Ref/GetAtt de recursos
   (pode usar !Sub com pseudo-parâmetros como ${AWS::StackName})

[OPINION — Alex Pulver, AWS CDK team] For complex architectures, the coupling created by Export/ImportValue makes operations like delete and refactoring difficult. An alternative is using SSM Parameter Store to share values between stacks, maintaining decoupling.


6. aws cloudformation deploy — what happens under the hood

aws cloudformation deploy is a high-level wrapper that:
1. Creates a changeset (never applies directly)
2. Waits for the changeset to be calculated
3. Executes the changeset automatically
4. Waits for the stack to reach CREATE_COMPLETE or UPDATE_COMPLETE

# Deploy básico
aws cloudformation deploy \
  --template-file template.yaml \
  --stack-name minha-stack \
  --parameter-overrides Env=prod InstanceType=t3.medium \
  --capabilities CAPABILITY_IAM \
  --profile prod

# Ver o changeset antes de executar (não aplica)
aws cloudformation deploy \
  --template-file template.yaml \
  --stack-name minha-stack \
  --no-execute-changeset

# Ver o changeset gerado
aws cloudformation describe-change-set \
  --stack-name minha-stack \
  --change-set-name <nome-do-changeset>

# Executar manualmente após revisar
aws cloudformation execute-change-set \
  --stack-name minha-stack \
  --change-set-name <nome-do-changeset>

--capabilities — when to use:

CAPABILITY_IAM          → template cria roles ou policies IAM sem nome customizado
CAPABILITY_NAMED_IAM    → template cria roles com nome explícito (mais permissivo)
CAPABILITY_AUTO_EXPAND  → template usa Transform (SAM, macros customizadas)

[FACT] Without the correct capability, the deploy fails with InsufficientCapabilitiesException. This is intentional — it's a safeguard against accidental creation of IAM resources.

aws cloudformation deploy flow diagram:

aws cloudformation deploy
        │
        ▼
  Stack existe?
    │         │
   Não        Sim
    │          │
    ▼          ▼
CREATE_     UPDATE_
CHANGESET   CHANGESET
    │          │
    └────┬─────┘
         ▼
   Aguarda REVIEW_IN_PROGRESS
         │
         ▼
   Executa changeset
         │
         ▼
   Aguarda CREATE_COMPLETE
      ou UPDATE_COMPLETE
         │
   Erro? → ROLLBACK_COMPLETE
           (stack volta ao estado anterior)

Most important stack statuses:

CREATE_IN_PROGRESS    → criação em andamento
CREATE_COMPLETE       → criação concluída com sucesso
CREATE_FAILED         → falha na criação (stack permanece)
ROLLBACK_IN_PROGRESS  → rollback em andamento após falha
ROLLBACK_COMPLETE     → rollback concluído (stack existe mas vazia)
UPDATE_IN_PROGRESS    → atualização em andamento
UPDATE_COMPLETE       → atualização concluída
UPDATE_ROLLBACK_*     → rollback de atualização
DELETE_IN_PROGRESS    → deleção em andamento
DELETE_FAILED         → falha na deleção (recurso não pôde ser deletado)

Practical example

Complete template for an application with S3 bucket + IAM role + value exports:

AWSTemplateFormatVersion: "2010-09-09"
Description: "Infraestrutura base — bucket de app + role de acesso"

Parameters:
  Env:
    Type: String
    AllowedValues: [dev, staging, prod]
    Default: dev

  ProjectName:
    Type: String
    MinLength: 3
    MaxLength: 20
    AllowedPattern: "^[a-z][a-z0-9-]+$"
    ConstraintDescription: "Lowercase, começa com letra, apenas letras/números/hifens"

Resources:
  AppBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: !If [IsProd, Retain, Delete]
    Properties:
      BucketName: !Sub "${ProjectName}-${Env}-${AWS::AccountId}-${AWS::Region}"
      VersioningConfiguration:
        Status: !If [IsProd, Enabled, Suspended]
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  AppBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref AppBucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: DenyNonTLS
            Effect: Deny
            Principal: "*"
            Action: s3:*
            Resource:
              - !GetAtt AppBucket.Arn
              - !Sub "${AppBucket.Arn}/*"
            Condition:
              Bool:
                aws:SecureTransport: false

  AppRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${ProjectName}-${Env}-app-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: BucketAccess
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                  - s3:DeleteObject
                Resource: !Sub "${AppBucket.Arn}/*"

Conditions:
  IsProd: !Equals [!Ref Env, prod]

Outputs:
  BucketName:
    Description: "Nome do bucket"
    Value: !Ref AppBucket
    Export:
      Name: !Sub "${AWS::StackName}-BucketName"

  BucketArn:
    Description: "ARN do bucket"
    Value: !GetAtt AppBucket.Arn
    Export:
      Name: !Sub "${AWS::StackName}-BucketArn"

  AppRoleArn:
    Description: "ARN da role da aplicação"
    Value: !GetAtt AppRole.Arn
    Export:
      Name: !Sub "${AWS::StackName}-AppRoleArn"

Deploy:

# Validar o template antes de enviar
aws cloudformation validate-template --template-body file://template.yaml

# Deploy em dev
aws cloudformation deploy \
  --template-file template.yaml \
  --stack-name meu-projeto-infra \
  --parameter-overrides Env=dev ProjectName=meu-projeto \
  --capabilities CAPABILITY_NAMED_IAM \
  --profile dev

# Ver os outputs após o deploy
aws cloudformation describe-stacks \
  --stack-name meu-projeto-infra \
  --query 'Stacks[0].Outputs' \
  --output table

# Ver todos os exports disponíveis na conta/região
aws cloudformation list-exports --query 'Exports[*].[Name,Value]' --output table

Common pitfalls

1. Using Ref on IAM Roles when you need the ARN

!Ref MyRole returns the role name, not the ARN. When you pass this to a field that expects an ARN (like RoleArn of an ECS TaskDefinition), CloudFormation accepts the template but the service returns an error at runtime. Always use !GetAtt MyRole.Arn for ARNs.

2. Deleting a stack with exports in use

If another stack uses !ImportValue "minha-stack-BucketArn", you cannot delete minha-stack nor modify that output. CloudFormation returns Export cannot be updated as it is in use by another stack. To resolve: first remove the ImportValue from the consuming stack, deploy it, then delete or modify the exporting stack.

3. Unnecessary DependsOn

When you use !Ref or !GetAtt from one resource in another, CloudFormation already infers the dependency automatically. Explicit DependsOn is only necessary when a resource depends on another without directly referencing it — for example, a Lambda function that queries an endpoint that only exists after an EC2 instance is running.


Reflection exercise

You are migrating an existing architecture to CloudFormation and need to split the infra into two stacks: infra-base (VPC, subnets, security groups) and infra-app (ECS service, ALB, IAM roles).

The infra-app stack needs to reference the VPC ID and subnet IDs created by infra-base. One colleague proposes using Export/ImportValue. Another proposes using SSM Parameter Store as an intermediary (infra-base writes the values to SSM, infra-app reads them via AWS::SSM::Parameter::Value).

What are the real trade-offs of each approach? Consider: stack lifecycle, ease of debugging, future refactoring operations, and what happens when you need to recreate infra-base from scratch in a new account. Which would you choose and why?


Resources for further study

Template anatomy and sections:
- CloudFormation template sections — complete reference of all sections with examples for each field.

Intrinsic functions:
- Intrinsic function reference — lists all functions with what they return per resource type. Check here whenever you have doubts about what Ref returns for a specific resource.

Cross-stack references:
- Refer to resource outputs in another CloudFormation stack — complete walkthrough of Export/ImportValue with documented failure scenarios.

cfn-lint:
- cfn-lint on GitHub — static linter for CloudFormation templates. Detects wrong types, invalid references, and nonexistent properties before deploy. Install with pip install cfn-lint and run with cfn-lint template.yaml.