Skip to main content

☁️ AWS CloudFormation

Terraform speaks every cloud. But when you live entirely inside AWS, its native IaC service earns a serious look: CloudFormation manages your state for you, rolls back automatically on failure, and integrates with every AWS feature the day it launches.

Week 13 · Wednesday: Infrastructure as Code · Lecture 3

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what CloudFormation is and how it differs from Terraform
  • Structure a template in YAML with parameters, resources, and outputs
  • Describe what a stack is and how AWS manages its lifecycle and state
  • Use core intrinsic functions like !Ref, !GetAtt, and !Sub
  • Preview changes safely with change sets and catch manual edits with drift detection
  • Decide when CloudFormation or Terraform is the better fit

Estimated Time: 65 minutes

Practice: Read a template, resolve intrinsic functions by hand, and reason about change sets.

In This Lesson

What Is CloudFormation?

AWS CloudFormation is Amazon's native Infrastructure as Code service. You describe the AWS resources you want in a declarative template — written in YAML or JSON — and CloudFormation provisions and manages them as a single unit called a stack. Launched in 2011, it was one of the first IaC services from a major cloud provider and supports essentially every AWS resource type.

The workflow mirrors what you already know from Terraform, with one big convenience: AWS manages the state for you. There's no state file to store, encrypt, or lock — the CloudFormation service tracks everything on its side.

