37.1 CloudFormation Templates: JSON and YAML Structure
Alright, let’s talk about the blueprint itself: the CloudFormation template. This is the file you’ll be slinging around, and AWS, in its infinite wisdom, gives you two equally frustrating ways to write it: JSON and YAML.
I know, I know. Your first instinct is to recoil from JSON. It’s verbose, it hates comments (a truly criminal omission), and a single misplaced comma will ruin your entire afternoon. YAML, with its whitespace sensitivity, feels like the “better” option, and for the most part, it is. But be warned: YAML has its own dark arts, like anchors and aliases, that can make a simple template look like a mind-bending puzzle. My advice? Stick with straightforward YAML. It’s more human-readable, and you can actually leave notes for your future self (or the poor soul who has to maintain your code) using # comments.
The core thing to understand is that both formats, once parsed, result in exactly the same underlying data structure—a big bag of key-value pairs that AWS uses to build your stuff. The choice is purely about authoring experience.
The Absolute Non-Negotiables: AWSTemplateFormatVersion and Description
Every template, whether JSON or YAML, must start with AWSTemplateFormatVersion. It’s not a version of your template; it’s the version of the CloudFormation template language itself. As of my writing this, you’ll almost certainly just set it to '2010-09-09' and forget about it forever. It’s like a magic incantation. Just include it.
The Description is optional but, frankly, should be mandatory. Use it. Describe what this stack does. “Creates a VPC” is okay. “Creates the core production VPC with three tiers and NAT gateways” is better. Your memory is worse than you think.
AWSTemplateFormatVersion: '2010-09-09'
Description: 'A simple stack that proves I can run a server. Not for production. Seriously.'
Your Sandbox: Parameters and Pseudo Parameters
Parameters are how you make a template reusable. They’re the variables that someone (you, a CI/CD pipeline) can pass in at stack creation or update time. Things like instance types, environment names (dev, prod), or key pairs.
Then there are Pseudo Parameters. These are AWS’s gift to you—pre-defined parameters that are always available. You don’t declare them; you just reference them. They give you things like the AWS Region (AWS::Region) or Account ID (AWS::AccountId) you’re working in. Incredibly useful.
Parameters:
InstanceType:
Type: String
Default: t3.micro
AllowedValues:
- t3.micro
- t3.small
- m5.large
Description: 'EC2 instance type. Do not set to m5.metal unless you hate money.'
EnvironmentName:
Type: String
Default: dev
Description: 'Environment name used for resource tagging'
# Later, in the Resources section...
Resources:
MyInstance:
Type: AWS::EC2::Instance
Properties:
InstanceType: !Ref InstanceType # This gets the value from the parameter
ImageId: ami-12345678
Tags:
- Key: Environment
Value: !Ref EnvironmentName
- Key: Name
Value: !Sub "Web Server in ${AWS::Region}" # Using a pseudo parameter
The Main Event: The Resources Section
This is the heart of the template. It’s where you declare the AWS resources you want to create. Every resource has a logical ID (the name you give it inside the template, like MyInstance) and a type (like AWS::EC2::Instance).
The Properties section is the most important part. This is where you configure the resource. Getting this right is 90% of the battle. The properties you can set are dictated by the resource type. Pro tip: Bookmark the AWS Resource and Property Types Reference. It’s your bible. You will live there.
Here’s a brutally common pitfall: deletion policies. By default, when you delete a stack, CloudFormation will delete almost everything it created. This is fantastic for cleaning up test stacks and terrifying for production. If you have a database (an AWS::RDS::DBInstance) or an S3 bucket (AWS::S3::Bucket) with data you cannot lose, you must set a DeletionPolicy.
Resources:
MyProductionDatabase:
Type: AWS::RDS::DBInstance
DeletionPolicy: Retain # The most important line in this template
Properties:
DBInstanceClass: db.t3.small
Engine: mysql
MasterUsername: admin
MasterUserPassword: '{{resolve:secretsmanager:MySecret:SecretString:password}}' # Never hardcode passwords! Use Secrets Manager.
AllocatedStorage: '20'
MyTempBucket:
Type: AWS::S3::Bucket
# No DeletionPolicy, so it gets deleted on stack deletion. Perfect for temp data.
Wiring Things Together: Intrinsic Functions
This is where CloudFormation gets its power. You can’t just hardcode everything. You need to reference other resources, join strings, or conditionally create things. That’s what intrinsic functions are for. They’re the language you use inside your template.
!Ref: The workhorse. Gets the value of a Parameter or the ID/ARN of a Resource (which one depends on the resource).!GetAtt: Gets an attribute of a resource. Need the DNS name of a load balancer? That’s!GetAtt MyLoadBalancer.DNSName.!Sub: String substitution. Injects values into a string. Essential for building complex strings.!ImportValue: Grabs the output value from another stack. This is how you share things between stacks.
Resources:
MySecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow HTTP and SSH
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: !Ref MyHomeIPParameter
MyInstance:
Type: AWS::EC2::Instance
Properties:
InstanceType: !Ref InstanceType
ImageId: ami-12345678
SecurityGroupIds:
- !Ref MySecurityGroup # This passes the ID of the security group
UserData:
Fn::Base64: !Sub |
#!/bin/bash
echo "Hello from ${AWS::StackName} in ${AWS::Region}" > /tmp/hello.txt
The Grand Finale: Outputs
After your stack is created, how does another stack or a human know what was created? The Outputs section. It’s a way to expose important information from your stack, like the public DNS name of your new website. These can be viewed in the AWS Console or fetched via the CLI, and most importantly, they can be !ImportValue-d by other stacks.
Outputs:
WebsiteURL:
Description: 'The URL of our fabulous website'
Value: !Sub 'http://${MyInstance.PublicDnsName}' # Using the resource's public DNS attribute
DatabaseEndpoint:
Description: 'The connection string for the database'
Value: !GetAtt MyProductionDatabase.Endpoint.Address
So there you have it. The skeleton of every CloudFormation template you’ll ever write. Master this structure, and you’ve got the foundation for building anything AWS has to offer, without ever clicking that cursed “Create resource” button in the console again.