Clicking through the console is fine until the day you have to build it again — in another region, in another account, or because someone deleted something and nobody remembers what it was called. CloudFormation is AWS's answer: describe the resources in a file, hand the file to AWS, and let it work out what to create, change or delete.
The examples here are from the stack that runs this site's admin API, because a real template shows the parts that matter and a tutorial template does not.
A template's five sections
Only Resources is required. The rest earn their place as the stack grows.
| Section | What it is for |
|---|---|
Parameters | Values supplied at deploy time — environment name, a certificate ARN, a bucket |
Conditions | Booleans built from parameters, used to include or skip a resource |
Resources | The actual things. Each has a logical id, a
Type, and Properties |
Outputs | Values to read back afterwards, or export to another stack |
Mappings | Static lookup tables, usually keyed by region |
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
Env:
Type: String
Default: prod
AllowedValues: [prod, dev]
CertificateArn:
Type: String
Default: ''
Description: Regional ACM cert in this stack's region (NOT us-east-1)
Conditions:
HasCustomDomain: !Not [!Equals [!Ref CertificateArn, '']]
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub 'lovemesomecoding-admin-api-${Env}'
Handler: app.main.handler
Runtime: python3.12
MemorySize: 512
Timeout: 30
Outputs:
ApiUrl:
Value: !GetAtt HttpApi.ApiEndpoint!Ref reads a parameter or another resource's main identifier, !GetAtt
reads a named attribute of a resource, and !Sub interpolates both into a string. Those
three cover most templates.
SAM is shorthand, not a different service
That Transform: AWS::Serverless-2016-10-31 line is what makes
AWS::Serverless::Function legal. A transform is a macro: CloudFormation expands it
server-side into ordinary resources — an AWS::Lambda::Function, an execution role,
a log group — before it deploys anything.
So SAM is not a competing tool. It is CloudFormation with a preprocessor, and a SAM stack is a CloudFormation stack you can inspect in the CloudFormation console. That also means anything SAM cannot express, you can drop into the same template as a raw resource.
Change sets: see it before you do it
The feature that makes CloudFormation safe to run against production. A change set is a diff: what would be added, modified, or replaced.
aws cloudformation create-change-set \
--stack-name lovemesomecoding-admin-api-prod \
--change-set-name review \
--template-body file://template.yaml \
--capabilities CAPABILITY_IAM
aws cloudformation describe-change-set \
--stack-name lovemesomecoding-admin-api-prod \
--change-set-name review \
--query "Changes[].ResourceChange.[Action,LogicalResourceId,Replacement]" \
--output tableThe column to read is Replacement. True means CloudFormation will
delete the resource and create a new one — a new database, a new bucket, a new identifier for
everything pointing at it. Some properties simply cannot be updated in place, and the change set
is where you find that out instead of during the deploy.
The trap: an omitted parameter keeps its old value
This one has cost real time on this site, so it gets its own heading.
CloudFormation keeps the PREVIOUS value for any parameter an update omits. It does not adopt a changed default from your new template.
The consequences are quiet and confusing. You edit a default in template.yaml,
commit it, deploy, and the stack keeps running the old value — the template in git no longer
describes what is deployed. There is no warning, because from CloudFormation's point of view you
asked for no change.
The fix is to pass every parameter explicitly on every deploy, which is what this site's deploy script does:
aws cloudformation deploy \
--template-file template.yaml \
--stack-name lovemesomecoding-admin-api-prod \
--capabilities CAPABILITY_IAM \
--no-fail-on-empty-changeset \
--parameter-overrides Env=prod DataEnv=prod CorsOrigins="$CORS_ORIGINS"There is a sharper edge on the same rule. An empty string is a real value, not an omission — so
passing CertificateArn='' flips HasCustomDomain to false, and
CloudFormation then deletes the custom domain and its DNS record. A missing environment
variable in a deploy script becomes a silent outage. Guard against it in the script rather than
trusting yourself to remember.
Drift: when someone clicks something
CloudFormation does not stop anyone editing a managed resource in the console, and it will not notice on its own. Someone widens a security group by hand to unblock themselves on a Friday, and the template quietly stops describing reality — until the next deploy puts it back and breaks whatever depended on the manual change.
Drift detection is the check for this, and it is worth running before any deploy you are nervous about:
aws cloudformation detect-stack-drift --stack-name lovemesomecoding-admin-api-prod
aws cloudformation describe-stack-resource-drifts \
--stack-name lovemesomecoding-admin-api-prod \
--stack-resource-drift-status-filters MODIFIED DELETEDNote that the first call starts an asynchronous detection and returns an id; the second reads the result once it has finished. Drift that is deliberate belongs in the template. Drift that is not is a deploy about to surprise you.
When an update fails
CloudFormation rolls back by default: it returns the stack to its last good state, which is usually what you want and occasionally maddening, because the error scrolls past during the rollback. The stack events are where the real message is — read the oldest failure, not the newest, since everything after it is rollback noise.
aws cloudformation describe-stack-events \
--stack-name lovemesomecoding-admin-api-prod \
--query "StackEvents[?ResourceStatus=='CREATE_FAILED'].[LogicalResourceId,ResourceStatusReason]" \
--output tableA stack stuck in ROLLBACK_COMPLETE cannot be updated — it can only be deleted and
recreated. That is only survivable if the stack really does describe everything, which is the
argument for not doing half your infrastructure by hand.