graph LR A["Author template
YAML or JSON"] --> B["Create stack"] B --> C["CloudFormation service"] C --> D["Provision AWS resources"] D --> E["Stack created"] E --> F["Update or delete via new template"] F --> C

🍳 The recipe-and-kitchen analogy

  • Templates are recipes: they list every ingredient and step to create a dish.
  • Stacks are the finished dishes — a set of resources working together.
  • Parameters are customer preferences that tweak the recipe, like "extra spicy."
  • Change sets are the chef reviewing exactly what will change before touching the plate.

A standardized recipe gives consistent dishes every time — just as a template gives consistent infrastructure across environments.

CloudFormation vs. Terraform

Both are declarative IaC tools, but they make different trade-offs. Knowing the split helps you choose — and many teams use both.

FeatureAWS CloudFormationTerraform
Cloud supportAWS onlyMulti-cloud (AWS, Azure, GCP, and more)
LanguageYAML or JSONHCL
StateManaged by AWS — nothing to storeYou manage the state file
Change previewChange setsterraform plan
RollbackAutomatic on failureManual / custom automation
Drift detectionBuilt inVia plan / refresh
New AWS featuresOften available immediatelyMay lag until the provider updates

💡 A simple rule of thumb

If your world is 100% AWS and you value automatic rollback plus zero state management, CloudFormation is a natural fit. If you span multiple clouds or prefer HCL's concise syntax and huge module ecosystem, Terraform wins. Neither choice is wrong — they solve the same problem from different angles.

Templates & Stacks

A template is a text file describing your resources. A stack is what you get when CloudFormation instantiates that template — a managed collection of real AWS resources you create, update, and delete together.

Anatomy of a template

Here's a minimal but complete YAML template that creates one EC2 instance:

AWSTemplateFormatVersion: '2010-09-09'
Description: A simple EC2 instance

Parameters:
  InstanceType:
    Description: EC2 instance size
    Type: String
    Default: t3.micro
    AllowedValues: [t3.micro, t3.small, t3.medium]

Resources:
  WebServer:
    Type: AWS::EC2::Instance        # the resource type
    Properties:
      InstanceType: !Ref InstanceType   # pull the parameter value
      ImageId: ami-0c55b159cbfafe1f0
      Tags:
        - Key: Name
          Value: WebServer

Outputs:
  InstanceId:
    Description: ID of the created instance
    Value: !Ref WebServer

Every template shares this shape: an optional Parameters section for inputs, a required Resources section describing what to build, and an optional Outputs section exposing useful values.

A stack groups resources into one unit

graph TD A["Stack: my-web-app"] --> B["EC2 instance"] A --> C["Security group"] A --> D["S3 bucket"] A --> E["RDS database"] B --> F["Managed together:
create, update, delete as one"] C --> F D --> F E --> F

Because a stack is one unit, deleting it cleanly removes every resource it created — no orphaned servers quietly running up your bill. And if a resource fails to create, CloudFormation automatically rolls the whole stack back to its last good state.

Managing stacks from the CLI

# Create a stack from a template file
aws cloudformation create-stack \
  --stack-name my-web-app \
  --template-body file://webapp.yaml \
  --parameters ParameterKey=InstanceType,ParameterValue=t3.small

# Delete a stack (removes ALL its resources)
aws cloudformation delete-stack --stack-name my-web-app

# Read a stack's outputs
aws cloudformation describe-stacks \
  --stack-name my-web-app \
  --query "Stacks[0].Outputs"

Parameters & Outputs

Parameters let you pass custom values in at stack-creation time — the same template can build a tiny dev stack or a beefy production one. Outputs expose values after the stack is built.

Parameters with types and constraints

Parameters:
  # String with a fixed set of allowed values
  Environment:
    Description: Deployment environment
    Type: String
    Default: Development
    AllowedValues: [Development, Staging, Production]

  # Number with min/max bounds
  WebServerCapacity:
    Description: Desired number of web servers
    Type: Number
    Default: 2
    MinValue: 1
    MaxValue: 10

  # AWS-specific type: the console shows a dropdown of real VPCs
  VpcId:
    Description: VPC to deploy into
    Type: AWS::EC2::VPC::Id

  # Regex-constrained string
  DBUsername:
    Description: Database admin username
    Type: String
    MinLength: 1
    MaxLength: 16
    AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*"
    ConstraintDescription: must start with a letter, alphanumeric only

✅ AWS-specific parameter types are a gift

Types like AWS::EC2::VPC::Id or AWS::EC2::KeyPair::KeyName make the console render a dropdown of your real resources and validate the choice before the stack even starts. Fewer typos, fewer failed deploys.

Outputs — and cross-stack exports

Outputs:
  WebsiteURL:
    Description: Public URL of the site
    Value: !Sub "http://${WebServer.PublicDnsName}"

  BucketArn:
    Description: ARN of the data bucket
    Value: !GetAtt DataBucket.Arn
    Export:                              # make it importable by other stacks
      Name: !Sub "${AWS::StackName}-BucketArn"

The Export block publishes a value other stacks can pull in with !ImportValue — the CloudFormation way to wire separate stacks together.

Intrinsic Functions

Templates aren't just static lists — intrinsic functions add references and lightweight logic. In YAML they use the short !Name form. These four cover the vast majority of real templates.

FunctionWhat it doesExample
!RefReference a parameter or resource!Ref InstanceType
!GetAttRead an attribute of a resource!GetAtt WebServer.PublicIp
!SubSubstitute variables into a string!Sub "http://${WebServer.PublicDnsName}"
!JoinJoin a list with a delimiter!Join ["-", [prod, app]]
Resources:
  DataBucket:
    Type: AWS::S3::Bucket
    Properties:
      # !Sub injects the stack name and region into the bucket name
      BucketName: !Sub "${AWS::StackName}-data-${AWS::Region}"

  ReadPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: !Sub "${AWS::StackName}-read"
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action: s3:GetObject
            # !GetAtt reads the bucket's ARN attribute
            Resource: !GetAtt DataBucket.Arn

💡 Pseudo-parameters come free

Values like AWS::StackName, AWS::Region, and AWS::AccountId are provided automatically. Combined with !Sub, they let you build unique, region-aware names without hardcoding anything.

Conditional resources

The Conditions section plus !If let a single template behave differently per environment:

Conditions:
  IsProduction: !Equals [!Ref Environment, Production]

Resources:
  Database:
    Type: AWS::RDS::DBInstance
    Properties:
      Engine: postgres
      # Production gets Multi-AZ high availability; others don't
      MultiAZ: !If [IsProduction, true, false]
      BackupRetentionPeriod: !If [IsProduction, 30, 7]

Change Sets & Drift

Two features make CloudFormation safe to run against production: change sets let you preview updates, and drift detection catches sneaky manual edits.

Change sets — CloudFormation's "plan"

A change set shows exactly how a proposed template update would affect your running resources before you execute it — the CloudFormation equivalent of terraform plan.

sequenceDiagram participant U as User participant CF as CloudFormation participant AWS as AWS Resources U->>CF: Create change set from new template CF->>CF: Compare proposed state with current state CF->>U: Show planned additions changes and replacements U->>CF: Review then execute the change set CF->>AWS: Apply the changes in order AWS->>CF: Report status CF->>U: Return the results
# Create a change set to preview an update
aws cloudformation create-change-set \
  --stack-name my-web-app \
  --change-set-name add-cache \
  --template-body file://webapp-v2.yaml

# Review it, then execute if it looks right
aws cloudformation execute-change-set \
  --stack-name my-web-app \
  --change-set-name add-cache

⚠️ Watch for "Replacement"

Some property changes can't be done in place — CloudFormation must replace the resource, deleting the old one and creating a new one. For a database or stateful resource, replacement can mean data loss. Always scan a change set for Replacement: True before executing against production.

Drift detection — catch the hand edits

If someone changes a resource in the console instead of through the template, the stack has drifted. CloudFormation can detect this so you can reconcile the template with reality.

# Kick off drift detection for a stack
aws cloudformation detect-stack-drift --stack-name my-web-app

# See which resources have drifted
aws cloudformation describe-stack-resource-drifts --stack-name my-web-app

📖 The lesson repeats for a reason

Whether it's Terraform state or a CloudFormation stack, the rule is the same: the template is the source of truth, and manual edits are drift. Change sets and drift detection are the guardrails that keep that promise honest.

Practice & Quiz

🏋️ Exercise 1: Resolve the functions by hand

Goal: The stack is named shop and deployed in us-east-1. What final string does this produce?

BucketName: !Sub "${AWS::StackName}-data-${AWS::Region}"
💡 Hint

!Sub replaces each ${...} with its value. AWS::StackName and AWS::Region are pseudo-parameters filled in automatically.

✅ Solution

It resolves to shop-data-us-east-1. The stack name and region are substituted in, producing a unique, region-aware bucket name with no hardcoded values.

🏋️ Exercise 2: Add a conditional output

Goal: Given a condition IsProduction, write an output BackupBucketName that returns !Ref BackupBucket in production and the literal string "none" otherwise.

✅ Solution
Outputs:
  BackupBucketName:
    Description: Backup bucket, if one was created
    Value: !If [IsProduction, !Ref BackupBucket, "none"]

!If takes a condition and two values, returning the first when the condition is true. This is how one template adapts to many environments.

🎯 Quick Quiz

Question 1: What is a CloudFormation stack?

Question 2: Which CloudFormation feature is the equivalent of terraform plan?

Question 3: A key advantage of CloudFormation over Terraform for an all-AWS shop is:

Best Practices & Pitfalls

✅ Do

  • Prefer YAML — it's more readable than JSON and supports comments
  • Validate templates with aws cloudformation validate-template before deploying
  • Always create and review a change set for production updates
  • Parameterize templates so one file serves dev, staging, and production
  • Reference secrets via dynamic references to Secrets Manager, never inline

❌ Don't

  • Hardcode passwords or keys in a template
  • Execute an update without checking for Replacement: True on stateful resources
  • Edit stack resources by hand in the console — that causes drift
  • Skip stack policies on production — protect critical resources from accidental replacement

⚠️ Pull secrets in dynamically

Resources:
  Database:
    Type: AWS::RDS::DBInstance
    Properties:
      Engine: postgres
      MasterUsername: admin
      # Resolved from Secrets Manager at deploy time — never in the template
      MasterUserPassword: '{{resolve:secretsmanager:prod/db:SecretString:password}}'

Dynamic references keep secrets out of your template and out of version control, the same discipline you applied to Terraform state.

Summary

🎉 Key Takeaways

  • CloudFormation is AWS-native IaC using YAML/JSON templates deployed as stacks
  • AWS manages state for you and rolls back automatically on failure
  • Parameters customize a template per environment; outputs expose and export values
  • Intrinsic functions!Ref, !GetAtt, !Sub, !If — add references and logic
  • Change sets preview updates safely; drift detection catches manual edits
  • Choose CloudFormation for all-AWS estates, Terraform for multi-cloud — both keep the template as the source of truth

📚 Additional Resources

🚀 What's Next?

You can now provision infrastructure with both a provider-agnostic tool and an AWS-native one. Once infrastructure is running, you need to watch it: the next lesson is Application Monitoring — metrics, logs, dashboards, and alerts that tell you your systems are healthy.

🎉 IaC unlocked!

Terraform and CloudFormation in your toolkit means you can build, version, and rebuild entire environments on command.