All of the project's infrastructure versioned as YAML code.

About this module

This is the most transformative module of the course. Everything you created manually in the previous modules will turn into version-controlled YAML files. After this, you'll never want to click through a console to create resources again.

Level: Intermediate · Estimated duration: 4-5 hours


Table of Contents

  1. Why Infrastructure as Code?
  2. Anatomy of a template
  3. Resources, Parameters, Outputs
  4. Stacks: creating, updating, deleting
  5. Template for DynamoDB
  6. Template for IAM
  7. Template for Beanstalk
  8. Nested stacks and composition
  9. Best practices and pitfalls
  10. Migrating what already exists
  11. Practice exercises
  12. Next steps

01. Why Infrastructure as Code?

So far you've created tables, roles, and environments by clicking through the console. It worked. But what happens if you need to recreate everything tomorrow, in another account, in another region? Or if you want to know what the state of the infrastructure was 30 days ago?

The problem with clickops

  • It's not reproducible — you can't recreate exactly what you did.
  • It's not versioned — there's no history of what changed and when.
  • It's slow — creating 30 resources manually takes hours; via code, seconds.
  • It's error-prone — forgetting a policy, a tag, a configuration.
  • It's not auditable — who changed what and why?
  • It's fragile — a person leaves the team and takes the infrastructure knowledge with them.

The IaC proposal

All infrastructure is described in declarative text files that live in Git, alongside the application code. To create/update the infrastructure, you apply these files via a command.

It's a radically different model, and once you get used to it, going back to clicking through a console feels barbaric.

The tools on the market

ToolCharacteristics
CloudFormationNative to AWS, YAML/JSON, no cost
TerraformMulti-cloud, HCL, requires state management
AWS CDKProgrammatic (TypeScript, Python), generates CloudFormation
PulumiProgrammatic multi-cloud, similar to CDK
Serverless FrameworkSpecific to Lambda/serverless, simplified

02. Anatomy of a template

A CloudFormation template is a YAML (or JSON) file with a fixed structure. Let's dissect it.

Minimal skeleton

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Minha primeira stack'
 
Parameters:
  # parâmetros de entrada
 
Resources:
  # os recursos AWS a serem criados
 
Outputs:
  # valores expostos pela stack

The main sections

  • AWSTemplateFormatVersion — the format version. Always use '2010-09-09'.
  • Description — a free-form description of the stack.
  • Parameters — values configurable at deploy time (e.g., environment name).
  • Mappings — static lookup tables (e.g., AMI per region).
  • Conditions — conditions for whether or not to create certain resources.
  • Resourcesrequired section — the AWS resources to create.
  • Outputs — values exposed by the stack, useful for other stacks.

A simple complete template

AWSTemplateFormatVersion: '2010-09-09'
Description: 'S3 bucket com versionamento'
 
Parameters:
  BucketName:
    Type: String
    Description: 'Nome do bucket S3'
 
Resources:
  MyBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      VersioningConfiguration:
        Status: Enabled
      Tags:
        - Key: Project
          Value: order-system
 
Outputs:
  BucketArn:
    Description: 'ARN do bucket criado'
    Value: !GetAtt MyBucket.Arn

The famous !Ref and !GetAtt

  • !Ref — references another element (parameter, resource). For a resource, it usually returns the main ID/name.
  • !GetAtt <Resource>.<Attribute> — gets a specific attribute of a resource (e.g., ARN, URL).
  • !Sub — variable substitution in a string.
  • !Join — string concatenation.

03. Resources, Parameters, Outputs

Let's go deeper into the three sections you'll use most.

Resources: the heart of the template

Each resource has a logical name (you choose it), a type (AWS::<Service>::<Resource>), and properties specific to that type.

Resources:
  OrdersTable:                          # nome lógico (escolhido por você)
    Type: AWS::DynamoDB::Table          # tipo do recurso
    Properties:                         # propriedades específicas
      TableName: Orders
      AttributeDefinitions:
        - AttributeName: orderId
          AttributeType: S
      KeySchema:
        - AttributeName: orderId
          KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5

