Infrastructure as code (IaC) describes infrastructure in files that teams can review, version, and apply repeatedly. On AWS, CloudFormation and Terraform both support declarative configuration, but they track deployed resources differently and have distinct update workflows. A change set or Terraform plan previews proposed work; deployment can still fail partway through, require replacement, or affect data and dependent services. This guide compares the two tools and shows how to review changes, protect state, and keep resource ownership clear.

Infrastructure as Code on AWS

With declarative IaC, you describe the resources and settings you want; the tool compares that configuration with its record of deployed infrastructure and proposes or performs changes. This makes infrastructure changes easier to review and repeat, but it does not remove the need to understand what is already running or what a change will do.

Define the desired state and assign ownership

Before writing a template, identify which resources belong in it, how they relate to other systems, and who is allowed to change them. A resource should have one clear owner. If an existing bucket, database, or network is created by hand or managed by another tool, deliberately import it using the tool's supported workflow, leave it under its current owner, or plan a replacement. CloudFormation and Terraform each document an import process for supported resources; HashiCorp explains Terraform imports and one-to-one state bindings. Declaring an existing resource without adopting it can create a duplicate; managing it from two states can cause conflicting changes.

Use IaC for repeatable change control

Versioned configuration helps teams explain why a resource exists and review changes before deployment. Reusable templates and modules reduce repeated setup and support more consistent environments. Each proposed change still needs review against current resource state, permissions, service limits, and data dependencies.

How CloudFormation and Terraform differ

CloudFormation manages resources through AWS stacks and their templates. It primarily manages AWS resources, and its registry can extend templates with activated third-party and private resource types. Terraform uses provider plugins to manage AWS and many other services, and stores configuration-to-resource bindings in state, locally by default or in a configured remote backend. Teams must protect that state. See the AWS overview of CloudFormation stacks and HashiCorp’s explanation of Terraform state.

ConcernCloudFormationTerraform
ConfigurationJSON or YAML templates, with intrinsic functions and stack parametersHCL configuration, providers, and reusable modules
Deployment recordThe AWS service tracks each stack's template and managed resourcesTerraform state maps configuration resource instances to remote objects
Review workflowCreate and inspect a change set before executing a stack updateCreate and inspect a plan; optionally save that exact plan for an approved apply
Useful fitAWS-focused stacks and teams that want CloudFormation-managed deploymentAWS plus other provider-managed systems, or teams already operating Terraform state and workflows

Neither tool makes a whole deployment an atomic transaction. CloudFormation can roll back many failed stack operations, but rollback can also fail; inspect the stack status and resources before retrying. HashiCorp documents that Terraform does not automatically roll back a partially completed apply, so inspect state and live resources before planning a recovery. See AWS guidance on CloudFormation rollback failures and HashiCorp guidance on Terraform apply errors.

Manage AWS resources with CloudFormation

A CloudFormation stack groups resources declared in a template. CloudFormation keeps the template, parameters, managed resources, and operation status in its service, so teams do not manage a separate state file. When a deployment fails, stack events show which operation failed and whether rollback completed. Wait for a terminal stack status and inspect affected resources before starting another update.

Keep templates explicit and reviewable

Templates may contain parameters, mappings, conditions, resources, and outputs. Parameters are useful for values that vary by deployment, such as an environment's CIDR range. Prefer clear parameters and outputs for values crossing stack boundaries; do not assume one stack can read another stack's template data automatically.

This example creates a private-by-default bucket and grants one specified role permission to read objects under a single prefix. Supply the exact role ARN as a parameter; for a cross-account role, its own account must also grant the needed identity permission.

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Parameters": {
    "ReportReaderRoleArn": {
      "Type": "String",
      "Description": "ARN of the role allowed to read report objects"
    }
  },
  "Resources": {
    "ReportBucket": {
      "Type": "AWS::S3::Bucket",
      "Properties": {
        "PublicAccessBlockConfiguration": {
          "BlockPublicAcls": true,
          "BlockPublicPolicy": true,
          "IgnorePublicAcls": true,
          "RestrictPublicBuckets": true
        }
      }
    },
    "ReportBucketReadPolicy": {
      "Type": "AWS::S3::BucketPolicy",
      "Properties": {
        "Bucket": { "Ref": "ReportBucket" },
        "PolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [{
            "Sid": "ReadReportsForOneRole",
            "Effect": "Allow",
            "Principal": { "AWS": { "Ref": "ReportReaderRoleArn" } },
            "Action": "s3:GetObject",
            "Resource": { "Fn::Sub": "${ReportBucket.Arn}/reports/*" }
          }]
        }
      }
    }
  }
}

