What CloudFormation is & the model
Templates, stacks, managed provisioning.
An architect does not lay the bricks. They draw the finished building, hand the drawing to a builder, and the builder works out the order: foundation first, then the walls, then the roof. AWS CloudFormation (Amazon Web Services' built-in infrastructure-as-code service) works the same way. You write a template, a plain-text document that describes the cloud resources you want, and CloudFormation reads it, figures out the order, and builds everything by calling Amazon's own APIs (application programming interfaces, the command channels each AWS service listens on). Infrastructure as code means your servers, networks, and permissions live in a file you can read, review, and check into version control, instead of a pile of console clicks nobody remembers making.
Because CloudFormation is part of AWS, there is nothing to install and no separate state file to babysit. AWS stores the record of what it built, tracks every resource, and knows how to create, update, and delete each one. That native position is the whole pitch against an outside tool. CloudFormation learns each AWS resource's lifecycle first-hand, orders the dependencies for you, and rewinds automatically when a build fails partway through. It is also the foundation under the higher-level tools you will meet later: the Serverless Application Model (SAM) and the Cloud Development Kit (CDK), which turns real programming languages into CloudFormation templates behind the scenes. The price of all this is lock-in. CloudFormation speaks AWS, and only AWS.
Templates, stacks, and resources
Three words carry the whole model. A template is the blueprint: a declarative document, which means you write down the end state you want (this bucket, that database, these permissions) and let CloudFormation work out the steps to get there. You never write 'create the bucket, then wait, then attach the policy.' You describe the finished shape and hand it over. Templates are written in YAML or JSON (two plain-text formats for structured data; YAML is the easier of the two to read). A resource is one thing inside that blueprint: a single Amazon S3 bucket (Simple Storage Service, AWS's object storage), one EC2 virtual machine (Elastic Compute Cloud), one IAM role (Identity and Access Management, the permission system). A stack is the finished building. It is the live set of resources CloudFormation created from one template and now manages together. Everything in a stack is born together, changes together, and, if you delete the stack, dies together, like a labeled crate on moving day: whatever went in comes out together when you empty it.
AWSTemplateFormatVersion: "2010-09-09"Description: One private S3 bucket, managed as a stackResources:DataBucket:Type: AWS::S3::BucketProperties:BucketEncryption:ServerSideEncryptionConfiguration:- ServerSideEncryptionByDefault:SSEAlgorithm: AES256PublicAccessBlockConfiguration:BlockPublicAcls: trueBlockPublicPolicy: trueIgnorePublicAcls: trueRestrictPublicBuckets: true
The only section CloudFormation actually requires is Resources; the version line and description are optional politeness. DataBucket is the logical ID, your private name for the resource inside the template. Notice this bucket is locked down on purpose: encryption switched on by default, every public-access door bolted shut. Writing that into the blueprint means every copy you ever build starts safe, and any future attempt to loosen it has to show up as a change to a file someone can review.
From template to running infrastructure
Hand the template to CloudFormation and it becomes a stack. One command does it.
aws cloudformation create-stack \--stack-name demo-bucket \--template-body file://s3-bucket.yaml
{"StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/demo-bucket/8f2a1c40-85d1-11f0-b3e9-0e6c2f5a1b23"}
That Amazon Resource Name (ARN, AWS's globally unique address for a thing) is the stack's permanent handle. The command returns right away, because CloudFormation does the real work in the background. To follow along, ask the stack for its status. (If you would rather block until it finishes, aws cloudformation wait stack-create-complete --stack-name demo-bucket does exactly that.)
aws cloudformation describe-stacks \--stack-name demo-bucket \--query 'Stacks[0].StackStatus' --output text
CREATE_COMPLETE
A stack moves through a small vocabulary of states: CREATE_IN_PROGRESS while it builds, CREATE_COMPLETE when every resource is up, and a set of ROLLBACK states when something goes wrong. Those words are the contract your scripts and pipelines check before they trust a stack. Underneath, CloudFormation did the managed provisioning for you: it read the template, decided what depended on what, called the S3 API to create the bucket, waited for AWS to confirm, and wrote the result into the stack's own records. You made zero API calls by hand. You described the end state, and the service made it real, in the right order.
The stack keeps a ledger
When you named the bucket DataBucket, AWS did not actually call it that. Ask the stack what it really built.
aws cloudformation list-stack-resources \--stack-name demo-bucket \--query 'StackResourceSummaries[].[LogicalResourceId,PhysicalResourceId,ResourceStatus]' \--output text
DataBucket demo-bucket-databucket-1a2b3c4d5e6f CREATE_COMPLETE
DataBucket is the logical ID, the nickname you use in the template. demo-bucket-databucket-1a2b3c4d5e6f is the physical ID, the real name AWS handed out. The stack holds the mapping between the two, and that mapping is the quiet center of the whole model. Change the template later and run an update, and CloudFormation looks up DataBucket, finds the exact bucket behind it, and changes that one instead of making a second. The logical ID is how you point at a resource for its whole life without ever hard-coding its real name, the shipping label you wrote (Gift A) versus the tracking number the courier assigned. One warning follows from this: some properties cannot be changed in place. Edit a bucket's BucketName and CloudFormation cannot rename a bucket that already exists, so it builds a new empty bucket, swaps the stack's mapping onto it, and tries to remove the old one. Your application now reads from an empty bucket, and the data in the old one does not follow. (If that old bucket still holds objects, S3 refuses to delete it, so CloudFormation leaves it orphaned outside the stack, unmanaged and still costing you money.)
Other infrastructure-as-code tools, like Terraform, keep this same ledger in a state file that you have to store somewhere safe and lock so two people cannot edit it at once. CloudFormation keeps the ledger inside AWS. There is no state file to lose, leak, or corrupt, and the locking is handled for you. Since that record can hold sensitive values, keeping it inside AWS under IAM permissions instead of a file on someone's laptop is one less thing that can spill.
When a build fails, the whole thing rewinds
A bank transfer either completes in full or not at all. It never strands half the money in transit. CloudFormation aims for that same all-or-nothing behavior, and it is the biggest reason to prefer it over a shell script that fires AWS commands one after another. Add a second bucket to the template with a name that is already taken (S3 bucket names are global, shared across every AWS customer on the planet) and watch what CloudFormation does.
AWSTemplateFormatVersion: "2010-09-09"Resources:DataBucket:Type: AWS::S3::BucketLogBucket:Type: AWS::S3::BucketProperties:BucketName: logs # global name, already taken: this create fails
aws cloudformation create-stack \--stack-name demo-two \--template-body file://two-buckets.yaml
{"StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/demo-two/1b7d9e10-85d3-11f0-9a2c-0e6c2f5a1b23"}
Now read the stack's event stream, oldest first.
aws cloudformation describe-stack-events --stack-name demo-two \--query "reverse(StackEvents[?ResourceStatusReason!='Resource creation Initiated'].[Timestamp,LogicalResourceId,ResourceStatus])" \--output text
2026-07-21T11:02:01.104000+00:00 demo-two CREATE_IN_PROGRESS2026-07-21T11:02:04.882000+00:00 DataBucket CREATE_IN_PROGRESS2026-07-21T11:02:05.271000+00:00 LogBucket CREATE_IN_PROGRESS2026-07-21T11:02:07.930000+00:00 LogBucket CREATE_FAILED2026-07-21T11:02:26.017000+00:00 DataBucket CREATE_COMPLETE2026-07-21T11:02:29.660000+00:00 demo-two ROLLBACK_IN_PROGRESS2026-07-21T11:02:31.928000+00:00 DataBucket DELETE_IN_PROGRESS2026-07-21T11:02:33.114000+00:00 DataBucket DELETE_COMPLETE2026-07-21T11:02:34.550000+00:00 demo-two ROLLBACK_COMPLETE
Read it top to bottom. LogBucket failed within a couple of seconds, because its name was already taken. DataBucket was still being built at that moment, and CloudFormation does not rip a half-finished resource out from under itself, so it let DataBucket reach CREATE_COMPLETE first. Then, with one bucket standing and one dead, the stack flipped to ROLLBACK_IN_PROGRESS and went back to delete DataBucket, the resource that had actually worked, leaving you at ROLLBACK_COMPLETE with nothing half-built. To find out why LogBucket died, pull the reason CloudFormation recorded.
aws cloudformation describe-stack-events --stack-name demo-two \--query "StackEvents[?ResourceStatus=='CREATE_FAILED'].ResourceStatusReason" \--output text
Resource handler returned message: "The requested bucket name is not available. The bucket namespace is shared by all users of the system. Please select a different name and try again. (Service: S3, Status Code: 409, Request ID: 8F3KQ2ZP4NDABC12, Extended Request ID: 9m2Lk3n1oP0qRsTuVwXyZaBcDeFgHiJkLmNoPqRsTuVwXyZ012=)" (RequestToken: 3f7c9a21-2f0e-4d5b-9c1a-7b6e2d4a1c88, HandlerErrorCode: AlreadyExists)
That failure reason is written into the permanent event log, not printed once and lost. For a defender or an operator, this is the payoff: every create, update, and delete leaves a timestamped trail of what changed and why, and a failed deploy backs itself out instead of leaving a half-configured network with a hole in it.
What the model buys you
Three habits fall out of this design, and all three matter for security work. First, the event stream is an audit trail. Pair it with CloudTrail (AWS's log of every API call and the identity behind it) and you can answer both what changed, from the stack events, and who asked for it, from CloudTrail. Second, before an update ever touches production, a change set lets you preview it. aws cloudformation create-change-set stages the diff, and aws cloudformation describe-change-set prints it, including any resource marked Replacement: True, which means it will be destroyed and rebuilt. Reading that list is your last chance to catch a rename that would wipe a database. Third, drift detection tells you when someone changed a resource outside the stack, editing it by hand in the console, so a manual or unauthorized change cannot hide for long.
After any deploy, whether it went right or wrong, aws cloudformation describe-stack-events is the first place to look. It is the closest thing your infrastructure has to a flight recorder: every resource, every status, every reason, in order, kept by AWS whether the stack finished or rolled back. Learn to read it, and most CloudFormation mysteries answer themselves.
Try this
Work through “What the model buys you” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
The trap worth remembering here: a stack delete can take your database with it. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.