Parameters: making the template configurable

Parameters:
  EnvironmentName:
    Type: String
    Default: dev
    AllowedValues: [dev, staging, prod]
    Description: 'Ambiente de deploy'
 
  ReadCapacity:
    Type: Number
    Default: 5
    MinValue: 1
    MaxValue: 100

Common types: String, Number, List<Number>, CommaDelimitedList, and special AWS types like AWS::EC2::VPC::Id that validate that the value exists in the account.

Outputs: exposing values

Outputs:
  OrdersTableName:
    Description: 'Nome da tabela criada'
    Value: !Ref OrdersTable
    Export:
      Name: !Sub '${AWS::StackName}-TableName'
 
  OrdersTableArn:
    Description: 'ARN da tabela'
    Value: !GetAtt OrdersTable.Arn

Outputs are visible in the console and can be imported by other stacks via !ImportValue. We'll use this to connect the DynamoDB stack with the IAM stack.

04. Stacks: creating, updating, deleting

A stack is an instance created from a template. It's the unit of deployment in CloudFormation.

The lifecycle of a stack

  • CREATE_IN_PROGRESS — the stack is being created — resources are being provisioned.
  • CREATE_COMPLETE — everything created successfully. ✓
  • CREATE_FAILED — something failed. CloudFormation rolls back automatically.
  • UPDATE_IN_PROGRESS — updates are being applied.
  • UPDATE_ROLLBACK_IN_PROGRESS — there was an error during the update; reverting to the previous state.
  • DELETE_IN_PROGRESS — the stack is being deleted — resources are being removed.
  • DELETE_COMPLETE — the stack and all resources deleted.

Creating the stack via CLI

aws cloudformation create-stack \
  --profile aws-curso \
  --stack-name orders-database \
  --template-body file://orders-table.yaml \
  --parameters \
    ParameterKey=EnvironmentName,ParameterValue=dev \
  --tags Key=Project,Value=order-system

Tracking progress

# Status atual
aws cloudformation describe-stacks \
  --profile aws-curso \
  --stack-name orders-database \
  --query "Stacks[0].StackStatus"
 
# Eventos detalhados (últimos)
aws cloudformation describe-stack-events \
  --profile aws-curso \
  --stack-name orders-database

Updating the stack

aws cloudformation update-stack \
  --profile aws-curso \
  --stack-name orders-database \
  --template-body file://orders-table.yaml \
  --parameters \
    ParameterKey=EnvironmentName,ParameterValue=dev

Deleting the stack

aws cloudformation delete-stack \
  --profile aws-curso \
  --stack-name orders-database

This command deletes all resources created by the stack. It's the cleanest way to "clean everything up" at the end of a study session.

05. Template for DynamoDB

Let's turn the Orders table (which you created in the console in Module 02) into a CloudFormation template. This exercise is the essence of migrating to IaC.

Template: orders-table.yaml

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Tabela DynamoDB para o serviço Orders'
 
Parameters:
  EnvironmentName:
    Type: String
    Default: dev
  ReadCapacity:
    Type: Number
    Default: 5
  WriteCapacity:
    Type: Number
    Default: 5
 
Resources:
  OrdersTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub 'Orders-${EnvironmentName}'
      BillingMode: PROVISIONED
      AttributeDefinitions:
        - AttributeName: orderId
          AttributeType: S
        - AttributeName: customerId
          AttributeType: S
        - AttributeName: createdAt
          AttributeType: S
        - AttributeName: status
          AttributeType: S
      KeySchema:
        - AttributeName: orderId
          KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: !Ref ReadCapacity
        WriteCapacityUnits: !Ref WriteCapacity

