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):

SectionRequired?Purpose
ResourcesYesThe 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.
ParametersNoValues passed at stack create/update time — instance types, environment names — referenced elsewhere with !Ref.
OutputsNoValues returned after deployment (resource IDs, URLs); can be exported for cross-stack references.
MappingsNoLookup tables (key→value) resolved with Fn::FindInMap — e.g. an AMI ID per Region.
ConditionsNoBoolean expressions that gate whether a resource is created or a property is set.
TransformNoApplies macros; specifies the AWS SAM version for serverless templates, or AWS::Include to pull in snippets.
Metadata, RulesNoExtra template data; parameter validation rules checked at create/update.
AWSTemplateFormatVersion, DescriptionNoFormat 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 attribute

Two 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 planterraform 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:false in --deployment-config to 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, or NOT_CHECKED.
  • Resource drift status: MODIFIED, DELETED, IN_SYNC, or NOT_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 synth synthesizes a CloudFormation template from your code; cdk deploy provisions 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 the Transform: AWS::Serverless-2016-10-31 macro 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

DimensionAWS CloudFormationTerraform / OpenTofu
Cloud coverageAWS-only (native)Multi-cloud via providers (AWS, Azure, GCP, SaaS)
LanguageJSON / YAML declarative templatesHCL (HashiCorp Configuration Language)
StateManaged by AWS — no file, no backend, no lock tableSelf-managed terraform.tfstate; you configure a remote backend + locking
Plan / previewChange sets (opt-in preview)terraform plan (idiomatic, run every time)
Failure handlingAutomatic rollback to last good state (default)No auto-rollback; partial apply left in state, fix-forward or destroy
DriftOn-demand drift detection operationImplicit in every plan (refresh)
Multi-account/regionStackSets (native, Organizations-integrated)Provider aliases / workspaces / separate state
ExtensibilityRegistry resource types + custom resources (Lambda/SNS)Providers (Go plugins)
Higher-level authoringCDK, SAM (synthesize to templates)CDK for Terraform (CDKTF), Terragrunt
CostNo 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_COMPLETE dead-ends. A stack that fails its initial create lands in ROLLBACK_COMPLETE and 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 you continue-update-rollback (possibly skipping the offending resource).
  • Surprise replacement. A property edit that quietly forces Replacement can destroy a stateful resource. Always read the change set’s Replacement column 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 Outputs export 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 FAILED on 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