Writing a CloudFormation Template
A CloudFormation template is a text file (written in YAML or JSON) that describes the AWS resources you want — like servers, databases, and networks — and how they connect. AWS reads this file and builds everything for you, so your infrastructure becomes code you can version, review, and reuse. This page walks through the anatomy of a template, the small helper functions that make it flexible, and how to deploy it from both the AWS Management Console (the web dashboard) and the AWS Command Line Interface (AWS CLI, the terminal tool). We will use YAML throughout because it is easier to read than JSON.
Template anatomy
A template is made of several top-level sections. Only Resources is required; the rest are optional but make your template far more powerful. Here is what each one does and when to reach for it.
| Section | Purpose | When to use |
|---|---|---|
Parameters | Inputs you supply at deploy time | When the same template should work for dev, staging, and prod |
Mappings | Fixed lookup tables (key to value) | When a value depends on region or environment but is known in advance |
Conditions | True/false rules that turn resources on or off | When prod needs extra resources that dev does not |
Resources | The actual AWS things to create (required) | Always |
Outputs | Values to return after the stack is built | When you need an IP, URL, or ID to use elsewhere |
A “stack” is simply the running result of a deployed template — all the resources managed together as one unit.
Parameters
Parameters let you pass values in at deploy time instead of hard-coding them. This means one template can build many environments.
Parameters:
EnvironmentName:
Type: String
AllowedValues: [dev, staging, prod]
Default: dev
Description: Which environment this stack is for
InstanceType:
Type: String
Default: t3.micro
When to use: any value that changes between deployments (sizes, names, counts). When NOT to use: secrets like passwords — put those in AWS Secrets Manager and reference them, never as a plain parameter default.
Mappings
A mapping is a static lookup table. The most common use is picking the right Amazon Machine Image (AMI — a prebuilt server image) per region, because an AMI ID is different in every region.
Mappings:
RegionToAMI:
us-east-1:
AMI: ami-0abcdef1234567890
eu-west-1:
AMI: ami-0123456789abcdef0
When to use: values that are known ahead of time and vary by a fixed key like region. When NOT to use: values a user should choose — use a parameter instead.
Conditions
Conditions create true/false flags from parameters, then attach them to resources so they are only built when the flag is true.
Conditions:
IsProd: !Equals [!Ref EnvironmentName, prod]
Resources
This is the heart of the template. Each resource has a logical name (your label), a Type (the AWS service), and Properties (its settings).
Resources:
AppServer:
Type: AWS::EC2::Instance
Properties:
InstanceType: !Ref InstanceType
ImageId: !FindInMap [RegionToAMI, !Ref "AWS::Region", AMI]
Tags:
- Key: Name
Value: !Sub "${EnvironmentName}-app-server"
AppDatabase:
Type: AWS::RDS::DBInstance
DeletionPolicy: Retain
Properties:
Engine: postgres
DBInstanceClass: db.t3.micro
AllocatedStorage: "20"
Condition: IsProd
Outputs
Outputs return useful values after the build, such as a server ID or a public address you need for the next step.
Outputs:
ServerId:
Description: The EC2 instance ID
Value: !Ref AppServer
ServerPublicIp:
Value: !GetAtt AppServer.PublicIp
Intrinsic functions
Intrinsic functions are small built-in helpers that compute values while the stack is building. The three you will use most:
| Function | Short form | What it returns | Example |
|---|---|---|---|
Ref | !Ref | The main ID/value of a resource or parameter | !Ref AppServer gives the instance ID |
Fn::GetAtt | !GetAtt | A specific attribute of a resource | !GetAtt AppServer.PublicIp |
Fn::Sub | !Sub | A string with variables filled in | !Sub "${EnvironmentName}-bucket" |
Use Ref for the obvious identifier, GetAtt when you need a detail like a private IP or Amazon Resource Name (ARN, a unique ID for an AWS resource), and Sub to build names and scripts from other values.
Deploying the template
Using the Console
- Open the AWS Management Console and go to the CloudFormation service.
- Click Create stack, then With new resources (standard).
- Under Specify template, choose Upload a template file and select your
.yamlfile. - Click Next, give the stack a name (for example
app-stack-dev), and fill in the parameter values. - Click Next through the options, check the acknowledgment box if it asks about creating IAM resources, then click Submit.
- Watch the Events tab — when the status reads
CREATE_COMPLETE, your resources are live.
Using the AWS CLI
aws cloudformation deploy \
--template-file app-stack.yaml \
--stack-name app-stack-dev \
--parameter-overrides EnvironmentName=dev InstanceType=t3.micro \
--capabilities CAPABILITY_NAMED_IAM
Output:
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - app-stack-dev
To see your outputs after deploy:
aws cloudformation describe-stacks \
--stack-name app-stack-dev \
--query "Stacks[0].Outputs"
Output:
[
{
"OutputKey": "ServerId",
"OutputValue": "i-0a1b2c3d4e5f"
},
{
"OutputKey": "ServerPublicIp",
"OutputValue": "52.23.45.67"
}
]
The “update requires replacement” gotcha
When you change a property and update the stack, CloudFormation does one of three things: an in-place update (safe), an update with some interruption (a reboot), or a replacement — it creates a brand-new resource and deletes the old one. Replacement is the dangerous one. Changing the ImageId on an EC2 instance, or the DBName on a database, forces a replacement. That means the old EC2 instance is destroyed (its public IP changes) or, worse, your database is deleted along with its data.
Every property in the AWS documentation has an “Update requires” note: No interruption, Some interruption, or Replacement. Always read it before changing a property on a live stateful resource.
Warning: Set
DeletionPolicy: Retain(andUpdateReplacePolicy: Retain) on databases, S3 buckets, and anything else holding data. This tells CloudFormation to keep the resource even if the stack would otherwise delete or replace it — your safety net against accidental data loss.
Tip: Run
aws cloudformation deploywithout--no-execute-changesetremoved, or create a change set first, to preview exactly what will change. A change set lists every action — including anyReplacement: True— before you commit. An accidental RDS replacement can mean hours of downtime and lost data, so this preview is free insurance.
Cost note: CloudFormation itself is free — you pay only for the resources it creates. A t3.micro EC2 instance runs about 0.0104 USD per hour (around 7.50 USD per month) in us-east-1, and a db.t3.micro PostgreSQL database about 0.017 USD per hour. Deleting the stack removes those resources and stops the charges.
Best Practices
- Keep
Resourcesminimal and split large systems into multiple smaller stacks that are easier to reason about and update. - Use
Parametersfor anything that varies between environments, andMappingsonly for fixed, known-ahead values. - Always check the “Update requires” note before changing a property on a live database or instance.
- Add
DeletionPolicy: RetainandUpdateReplacePolicy: Retainto every stateful resource (databases, buckets, volumes). - Preview changes with a change set before applying them to production stacks.
- Store templates in version control (Git) and review changes like any other code.
- Tag resources with
!Subso every environment’s resources are clearly named and easy to find.