Continued — the GSIs:

      GlobalSecondaryIndexes:
        - IndexName: by-customer-index
          KeySchema:
            - AttributeName: customerId
              KeyType: HASH
            - AttributeName: createdAt
              KeyType: RANGE
          Projection:
            ProjectionType: ALL
          ProvisionedThroughput:
            ReadCapacityUnits: !Ref ReadCapacity
            WriteCapacityUnits: !Ref WriteCapacity
 
        - IndexName: by-status-index
          KeySchema:
            - AttributeName: status
              KeyType: HASH
            - AttributeName: createdAt
              KeyType: RANGE
          Projection:
            ProjectionType: ALL
          ProvisionedThroughput:
            ReadCapacityUnits: !Ref ReadCapacity
            WriteCapacityUnits: !Ref WriteCapacity
 
      Tags:
        - Key: Project
          Value: order-system
        - Key: Environment
          Value: !Ref EnvironmentName
 
Outputs:
  TableName:
    Value: !Ref OrdersTable
    Export:
      Name: !Sub '${AWS::StackName}-TableName'
  TableArn:
    Value: !GetAtt OrdersTable.Arn
    Export:
      Name: !Sub '${AWS::StackName}-TableArn'

06. Template for IAM

Let's create the IAM policy that grants access to the Orders table, and the Beanstalk instance role with that policy attached — all in a single template.

Template: orders-iam.yaml

AWSTemplateFormatVersion: '2010-09-09'
Description: 'IAM Role para o serviço Orders'
 
Parameters:
  DatabaseStackName:
    Type: String
    Description: 'Nome da stack que criou a tabela Orders'
 
Resources:
  OrdersServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: orders-service-instance-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier

Inline policy for DynamoDB access

      Policies:
        - PolicyName: dynamodb-orders-access
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:GetItem
                  - dynamodb:PutItem
                  - dynamodb:UpdateItem
                  - dynamodb:DeleteItem
                  - dynamodb:Query
                Resource:
                  - !ImportValue 
                      Fn::Sub: '${DatabaseStackName}-TableArn'
                  - !Sub
                    - '${TableArn}/index/*'
                    - TableArn: !ImportValue 
                        Fn::Sub: '${DatabaseStackName}-TableArn'
 
  OrdersInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: orders-service-instance-profile
      Roles:
        - !Ref OrdersServiceRole
 
Outputs:
  InstanceProfileArn:
    Value: !GetAtt OrdersInstanceProfile.Arn
    Export:
      Name: !Sub '${AWS::StackName}-ProfileArn'

Deploy in order

# 1. Crie a stack do banco primeiro
aws cloudformation create-stack \
  --profile aws-curso \
  --stack-name orders-database \
  --template-body file://orders-table.yaml
 
# 2. Aguarde concluir
aws cloudformation wait stack-create-complete \
  --profile aws-curso \
  --stack-name orders-database
 
# 3. Crie a stack do IAM, referenciando a primeira
aws cloudformation create-stack \
  --profile aws-curso \
  --stack-name orders-iam \
  --template-body file://orders-iam.yaml \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameters \
    ParameterKey=DatabaseStackName,ParameterValue=orders-database

--capabilities CAPABILITY_NAMED_IAM is required whenever the template creates IAM resources with a custom name. CloudFormation asks for explicit confirmation for security reasons.

07. Template for Beanstalk

Finally, the Beanstalk environment template. Here you'll see the real complexity — environments have many configuration options.

Template: orders-beanstalk.yaml (part 1)

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Beanstalk environment para Orders'
 
Parameters:
  EnvironmentName:
    Type: String
    Default: dev
  IamStackName:
    Type: String
  DatabaseStackName:
    Type: String
  SolutionStackName:
    Type: String
    Default: '64bit Amazon Linux 2023 v6.1.0 running Node.js 20'
 
Resources:
  OrdersApplication:
    Type: AWS::ElasticBeanstalk::Application
    Properties:
      ApplicationName: orders-service
      Description: 'Microserviço de Pedidos'

