Skip to content
Infrastructure as Code iac tools 5 min read

AWS CloudFormation

AWS CloudFormation is Amazon’s first-party Infrastructure as Code service. You describe a set of AWS resources in a declarative YAML or JSON template, hand it to CloudFormation, and it provisions, updates, and deletes those resources as a single managed unit called a stack. The defining trait is that AWS owns the state for you — there is no state file to store, lock, or back up — and the engine offers AWS-native features like change sets and drift detection. If your footprint is exclusively AWS and you want zero external tooling, CloudFormation is the path of least resistance; if you span multiple clouds or want a richer module ecosystem, Terraform usually wins.

Templates: the unit of declaration

A CloudFormation template is a structured document with a handful of top-level sections — Parameters, Resources, Outputs, and optionally Mappings, Conditions, and Transform. Only Resources is required. Templates support intrinsic functions such as !Ref, !GetAtt, and !Sub for wiring values between resources, which fill the role that interpolation and references play in HCL.

Here is a minimal template that creates an S3 bucket with a parameterized name and exports its ARN:

AWSTemplateFormatVersion: "2010-09-09"
Description: Access-log bucket managed by CloudFormation

Parameters:
  BucketName:
    Type: String
    Default: acme-app-access-logs

Resources:
  LogBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      VersioningConfiguration:
        Status: Enabled
      Tags:
        - Key: Environment
          Value: prod
        - Key: ManagedBy
          Value: cloudformation

Outputs:
  LogBucketArn:
    Description: ARN of the access-log bucket
    Value: !GetAtt LogBucket.Arn
    Export:
      Name: acme-log-bucket-arn

You deploy it as a stack with the AWS CLI:

aws cloudformation deploy \
  --template-file logs.yaml \
  --stack-name acme-logs \
  --parameter-overrides BucketName=acme-app-access-logs

Output:

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - acme-logs

Stacks and change sets

A stack is the deployment boundary: every resource in a template is created, updated, and rolled back together. If a resource fails to create during a stack operation, CloudFormation automatically rolls the whole stack back to its previous good state, so you never end up half-applied — a meaningful safety property compared to a raw terraform apply that errors midway.

A change set is CloudFormation’s equivalent of terraform plan. It computes the difference between your template and the live stack and shows exactly which resources will be added, modified, or replaced before you execute it. Crucially, it flags replacement — a property change that forces the resource to be destroyed and recreated, the same concept HCL surfaces as # forces replacement.

aws cloudformation create-change-set \
  --stack-name acme-logs \
  --change-set-name add-lifecycle \
  --template-file logs.yaml \
  --change-set-type UPDATE

aws cloudformation describe-change-set \
  --stack-name acme-logs --change-set-name add-lifecycle \
  --query 'Changes[].ResourceChange.{Action:Action,Type:ResourceType,Replace:Replacement}'

Output:

[
  {
    "Action": "Modify",
    "Type": "AWS::S3::Bucket",
    "Replace": "False"
  }
]

Always review a change set before executing it. A property marked Replacement: True will delete and recreate the resource — for a database or stateful bucket that can mean data loss, exactly as it would with a Terraform replacement.

Managed state and drift detection

CloudFormation stores the authoritative state of each stack inside the AWS service itself. There is no terraform.tfstate to put in S3, no DynamoDB lock table to configure, and no risk of a developer running with a stale local copy. Concurrency is handled for you: a stack can only run one operation at a time.

The trade-off for not seeing the state is drift — when someone changes a resource out of band (a console edit, a script, another team). Drift detection asks CloudFormation to compare each resource’s live configuration against the template it deployed:

aws cloudformation detect-stack-drift --stack-name acme-logs
aws cloudformation describe-stack-resource-drifts --stack-name acme-logs \
  --stack-resource-drift-status-filters MODIFIED DELETED

This is conceptually the same as a Terraform refresh-only plan, but it is an explicit, on-demand operation rather than something that happens automatically on every plan.

Terraform vs CloudFormation on AWS

Both are mature, declarative, and free to use (you pay only for the resources). The decision usually comes down to cloud breadth, ecosystem, and how much you value AWS-native integration.

DimensionCloudFormationTerraform / OpenTofu
Cloud scopeAWS onlyMulti-cloud and SaaS via 4,000+ providers
LanguageYAML / JSON templatesHCL (with for_each, expressions, functions)
StateManaged by AWS, no state fileSelf-managed state file + locking backend
Plan previewChange setsterraform plan
DriftOn-demand drift detectionRefresh-only plan
ModularityNested stacks, StackSetsModules + public registry
New AWS service supportSame-day, first-partyUsually days behind via the AWS provider
RollbackAutomatic on failureManual / re-apply

A practical middle ground: many teams run Terraform but reach for CloudFormation only where AWS requires it (for example, Service Catalog products or some marketplace deployments), or use the AWS CDK, which synthesizes CloudFormation templates from a real programming language. Note that everything Terraform does here applies equally to OpenTofu, its open-source fork.

Best Practices

  • Prefer YAML over JSON for templates — it supports comments and the shorthand intrinsic functions (!Ref, !Sub) that keep templates readable.
  • Always create and inspect a change set before updating a production stack; never deploy blind.
  • Enable termination protection (aws cloudformation update-termination-protection) on stacks that own stateful resources to prevent accidental deletion.
  • Run drift detection on a schedule so out-of-band console changes are caught early rather than at the next deploy.
  • Break large environments into nested stacks or StackSets instead of one monolithic template to keep blast radius and update times small.
  • Use parameters and Conditions for environment differences rather than copy-pasting templates per environment.
  • If you need multi-cloud, a module registry, or richer logic, evaluate Terraform/OpenTofu before committing to CloudFormation.
Last updated June 14, 2026
Was this helpful?