AWS CloudFormation
AWS CloudFormation is Amazon Web Services’ (AWS) native infrastructure-as-code service: you write a template — a JSON or YAML document describing the AWS resources you want (EC2 instances, RDS databases, IAM roles) and their properties — and CloudFormation provisions and configures those resources for you, working out the dependency order so you do not have to (per the AWS “What is CloudFormation?” guide). The unit of deployment is the stack: a collection of resources you create, update, and delete as a single unit. What most distinguishes CloudFormation from a self-managed tool like Terraform is that AWS owns the whole reconcile loop — it stores the state for you (there is no state file to babysit), it previews changes through change sets, and it automatically rolls back a failed create or update to the last known good configuration. This note teaches the model end to end: templates, stacks, change sets, update behaviors and rollback, nesting and cross-stack references, StackSets, drift detection, the resource-provider and custom-resource model, and the CDK/SAM layers built on top.
Mental Model
Think of CloudFormation as a managed desired-state engine that lives inside AWS. You hand it a declaration (the template); it compares that declaration against what already exists (recorded in the stack it manages for you) and produces a set of actions — create, update-in-place, replace, or delete — then executes them in dependency order, in parallel where it safely can. The template is the source of truth for intent; the stack is CloudFormation’s record of what it owns; the change set is the preview of the diff before you commit; rollback is the safety net if execution fails partway.
flowchart TD T["Template<br/>(JSON / YAML)<br/>desired resources + properties"] -->|create-stack /<br/>update-stack| ENGINE["CloudFormation engine<br/>(AWS-managed control plane)"] ENGINE -->|reads| STACK["Stack<br/>CFN's record of owned resources<br/>+ managed state"] ENGINE -->|calls| API["AWS service APIs<br/>EC2 · S3 · RDS · IAM · ..."] API -->|create / update / replace / delete| REAL["Real AWS resources"] ENGINE -.->|on failure| RB["Automatic rollback<br/>to last known good state"] CS["Change set<br/>(the 'plan' — preview only)"] -.->|optional preview| ENGINE T -.->|generate diff| CS style STACK fill:#e8f0ff style RB fill:#ffe8e8 style CS fill:#fff5e0
What it shows and the insight to take: the template flows into the engine, which reconciles against the AWS-managed stack record and calls the underlying service APIs. Two things that are external concerns in Terraform are internal to CloudFormation here: the state (the stack record) and the failure-recovery path (automatic rollback). You never hold a state file, and you rarely have to manually clean up a half-finished deployment — those responsibilities moved into AWS.
Templates — the Declaration
A CloudFormation template is a text document in JSON or YAML made of named sections, only one of which is mandatory (per the template-sections reference):
| Section | Required? | Purpose |
|---|---|---|
Resources | Yes | The stack resources and their properties — the core of every template. Each resource has a logical ID, a Type (e.g. AWS::EC2::Instance), and properties. |
Parameters | No | Values passed at stack create/update time — instance types, environment names — referenced elsewhere with !Ref. |
Outputs | No | Values returned after deployment (resource IDs, URLs); can be exported for cross-stack references. |
Mappings | No | Lookup tables (key→value) resolved with Fn::FindInMap — e.g. an AMI ID per Region. |
Conditions | No | Boolean expressions that gate whether a resource is created or a property is set. |
Transform | No | Applies macros; specifies the AWS SAM version for serverless templates, or AWS::Include to pull in snippets. |
Metadata, Rules | No | Extra template data; parameter validation rules checked at create/update. |
AWSTemplateFormatVersion, Description | No | Format version pin and a human description. |
A minimal but realistic YAML template with commentary:
AWSTemplateFormatVersion: 2010-09-09 # the only valid format version to date
Description: A single S3 bucket with a parameterized name
Parameters: # runtime inputs
BucketSuffix:
Type: String
Default: demo
Description: Suffix appended to the bucket name
Resources: # REQUIRED — the actual resources
DataBucket: # <-- logical ID (template-local name)
Type: AWS::S3::Bucket # <-- resource type
Properties:
BucketName: !Sub "my-app-${BucketSuffix}" # !Sub does string interpolation
VersioningConfiguration:
Status: Enabled
Outputs: # values surfaced after deploy
BucketArn:
Description: ARN of the created bucket
Value: !GetAtt DataBucket.Arn # !GetAtt reads a resource attributeTwo vocabulary points that recur everywhere: a logical ID (DataBucket) is the name inside the template; after creation the resource also has a physical ID (the real bucket name AWS assigns) that CloudFormation tracks in the stack. The intrinsic functions — !Ref, !GetAtt, !Sub, !FindInMap, !If — are how you wire references between resources, and each such reference implicitly creates a dependency edge that the engine uses to order operations (see The Resource Dependency Graph).
Stacks — the Unit of Deployment
When you deploy a template you create a stack. Every resource in the template becomes part of that stack, and the stack is the granularity at which you operate: create-stack, update-stack, delete-stack. Deleting the stack deletes all its resources — “you easily manage a collection of resources as a single unit” (Welcome guide). Crucially, CloudFormation persists the stack’s state itself: the mapping from each logical ID to its physical resource, the current template, parameter values, and every event. This is the sharpest structural difference from Terraform, where you must configure a remote backend to hold terraform.tfstate and lock it (Terraform state docs). With CloudFormation there is no state file to lose, no backend to configure, no lock table to provision — AWS does it transparently.
A stack moves through a lifecycle of statuses. The ones you must recognize:
stateDiagram-v2 [*] --> CREATE_IN_PROGRESS: create-stack CREATE_IN_PROGRESS --> CREATE_COMPLETE: all resources created CREATE_IN_PROGRESS --> ROLLBACK_IN_PROGRESS: a resource fails ROLLBACK_IN_PROGRESS --> ROLLBACK_COMPLETE: created resources deleted CREATE_COMPLETE --> UPDATE_IN_PROGRESS: update-stack UPDATE_IN_PROGRESS --> UPDATE_COMPLETE: update succeeds UPDATE_IN_PROGRESS --> UPDATE_ROLLBACK_IN_PROGRESS: update fails UPDATE_ROLLBACK_IN_PROGRESS --> UPDATE_ROLLBACK_COMPLETE: rolled back to prior state UPDATE_ROLLBACK_IN_PROGRESS --> UPDATE_ROLLBACK_FAILED: rollback itself fails CREATE_COMPLETE --> DELETE_IN_PROGRESS: delete-stack DELETE_IN_PROGRESS --> [*]: DELETE_COMPLETE
What it shows and the insight to take: the status names carry real operational meaning. ROLLBACK_COMPLETE (from a failed create) is a near-dead stack — you generally have to delete and recreate it, because none of its resources survived. UPDATE_ROLLBACK_COMPLETE means a failed update was safely reverted to the previous good template. UPDATE_ROLLBACK_FAILED is the state you fear: the rollback itself could not complete (often an out-of-band change made the “known good” state unreachable) and needs manual intervention via continue-update-rollback.
Change Sets — the Plan Equivalent
Before applying an update you can generate a change set: CloudFormation compares the stack’s current state against your submitted template and/or parameter changes and produces a preview of exactly which resources it will add, modify, or delete, including a before-and-after comparison of properties and — critically — whether any change forces a replacement (change-sets guide). No changes are made when the change set is created; CloudFormation acts only when you execute it. This is the direct analogue of terraform plan → terraform apply.
sequenceDiagram participant U as Engineer participant CFN as CloudFormation participant S as Stack (managed state) U->>CFN: create-change-set (new template / params) CFN->>S: compare submitted vs current CFN-->>U: change set — Add / Modify / Remove,<br/>Replacement: True/False per resource U->>U: review the diff (catch forced replacement!) alt looks safe U->>CFN: execute-change-set CFN->>S: apply changes in dependency order CFN-->>U: UPDATE_COMPLETE else looks wrong U->>CFN: delete-change-set (nothing was applied) end Note over CFN,S: After execution, CFN removes all<br/>change sets for the stack
What it shows and the insight to take: the change set is a reviewable artifact, and the single most valuable field it surfaces is Replacement. The same template edit can be harmless or catastrophic depending on the resource: changing a tag on an EC2 instance is an in-place update, but changing its AvailabilityZone forces CloudFormation to create a new instance and delete the old one — a different physical ID and, for a stateful resource like an RDS DBInstance, potential data loss unless you snapshot first. You can create as many change sets as you like to compare options; after you execute one, CloudFormation deletes all change sets for that stack because they no longer apply.
Change-set creation runs pre-deployment validation (property syntax, name conflicts, service-quota limits), but it explicitly does not guarantee the update will succeed — runtime conditions can still fail during execution.
Update Behaviors and Automatic Rollback — the Key Differentiator
When you update a stack, CloudFormation picks one of three update behaviors per changed resource (update-behaviors reference):
- Update with No Interruption — the resource is updated without disruption and keeps its physical ID (e.g. certain CloudTrail trail properties).
- Updates with Some Interruption — updated with a brief interruption but the same physical ID (e.g. some EC2 instance properties).
- Replacement — CloudFormation creates a replacement resource with a new physical ID, repoints dependents to it, and deletes the old one. Which behavior applies depends on which property you changed; the resource-type reference documents this per property.
The headline behavior — and the biggest philosophical split from Terraform — is automatic rollback on failure. By default, if any resource fails during a create or update, CloudFormation stops and rolls the stack back to its last known good state, deleting resources it created during a failed create or reverting them during a failed update. You do not end up with a half-built environment; you end up back where you started (or in ROLLBACK_COMPLETE, which you then delete). Terraform, by contrast, has no automatic rollback — a failed apply leaves partially-created resources recorded in state and expects you to fix forward or destroy manually.
You can opt out of automatic rollback. The Preserve successfully provisioned resources option (CLI: --disable-rollback or --on-failure DO_NOTHING) tells CloudFormation to leave successfully created resources in place and stop at the first failure in each independent provisioning path, leaving failed resources in CREATE_FAILED or UPDATE_FAILED (stack-failure-options guide). You then remediate and resume with a Retry, Update, or Roll back operation — a faster inner loop than “roll back everything and start over” when the failure was, say, a missing IAM permission. There is also a rollback-stack command to roll a *_FAILED stack back to its last stable state on demand.
Uncertain
Verify: the exact default-rollback semantics of express mode (a newer faster stack-operation mode). Reason: the failure-options doc states express mode disables rollback by default and requires
disableRollback:falsein--deployment-configto re-enable it, which inverts the classic default; express mode is recent and evolving. To resolve: re-check the Express mode page at time of use.#uncertain
Composition — Nested Stacks, Cross-Stack References, StackSets
As infrastructure grows you factor templates three different ways, and they are genuinely different mechanisms:
Nested stacks. A parent template declares an AWS::CloudFormation::Stack resource pointing (via TemplateURL in S3) at a child template (nested-stacks guide). This builds a hierarchy: the top-level stack is the root stack; each nested stack has an immediate parent. You operate on the root — an update-stack on the root re-evaluates every nested stack and updates only those whose templates changed. Parameters flow down (Parameters: on the nested resource) and outputs flow back up (!GetAtt NestedStack.Outputs.SomeOutput). Nesting is for reusing a configuration within one deployment lineage (e.g. a standard load-balancer template pulled into many app stacks).
Cross-stack references. Two independent, peer stacks share values by one stack exporting an output (Outputs with an Export: name) and another importing it with Fn::ImportValue. Unlike nested stacks, neither owns the other; but an export creates a hard dependency — you cannot delete or modify an exported output while another stack imports it. Use cross-stack references to share long-lived infrastructure (a VPC ID, a shared security group) across many separately-managed stacks.
StackSets. A StackSet extends a stack across multiple AWS accounts and Regions with a single operation (StackSets guide). From an administrator account you define one template and deploy stack instances into selected target accounts across specified Regions. Permissions come in two flavors: self-managed (you create the IAM administrator and execution roles yourself) and service-managed (StackSets integrates with AWS Organizations and can auto-deploy to accounts as they join an organizational unit). This is the tool for org-wide baselines — a guardrail IAM role or logging config rolled out to every account.
flowchart TB subgraph NESTED["Nested stacks (one lineage)"] R["Root stack"] --> C1["Nested: network"] R --> C2["Nested: app"] end subgraph CROSS["Cross-stack (peers)"] NET["network-stack<br/>Outputs.VpcId (Export)"] -. "Fn::ImportValue" .-> APP["app-stack"] end subgraph SS["StackSet (multi-account/region)"] ADMIN["Admin account<br/>StackSet definition"] --> I1["Stack instance<br/>acct A / us-east-1"] ADMIN --> I2["Stack instance<br/>acct B / eu-west-1"] ADMIN --> I3["Stack instance<br/>acct C / ap-south-1"] end
What it shows and the insight to take: three orthogonal scaling axes. Nested stacks scale a single deployment by composition; cross-stack references scale across peer stacks by sharing exported values (with a rigidity cost — exports lock); StackSets scale across accounts and Regions from a central admin. Reaching for the wrong one — e.g. nesting when you needed a StackSet — produces brittle designs.
Drift Detection
Because resources can be changed outside CloudFormation (someone edits a security group in the EC2 console), the stack’s real configuration can drift from its template. Drift detection compares each supported resource’s actual property values against the expected values from the template and parameters (drift guide). The statuses:
- Stack drift status:
DRIFTED(one or more resources differ),IN_SYNC, orNOT_CHECKED. - Resource drift status:
MODIFIED,DELETED,IN_SYNC, orNOT_CHECKED(the resource type does not support drift detection). - Property difference types:
ADD,REMOVE,NOT_EQUAL.
Important limitations: drift detection only considers properties explicitly set in the template (not service defaults), it does not recurse into nested stacks automatically (run it on each nested stack), and certain properties can never be compared — CloudFormation “cannot map the source code of a Lambda function back” to the template, and services never return secret values like an IAM login-profile Password, so those are excluded. Unlike Terraform, where every plan implicitly refreshes and shows drift, CloudFormation drift detection is a separate, on-demand operation you must trigger (or automate via AWS Config / EventBridge).
The Resource-Provider Model and Custom Resources
CloudFormation supports a fixed but large catalog of resource types (AWS::Service::Resource). When you need something the catalog does not cover, there are two extension paths:
Custom resources let you inject arbitrary provisioning logic. You declare a resource of type Custom::MyThing (or AWS::CloudFormation::CustomResource) whose required ServiceToken property points at a Lambda function or SNS topic (custom-resources guide). On create/update/delete, CloudFormation sends a request (with a RequestType and a pre-signed S3 ResponseURL) to that token and waits; your code does its work and uploads a SUCCESS or FAILED response, optionally with output data the template reads via Fn::GetAtt. The default response timeout is 3600 seconds (one hour), tunable with ServiceTimeout.
sequenceDiagram participant CFN as CloudFormation participant L as Lambda (custom resource provider) participant S3 as Pre-signed S3 URL CFN->>L: request { RequestType: Create, ResponseURL, ResourceProperties } L->>L: do custom work (call any API, etc.) L->>S3: PUT { Status: SUCCESS, Data: {...} } CFN->>S3: poll for response S3-->>CFN: SUCCESS + Data CFN->>CFN: proceed with stack operation Note over CFN,L: No response before timeout ⇒ stack operation FAILS
What it shows and the insight to take: a custom resource is a callback into your own code embedded in the reconcile loop. The subtle failure mode is that if your Lambda throws before sending a response, CloudFormation gets nothing and blocks until the timeout — which is why robust custom resources always send a FAILED response in their error handler.
Registry-based resources (the CloudFormation registry) are the heavier, more capable path: you register a private or third-party resource type that supports full CRUDL (Create, Read, Update, Delete, List) and — unlike custom resources — participates in drift detection, without you wiring up an SNS topic or Lambda per operation. This is how third-party vendors ship first-class CloudFormation types.
Higher-Level Layers — CDK and SAM
CloudFormation templates are verbose, so AWS built friendlier layers that compile down to templates:
- AWS CDK (Cloud Development Kit) lets you define infrastructure in a general-purpose language — TypeScript, JavaScript, Python, Java, C#/.NET, or Go — using reusable components called constructs, composed into stacks and apps (CDK v2 guide).
cdk synthsynthesizes a CloudFormation template from your code;cdk deployprovisions it through CloudFormation. A few dozen lines of CDK can emit a 500-line template with 50+ resources. The value: real loops, types, IDE completion, unit tests — with CloudFormation still doing the actual provisioning “with rollback on error.” See Cloud Development Kit. - AWS SAM (Serverless Application Model) is a template transform: you write terser SAM syntax for serverless apps (
AWS::Serverless::Function,AWS::Serverless::Api) and theTransform: AWS::Serverless-2016-10-31macro expands it into full CloudFormation resources at deploy time.
Both are strictly authoring conveniences — the deployed artifact is always a CloudFormation stack, so everything above (change sets, rollback, drift, StackSets) still applies.
CloudFormation versus Terraform
| Dimension | AWS CloudFormation | Terraform / OpenTofu |
|---|---|---|
| Cloud coverage | AWS-only (native) | Multi-cloud via providers (AWS, Azure, GCP, SaaS) |
| Language | JSON / YAML declarative templates | HCL (HashiCorp Configuration Language) |
| State | Managed by AWS — no file, no backend, no lock table | Self-managed terraform.tfstate; you configure a remote backend + locking |
| Plan / preview | Change sets (opt-in preview) | terraform plan (idiomatic, run every time) |
| Failure handling | Automatic rollback to last good state (default) | No auto-rollback; partial apply left in state, fix-forward or destroy |
| Drift | On-demand drift detection operation | Implicit in every plan (refresh) |
| Multi-account/region | StackSets (native, Organizations-integrated) | Provider aliases / workspaces / separate state |
| Extensibility | Registry resource types + custom resources (Lambda/SNS) | Providers (Go plugins) |
| Higher-level authoring | CDK, SAM (synthesize to templates) | CDK for Terraform (CDKTF), Terragrunt |
| Cost | No charge for CloudFormation itself (pay for resources) | Terraform CLI free; HCP Terraform / TFC paid tiers |
The decision usually reduces to reach versus control: if you are AWS-only and want the deepest native integration with zero state plumbing and a built-in safety net, CloudFormation is the path of least resistance. If you are multi-cloud, or want the plan-review workflow as a first-class default and a vast provider/module ecosystem, Terraform/OpenTofu wins — at the cost of owning your state and your rollback strategy yourself.
Failure Modes and Gotchas
ROLLBACK_COMPLETEdead-ends. A stack that fails its initial create lands inROLLBACK_COMPLETEand can only be deleted, not updated — surprising newcomers who try to “fix and retry” in place.UPDATE_ROLLBACK_FAILED. If an out-of-band change made the previous good state unreachable, the automatic rollback can itself fail, stranding the stack until youcontinue-update-rollback(possibly skipping the offending resource).- Surprise replacement. A property edit that quietly forces
Replacementcan destroy a stateful resource. Always read the change set’sReplacementcolumn before executing — this is the CloudFormation equivalent of reading a Terraform plan for-/+ destroy and then create. - Export locks. You cannot change or delete an
Outputsexport while another stack imports it, which can wedge coordinated updates across cross-stack boundaries. - Custom-resource hangs. A provider Lambda that errors without sending a response blocks the stack until the (default one-hour) timeout — always respond
FAILEDon error. - Drift is not continuous. Drift detection is on-demand; without automation (AWS Config rules, scheduled EventBridge triggers) you will not notice drift until an update mysteriously fails.
See Also
- Terraform — the multi-cloud incumbent; the primary contrast (self-managed state, plan-review default, no auto-rollback)
- Azure Bicep — the equivalent AWS-native-vs-vendor story on Azure (Bicep → ARM)
- Cloud Development Kit — CDK and CDKTF; generating declarative templates from imperative code
- Infrastructure State Files — what state stores; CloudFormation manages it, Terraform does not
- Plan Apply and Destroy — the generic three-verb lifecycle change sets and stacks implement
- The Resource Dependency Graph — the DAG CloudFormation builds from
!Ref/!GetAttreferences - Configuration Drift and Drift Detection — the general drift concept CloudFormation’s drift detection implements
- Provisioning versus Configuration Management — CloudFormation provisions; Ansible/Chef configure
- Infrastructure as Code MOC — parent map (§5 Tools Landscape)
- Cloud Architecture MOC — the AWS primitives (VPC, EC2, RDS, IAM) CloudFormation provisions