Template: orders-beanstalk.yaml (part 2)

  OrdersEnvironment:
    Type: AWS::ElasticBeanstalk::Environment
    Properties:
      ApplicationName: !Ref OrdersApplication
      EnvironmentName: !Sub 'orders-${EnvironmentName}'
      SolutionStackName: !Ref SolutionStackName
      OptionSettings:
        - Namespace: aws:autoscaling:launchconfiguration
          OptionName: IamInstanceProfile
          Value: !ImportValue
            Fn::Sub: '${IamStackName}-ProfileArn'
 
        - Namespace: aws:autoscaling:launchconfiguration
          OptionName: InstanceType
          Value: t2.micro
 
        - Namespace: aws:elasticbeanstalk:environment
          OptionName: EnvironmentType
          Value: SingleInstance
 
        - Namespace: aws:elasticbeanstalk:application:environment
          OptionName: DYNAMODB_TABLE_NAME
          Value: !ImportValue
            Fn::Sub: '${DatabaseStackName}-TableName'
 
        - Namespace: aws:elasticbeanstalk:application:environment
          OptionName: AWS_REGION
          Value: !Ref AWS::Region
 
        - Namespace: aws:elasticbeanstalk:cloudwatch:logs
          OptionName: StreamLogs
          Value: true

Deploying the code (separately)

The template creates the empty environment. To deploy the code, use the aws elasticbeanstalk create-application-version command and then update-environment. Keeping code and infrastructure separate is a best practice: infrastructure changes rarely, the code every day.

08. Nested stacks and composition

A single giant template is hard to maintain. As the system grows, you break it into multiple stacks that reference each other — or you use nested stacks, where a parent stack orchestrates several children.

Two composition strategies

StrategyAdvantagesDisadvantages
Multiple stacks + ImportsIndependent lifecycle, each stack standaloneRequires a deploy order
Nested stacksA single deploy handles everythingStrong coupling, slow deploy

For our project we'll use multiple stacks — one for the database, one for IAM, one for the Beanstalk of each service. This mirrors the logical division of the system.

infra/
├── databases/
│   ├── orders-table.yaml
│   ├── inventory-table.yaml
│   └── payments-table.yaml
├── iam/
│   ├── orders-iam.yaml
│   ├── inventory-iam.yaml
│   └── payments-iam.yaml
├── beanstalk/
│   ├── orders-beanstalk.yaml
│   ├── inventory-beanstalk.yaml
│   └── payments-beanstalk.yaml
└── deploy.sh           # script que orquestra a ordem

Deploy script

#!/bin/bash
set -e
 
PROFILE=aws-curso
 
deploy_stack() {
  local STACK_NAME=$1
  local TEMPLATE=$2
  shift 2
  echo "Deploying ${STACK_NAME}..."
  aws cloudformation deploy \
    --profile $PROFILE \
    --stack-name $STACK_NAME \
    --template-file $TEMPLATE \
    --capabilities CAPABILITY_NAMED_IAM \
    --no-fail-on-empty-changeset \
    "$@"
}
 
# Camada 1: bancos
deploy_stack orders-db    databases/orders-table.yaml
deploy_stack inventory-db databases/inventory-table.yaml
deploy_stack payments-db  databases/payments-table.yaml
 
# Camada 2: IAM (depende dos bancos)
deploy_stack orders-iam    iam/orders-iam.yaml \
  --parameter-overrides DatabaseStackName=orders-db
# ... análogo para inventory e payments

09. Best practices and pitfalls

Principles that will save you headaches as the project grows.

Best practices

  • Version everything in Git — the template is code.
  • Use Change Sets before an update in production — see before you apply.
  • Small, focused templates — one stack per infra "domain."
  • Outputs with Export — makes composing stacks easier.
  • Tags on all resources — to find and organize costs.
  • Parameters with Default — makes the template usable without much typing.
  • Description on all important resources — inline documentation.
  • Use aws cloudformation validate-template before deploying.

