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
- Why Infrastructure as Code?
- Anatomy of a template
- Resources, Parameters, Outputs
- Stacks: creating, updating, deleting
- Template for DynamoDB
- Template for IAM
- Template for Beanstalk
- Nested stacks and composition
- Best practices and pitfalls
- Migrating what already exists
- Practice exercises
- 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
| Tool | Characteristics |
|---|---|
| CloudFormation | Native to AWS, YAML/JSON, no cost |
| Terraform | Multi-cloud, HCL, requires state management |
| AWS CDK | Programmatic (TypeScript, Python), generates CloudFormation |
| Pulumi | Programmatic multi-cloud, similar to CDK |
| Serverless Framework | Specific 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 stackThe 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.
- Resources — required 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.ArnThe 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: 5Parameters: 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: 100Common 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.ArnOutputs 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-systemTracking 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-databaseUpdating the stack
aws cloudformation update-stack \
--profile aws-curso \
--stack-name orders-database \
--template-body file://orders-table.yaml \
--parameters \
ParameterKey=EnvironmentName,ParameterValue=devDeleting the stack
aws cloudformation delete-stack \
--profile aws-curso \
--stack-name orders-databaseThis 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 WriteCapacityContinued — 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/AWSElasticBeanstalkWebTierInline 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: trueDeploying 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
| Strategy | Advantages | Disadvantages |
|---|---|---|
| Multiple stacks + Imports | Independent lifecycle, each stack standalone | Requires a deploy order |
| Nested stacks | A single deploy handles everything | Strong 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.
Recommended folder structure
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 payments09. 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?
Recommended strategy
- Write the template that describes the resources as you want them to be.
- Use CloudFormation's Import feature to "adopt" the existing resources.
- Check for drift after the import to see the differences.
- 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-databaseIf 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
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?
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.
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.)
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.
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.
