AWS Infrastructure as Code: CloudFormation and Terraform
Published on · Updated on
Rewrote the guide to correct CloudFormation and Terraform workflows, state handling, change safety, and current AWS infrastructure-as-code practices.
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.
| Concern | CloudFormation | Terraform |
|---|---|---|
| Configuration | JSON or YAML templates, with intrinsic functions and stack parameters | HCL configuration, providers, and reusable modules |
| Deployment record | The AWS service tracks each stack's template and managed resources | Terraform state maps configuration resource instances to remote objects |
| Review workflow | Create and inspect a change set before executing a stack update | Create and inspect a plan; optionally save that exact plan for an approved apply |
| Useful fit | AWS-focused stacks and teams that want CloudFormation-managed deployment | AWS 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.