Common pitfalls

  • Hardcoded ARNs — always use !Ref, !GetAtt, pseudo-parameters.
  • Missing DeletionPolicy on resources with data — one accidental deletion and the data is gone.
  • Giant templates — they become impossible to review.
  • Ignoring drift — someone changes something in the console and the template falls out of sync.
  • Forgetting CAPABILITY_NAMED_IAM — an obvious failure on named IAM resources.
  • Circular dependencies — A imports from B, B imports from A.

DeletionPolicy: protecting data

OrdersTable:
  Type: AWS::DynamoDB::Table
  DeletionPolicy: Retain     # NÃO deleta a tabela ao deletar a stack
  UpdateReplacePolicy: Retain # idem em casos de replace
  Properties:
    # ...

Retain: the resource remains even if the stack is deleted. Snapshot: takes a snapshot before deleting (RDS, EBS). Delete: the default, deletes it. For tables with production data, always Retain.

10. Migrating what already exists

You already have tables, roles, and environments created manually. How do you move all of that into CloudFormation without losing anything?

  1. Write the template that describes the resources as you want them to be.
  2. Use CloudFormation's Import feature to "adopt" the existing resources.
  3. Check for drift after the import to see the differences.
  4. Adjust the template to reflect the real state, or apply corrections.

Alternative: new stack, old ones discarded

For a study project, it's simpler: delete the old resources via console, then create everything via CloudFormation. You lose the test data (which is fictional anyway), but you gain a 100% IaC system from the start.

Detecting drift

# Inicia detecção
aws cloudformation detect-stack-drift \
  --profile aws-curso \
  --stack-name orders-database
 
# Aguarda terminar e mostra resultado
aws cloudformation describe-stack-resource-drifts \
  --profile aws-curso \
  --stack-name orders-database

If the status returns DRIFTED, someone touched the resource outside of CloudFormation. You need to decide: bring the change into the template, or revert the resource to the template's state.

11. Practice exercises

Exercício
01

Migrate the Orders table to CFN

Delete the manually created Orders table. Write the orders-table.yaml template following section 5 and deploy it via aws cloudformation deploy. Confirm: is the table created with the same GSIs? Are the tags correct?

Exercício
02

Create templates for Inventory and Payments

Apply the same pattern for the other two databases. You should have three separate templates, three separate stacks, with Outputs and Exports. Confirm via the console that everything was created correctly.

Exercício
03

Add DeletionPolicy

Modify the Orders template by adding DeletionPolicy: Retain and UpdateReplacePolicy: Retain. Update the stack. Then try to delete the stack. What happens? Why? (Afterwards, delete the table manually to clean up.)

Exercício
04

Complete IAM stack

Write and deploy the orders-iam stack following section 6. Verify: (a) the role was created; (b) the policy allows only the right actions; (c) the instance profile exists and is associated with the role.

Exercício
05

Complete deploy script

Write the deploy.sh that creates, in order, all the stacks: 3 databases + 3 IAMs + 3 beanstalks. Test it by running in a clean account (after deleting everything). Expected total time: 15-20 minutes to create the entire system.

12. Next steps

This module is a turning point. Before it, you created infrastructure by clicking. Now it lives in code, it's versioned, it's reproducible. Never back to clickops.

What you learned

  • Why IaC is non-negotiable in serious systems.
  • The anatomy of CloudFormation templates: Resources, Parameters, Outputs.
  • Intrinsic functions: !Ref, !GetAtt, !Sub, !ImportValue.
  • The lifecycle of stacks: create, update, delete.
  • Concrete templates for DynamoDB, IAM, and Beanstalk.
  • Composing stacks with Imports/Exports.
  • Best practices, pitfalls, DeletionPolicy.
  • How to migrate manual resources to IaC.

What's coming in Module 05

Messaging: SNS and SQS. We'll implement asynchronous communication between Orders and Inventory — when an order is created, an event is published to SNS, an SQS queue consumes it, and Inventory decrements the stock. It's the fundamental pattern of resilient distributed systems.

Infrastructure is code. Enjoy the journey — Module 05 is next.