The bucket policy grants only s3:GetObject on the reports/ object prefix to the supplied role. It does not grant bucket listing, writes, or public access. Validate policies against the workload's actual access needs before deployment.

Review and execute stack changes

For an update, create a CloudFormation change set and inspect which resources will be added, modified, deleted, or replaced. A change set may help identify replacements, but AWS does not guarantee that the update will succeed. Service validation, quotas, resource availability, and runtime behavior can still cause failure. Use the CloudFormation change-set guide to create and review a change set, then execute only the one that matches the approved change.

Some property changes require CloudFormation to replace a resource. For data-bearing resources, check the resource type's update behavior, plan backups and cutover, and inspect what happens to both old and new physical resources. DeletionPolicy controls resources deleted when a stack is deleted or a resource is removed from the template; it does not protect an old physical resource replaced during an update. UpdateReplacePolicy controls that old resource during replacement. Retain can leave a resource outside the stack's management and still incur charges, so it is not a backup or a complete rollback strategy. See AWS's references for DeletionPolicy and UpdateReplacePolicy.

Use stack events to diagnose failures

Stack events report resource operations and failure reasons. Read the first failing event, then check the service resource and its dependencies; later failures can be consequences of the first one. A rollback can itself fail if resource state, permissions, or service limits prevent CloudFormation from returning to the prior state. If rollback fails, follow the stack status and AWS recovery guidance before attempting another update.

Understand CloudFormation references and ordering

CloudFormation builds a dependency graph from references in resource properties and uses it to order operations. Keeping these relationships explicit helps avoid unnecessary sequencing and avoids assuming that templates deploy every resource one at a time.

Use parameters for values that vary

Parameters let callers provide validated inputs at stack creation or update. Set allowed values or constraints when practical, and give parameters useful descriptions. Avoid using a parameter as a substitute for a separate stack boundary when the values and resources have different owners or change lifecycles.

Use intrinsic references between resources

Ref, Fn::GetAtt, and Fn::Sub can refer to other resources or their attributes. When a resource property uses one of these references, CloudFormation creates an implicit dependency: the referenced resource is created first and deleted after its dependent. Prefer these references to copying generated IDs or ARNs into templates.

Add DependsOn only for ordering the graph cannot infer

Use DependsOn when a real ordering requirement is not expressed through a resource reference, such as waiting for a VPC gateway attachment before creating a public resource. Put it on the resource whose operation must wait. DependsOn is a resource attribute, not resource Metadata; metadata stores descriptive data and does not control creation order. AWS documents the DependsOn attribute and implicit dependencies.

Reuse CloudFormation templates deliberately

Split templates when components have independent owners or change lifecycles, not only to make the source shorter. Stack boundaries add interfaces and update coordination that teams must maintain.

Use mappings for fixed lookup data

A mapping is a static lookup table in a template, suitable for fixed values such as a region-to-AMI mapping. Use parameters for inputs that vary at deployment time and a configuration service for live shared values. A nested stack does not inherit a parent's mappings. Pass the values a child needs through its Parameters property, or return values through stack outputs. See AWS documentation for mappings and nested stacks.

Use nested stacks when components share a lifecycle

A nested stack is a stack resource managed by a parent stack. The parent passes inputs explicitly and can reference child outputs. Parent updates can affect the child stack, so the parent remains responsible for its nested stack lifecycle. Keep inputs, outputs, and ownership clear.

Consider registry modules for repeated resource patterns

CloudFormation modules package reusable resource configurations that can be included in templates. Review the expanded resources and version behavior as part of the deployment review. The CloudFormation registry also supports activated third-party and private extensions, so CloudFormation can manage resource types published outside AWS; confirm extension ownership and update behavior before relying on one. See AWS's CloudFormation registry overview.

Use Terraform on AWS with an explicit state model

Terraform configuration describes resources through provider plugins. Its state is a separate record that binds each configured resource instance to one remote object. Treat that state as sensitive operational data: its contents and integrity affect future plans and applies.

Choose based on resources and operating model

