Write CloudFormation Templates
Write CloudFormation templates for AWS resources — AWS Cloud & DevOps with Python.
Focus: write cloudformation templates for aws resources
You’ve built the Python app, wrapped it in a container, and pushed it to ECR. Now you need a repeatable, reviewable way to spin up the supporting AWS infrastructure — VPC, load balancer, database, permissions — without clicking through the console at 2 a.m. If you’ve ever deployed by hand or with a pile of shell scripts, you know the pain: drift between environments, missing security group rules, and "it worked on my machine" syndrome. This lesson shows you how to write CloudFormation templates for AWS resources — a single source of truth that turns your infrastructure into code you can version, review, and deploy with confidence.
The problem this lesson solves
Manual infrastructure management is fragile and slow. You or your team might be doing any of this today:
- Clicking through the AWS Management Console to create EC2 instances, S3 buckets, or RDS databases.
- Copy-pasting shell commands from an old README that no one fully trusts.
- Discovering that staging has a different security group than production, and no one knows why.
These approaches don’t scale. They introduce configuration drift, make audits painful, and turn rollbacks into archaeology. Infrastructure as Code (IaC) fixes this by describing your AWS resources in a declarative file. AWS CloudFormation is AWS’s native IaC service — it turns a template (JSON or YAML) into a stack, and then creates, updates, or deletes all the resources in that stack as a single unit.
Pro tip: If you’ve used Terraform, the mental shift is small. CloudFormation is AWS-native, deeply integrated with IAM and CloudTrail, and free (you only pay for the resources it creates).
Core concept / mental model
Think of a CloudFormation template as a blueprint for your cloud infrastructure. Just as an architect’s blueprint describes every wall, door, and electrical outlet, your template describes every AWS resource — its type, properties, and dependencies. CloudFormation acts as the general contractor that reads the blueprint and builds everything in the right order.
Here’s the high-level anatomy of a template:
- AWSTemplateFormatVersion — (optional) identifies the template version.
- Description — a human-readable summary.
- Parameters — inputs you pass at deployment time (e.g., environment name, instance type).
- Resources — the core section: every AWS resource you want to create.
- Outputs — values you want to expose after the stack is deployed (e.g., the endpoint URL).
The key insight is declarative management: you say what you want, not how to build it. CloudFormation determines the order — it knows an EC2 instance depends on its security group, so it creates the group first.
How it works step by step
Writing a CloudFormation template follows a predictable workflow. Let’s break it down:
-
Define the resource types. Each AWS resource has a logical name (your choice) and a
Type(e.g.,AWS::EC2::Instance,AWS::S3::Bucket). You’ll reference these types constantly. -
Set properties. Every resource type supports a set of properties. For an EC2 instance, you set
ImageId,InstanceType, andSecurityGroupIds. For an S3 bucket, you might setBucketNameandVersioningConfiguration. -
Wire dependencies with references. Use the
!Refintrinsic function to reference another resource’s logical name (which returns its ID or ARN), and!GetAttto fetch an attribute (like an instance’s public IP). -
Add parameters for flexibility. Instead of hardcoding values, use
Parametersso you can deploy the same template to dev, staging, and prod with different inputs. -
Declare outputs. Expose useful values like the database endpoint or load balancer DNS name so other stacks or scripts can consume them.
-
Validate and deploy. Use the AWS CLI or console to validate your template, then create a stack. CloudFormation handles provisioning, rollback on failure, and cleanup.
Hands-on walkthrough
Let’s put this into practice. We’ll write a template that creates an S3 bucket and an EC2 instance in a new VPC — a minimal but realistic setup.
Example 1: A simple S3 bucket
Start with the simplest possible template.
AWSTemplateFormatVersion: '2010-09-09'
Description: A simple S3 bucket
Resources:
MyBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: my-unique-bucket-name-12345
VersioningConfiguration:
Status: Enabled
Outputs:
BucketName:
Value: !Ref MyBucket
Description: Name of the created bucket
Save as s3-bucket.yaml. Deploy with AWS CLI:
aws cloudformation deploy \
--template-file s3-bucket.yaml \
--stack-name my-s3-stack
Pro tip:
aws cloudformation deployis the recommended CLI command — it handles change sets and rollbacks automatically. After deployment, you’ll see the bucket in your account, versioning enabled.
Example 2: EC2 with a security group and parameterized instance type
Now let’s make it more realistic: an EC2 instance that only allows SSH from your IP, with the instance type passed as a parameter.
AWSTemplateFormatVersion: '2010-09-09'
Description: EC2 instance with a security group
Parameters:
InstanceType:
Type: String
Default: t2.micro
AllowedValues:
- t2.micro
- t3.micro
Description: EC2 instance type
KeyName:
Type: AWS::EC2::KeyPair::KeyName
Description: Existing key pair to SSH into the instance
SSHLocation:
Type: String
Default: 0.0.0.0/0
Description: IP range allowed for SSH
Resources:
InstanceSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow SSH from specified IP
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: !Ref SSHLocation
MyEC2Instance:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0abcdef1234567890 # Replace with a valid AMI for your region
InstanceType: !Ref InstanceType
KeyName: !Ref KeyName
SecurityGroupIds:
- !Ref InstanceSecurityGroup
Outputs:
InstancePublicIP:
Value: !GetAtt MyEC2Instance.PublicIp
Description: Public IP of the EC2 instance
Deploy it:
aws cloudformation deploy \
--template-file ec2.yaml \
--stack-name my-ec2-stack \
--parameter-overrides InstanceType=t3.micro KeyName=my-key
Expected output: CloudFormation creates the security group first, then the instance, and prints the public IP in the stack outputs.
Example 3: Python-friendly — generate a template with a script
As a Python developer, you can leverage Python to generate or manage your templates. Here’s a small script that reads a YAML template and prints its resources — useful for validation or documentation.
import yaml
from pathlib import Path
def load_template(path: str) -> dict:
"""Load a CloudFormation template from YAML."""
with open(path) as f:
return yaml.safe_load(f)
if __name__ == "__main__":
template = load_template("ec2.yaml")
print("Resources in template:")
for logical_id, resource in template.get("Resources", {}).items():
print(f" - {logical_id}: {resource['Type']}")
Save as inspect_template.py, then run:
python inspect_template.py
Output:
Resources in template:
- InstanceSecurityGroup: AWS::EC2::SecurityGroup
- MyEC2Instance: AWS::EC2::Instance
This is a tiny demo, but in real projects you can use Python to lint templates, inject parameters, or even generate resource blocks programmatically.
Compare options / when to choose what
CloudFormation isn’t your only IaC option. Here’s how it stacks up against the most common alternatives:
| Tool | Language | AWS-native | Best for |
|---|---|---|---|
| CloudFormation | YAML/JSON | Yes | Teams fully on AWS; deep integration with IAM, CloudTrail, and StackSets |
| Terraform | HCL | No (multi-cloud) | Multi-cloud or existing HashiCorp workflows |
| AWS CDK | Python/TypeScript/etc. | Yes | Developers who prefer real programming languages over YAML |
| SAM | YAML (serverless extensions) | Yes | Serverless applications (Lambda, API Gateway) |
When to choose CloudFormation:
- You want a single tool for all AWS resources, not just serverless.
- You need native rollback and drift detection.
- You want to avoid managing state files (CloudFormation does it for you).
When to consider something else:
- You need multi-cloud support → Terraform.
- You want to write infrastructure in Python and use object-oriented patterns → AWS CDK (which still synthesizes CloudFormation templates!).
- You’re doing pure serverless → SAM is more concise.
Key point: No matter which you choose, understanding CloudFormation templates gives you a foundation. Even CDK compiles down to CloudFormation, so debugging a CDK deployment often means reading the generated template.
Troubleshooting & edge cases
Here are the most common problems you’ll hit when writing CloudFormation templates, and how to fix them.
1. "Template format error: YAML not well-formed"
This means your YAML has indentation or syntax errors. Check:
- Indentation must be consistent (spaces, not tabs).
- Colons must be followed by a space before the value (unless it’s a key at the end).
- Use a YAML linter or
python -c "import yaml, sys; yaml.safe_load(sys.stdin)"to validate.
2. "Resource creation cancelled" or stack rolls back
CloudFormation rolls back automatically if a resource fails. Common causes:
- Invalid AMI ID — AMIs are region-specific; your
ami-0abcdef...may not exist. Find a valid one withaws ec2 describe-images. - Insufficient IAM permissions — your deploy user must have
cloudformation:CreateStackand permissions for each resource. - Resource limit — e.g., you exceeded the VPC limit in the region.
Check the Events tab in the CloudFormation console — it shows exactly which resource failed and why.
3. "No updates are to be performed"
You deployed but nothing changed. This usually means you didn’t modify the template or parameters. CloudFormation compares changes and skips if nothing differs. Double-check your template file path and that you passed new parameter values.
4. !Ref is not a number
If you try to use !Ref where an integer is expected (e.g., Port), CloudFormation might complain because !Ref returns a string. Use the !Select or !GetAtt correctly, or pass the value as a parameter with the right Type (e.g., Type: Number).
5. Hardcoded values that break reuse
If you hardcode an S3 bucket name, it must be globally unique. Use !Sub '${AWS::StackName}-bucket' instead to avoid collisions.
What you learned & what's next
You now know how to write CloudFormation templates for AWS resources — the core skill for treating infrastructure as code on AWS. You learned:
- The anatomy of a template: Parameters, Resources, Outputs.
- How to define real resources like S3 buckets and EC2 instances.
- How to use intrinsic functions
!Refand!GetAttto wire resources together. - How to deploy with
aws cloudformation deploy. - How to compare CloudFormation with Terraform, CDK, and SAM.
- How to troubleshoot common template errors.
This foundational knowledge unlocks the next step in your DevOps journey: managing stacks at scale. Next, you’ll learn how to update stacks safely using change sets, handle drift detection, and structure large templates with nested stacks or modules. By mastering CloudFormation, you’re building the muscle you need for advanced infrastructure automation — and for tools like AWS CDK that sit on top of it.
Practice recap
Take the EC2 template from this lesson and extend it: add an S3 bucket and grant the EC2 instance an IAM role with read access to that bucket. Deploy it to your account, then try deleting the stack and confirm all resources are removed — that’s CloudFormation’s magic.
Common mistakes
- Hardcoding resource names that aren't globally unique (e.g., S3 bucket names) — causes deployment failures; use
!Sub '${AWS::StackName}-bucket'instead. - Forgetting that AMI IDs are region-specific — a template that works in us-east-1 fails in eu-central-1; avoid hardcoding AMIs, use SSM parameters.
- Misplacing indentation in YAML — a single tab or missing space makes the whole template invalid; validate with a linter before deploying.
- Not using
Parametersfor values that change between environments — you end up editing the template for every deployment instead of passing--parameter-overrides. - Assuming CloudFormation creates resources in the order you write them — it manages dependencies for you, but you must reference them with
!Refor!GetAttcorrectly or you'll get circular dependency errors.
Variations
- Use AWS CDK to define infrastructure in Python (or TypeScript) — it compiles to CloudFormation templates and adds programming constructs like loops and conditionals.
- Use Terraform if you need multi-cloud support or a different state management model — same concepts but HCL syntax.
- Use AWS SAM for serverless-only apps — it's a CloudFormation extension with simplified syntax for Lambda, API Gateway, and DynamoDB.
Real-world use cases
- Provision a full staging environment (VPC, EC2, RDS, load balancer) with one command and replicate it for production using the same template.
- Automate a secure S3 bucket with versioning and encryption for storing application backups — deployed as part of a CI/CD pipeline.
- Create a reusable template that spins up a bastion host and security groups for auditors to access private subnets temporarily.
Key takeaways
- CloudFormation turns infrastructure into code — declarative YAML/JSON templates that create, update, and delete AWS resources as a single unit.
- A template has four main sections: Parameters (inputs), Resources (what you create), Outputs (values you expose), and optional Conditions.
- Use
!Refto reference resource logical IDs and!GetAttto fetch attributes — this wires dependencies so CloudFormation builds in the right order. - AWS CloudFormation provides automatic rollback and drift detection, making it safer than manual console changes.
- While alternatives like Terraform and CDK exist, CloudFormation is the AWS-native baseline — even CDK generates it under the hood.
- Use parameters for anything that changes between environments — hardcoding values is the fastest path to failure.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.