Terraform may fit teams managing AWS alongside other provider-supported systems or already using Terraform across their organization. CloudFormation may fit teams that want AWS stack lifecycle and service integration. Compare the exact resource types, required extensions, team experience, state operations, and deployment controls you need rather than treating either tool as universally simpler.

Use modules to define reusable interfaces

A Terraform module groups resources behind inputs and outputs. Declare provider requirements in each module. Child modules inherit the caller's default provider configuration; pass aliased configurations explicitly. See HashiCorp guidance on provider configurations in modules. Pin remote module versions deliberately because Terraform's provider lock file does not lock module selections.

Inspect the proposed plan before applying

terraform plan refreshes information from managed objects and compares configuration with state to propose actions. Review creates, updates, replacements, and destroys against the intended change. Plans can become stale if another operation changes the state, and a reviewed plan cannot establish how every service will behave when the changes run. A difference from an out-of-band change can appear as drift; decide whether the configuration or live setting is intended before reconciling it. HashiCorp explains how to review Terraform drift.

Protect Terraform state and control deployment access

Configure remote state before team use

For a team, put state in a remote backend so runs share one record. The Terraform S3 backend stores state in S3; its DynamoDB option supplies legacy locking only. Create a dedicated private S3 bucket before initializing the backend, enable server-side encryption and bucket versioning, and enable native S3 state locking with use_lockfile = true. The native lockfile option requires Terraform 1.10 or later; the Terraform 1.10 release notes introduced S3 native state locking. Terraform's DynamoDB-based S3 locking option is deprecated. Run terraform init to configure the backend before planning or applying, and use short-lived role credentials from the CI identity or AWS credential chain instead of storing keys in configuration.

terraform {
  required_version = ">= 1.10.0, < 2.0.0"

  backend "s3" {
    bucket       = "replace-with-existing-state-bucket"
    key          = "network/production.tfstate"
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true
  }
}

Scope IAM permissions to the required state path and its .tflock object. State and saved plans can contain secret values even when Terraform redacts them from terminal output. Keep them out of source control, restrict access, and protect plan artifacts. A sensitive annotation hides values in CLI output; it does not by itself remove those values from state or plan files. Read HashiCorp's current guides to the S3 backend and permissions and sensitive data in Terraform.

Apply the exact plan that was approved

In a guarded CI workflow, create a saved plan for the exact commit under review, make its readable output available to reviewers, and apply that saved plan only after the required approval. An apply of a saved plan runs the actions it contains without another interactive approval, so protect the plan file as a sensitive deployment artifact and do not apply an outdated plan. HashiCorp explains this saved-plan workflow. The preview improves visibility; live resource changes can still fail and may leave partial results.

Separate environments and make cleanup explicit

Use separate AWS accounts where practical for development, test, and production. Give each environment its own state boundary and narrowly scoped deployment role to make cross-environment changes deliberate. Constrain Terraform, provider, and module versions. Commit .terraform.lock.hcl to preserve provider selections; Terraform does not use that file to lock remote module selections, so pin registry module versions when exact repeatability is required. Review deliberate dependency upgrades like code changes. See HashiCorp guidance on version constraints and the provider dependency lock file. If you create short-lived infrastructure for a branch, tie cleanup to an explicit policy scoped to that temporary environment. Keep production destruction behind its own reviewed workflow, separate from Git branch deletion.

Build a reviewable IaC workflow

CloudFormation and Terraform both make infrastructure changes describable and reviewable, but they use different deployment records. CloudFormation manages stack operations; Terraform relies on state, backend access, and provider behavior. In either tool, review the actual proposed operations, confirm resource ownership, and plan how to recover before approving a production change.

Key practices

  • Keep one clear owner for each resource and use the tool's import workflow when taking over existing infrastructure.
  • Inspect CloudFormation change sets or Terraform plans for replacement, deletion, access, and data effects; neither preview promises a successful or atomic deployment.
  • Protect remote Terraform state and saved plans, enable backend locking, and apply the reviewed plan through a guarded identity.
  • Keep environment state and permissions separate, and make temporary-resource cleanup deliberate.

Continue with focused guides

For practical pitfalls around ownership, validation, and Terraform state locking, read five common AWS IaC pitfalls and how to avoid them. To understand what CloudFormation drift detection can and cannot establish, see the CloudFormation drift detection guide.