Introduction
Clicking through a cloud console works for a first experiment. It stops working the moment you need the same environment in staging and production, a repeatable disaster recovery region, or an audit trail of who changed a security group and when. Infrastructure as Code (IaC) solves this by describing infrastructure in version-controlled text files that can be reviewed, tested, and applied the same way every time.
On AWS, the two dominant options are Terraform (HashiCorp, cloud-agnostic) and AWS CloudFormation (AWS-native). Both are declarative: you describe the desired end state, and the tool works out what to create, update, or delete. The differences show up in how they manage state, how they handle multiple clouds, and how they fit into your existing tooling.
This article walks through the core concepts, builds the same small network and storage stack in both tools, and ends with guidance on choosing between them.
Core Concepts Shared by Both Tools
Regardless of the tool, a solid IaC workflow rests on a few ideas:
- Declarative desired state. You describe what should exist, not the sequence of API calls to create it.
- Plan before apply. Both tools can show a diff of proposed changes before touching real resources. Treat that step as a code review for infrastructure.
- State tracking. The tool must know which real resources map to which definitions. Terraform keeps this in a state file; CloudFormation keeps it inside the stack itself.
- Idempotency. Applying the same definition twice should produce no changes the second time.
- Drift. Resources changed outside the tool (for example, through the console) create a mismatch between code and reality. Detecting and resolving drift is an ongoing operational concern.
Terraform: Multi-Cloud and Provider-Based
Terraform uses HashiCorp Configuration Language (HCL) and a plugin model called providers. The AWS provider maps HCL resources to AWS APIs, and other providers cover Azure, GCP, Kubernetes, and many SaaS products, which makes Terraform a good fit when your estate spans more than one platform.
Strengths
- Cloud-agnostic syntax. Once your team knows HCL, the same workflow applies across providers.
- Modules. Reusable building blocks, published in a registry or kept in your own repositories, encapsulate standards such as tagging, encryption defaults, and logging.
- Rich ecosystem. Linters (tflint), security scanners (checkov, tfsec), and testing frameworks (terraform test) are mature.
- Explicit state control. Remote backends such as S3 with DynamoDB locking, or Terraform Cloud, give you fine-grained control over who can apply what.
Costs
- You own the state. A misconfigured backend, a lost state file, or concurrent applies can corrupt your view of reality. Remote state with locking is not optional in a team setting.
-
Provider versions drift. Pin provider versions in
required_providersand upgrade deliberately.
Example: VPC and an encrypted S3 bucket in Terraform
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "acme-terraform-state"
key = "network/prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "acme-terraform-locks"
encrypt = true
}
}
provider "aws" {
region = var.region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "terraform"
}
}
}
variable "region" {
type = string
default = "us-east-1"
}
variable "environment" {
type = string
default = "prod"
}
variable "vpc_cidr" {
type = string
default = "10.0.0.0/16"
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.environment}-main-vpc"
}
}
data "aws_caller_identity" "current" {}
resource "aws_s3_bucket" "artifacts" {
bucket = "${var.environment}-acme-artifacts-${data.aws_caller_identity.current.account_id}"
}
resource "aws_s3_bucket_server_side_encryption_configuration" "artifacts" {
bucket = aws_s3_bucket.artifacts.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "artifacts" {
bucket = aws_s3_bucket.artifacts.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
output "vpc_id" {
value = aws_vpc.main.id
}
output "artifacts_bucket" {
value = aws_s3_bucket.artifacts.bucket
}
Typical workflow:
terraform init
terraform plan -out=tfplan
terraform apply tfplan
Saving the plan to a file and applying that exact file guarantees that what was reviewed is what gets executed.
CloudFormation: AWS-Native and Managed
CloudFormation describes resources in JSON or YAML templates and deploys them as stacks. AWS holds the state for you, rolls back failed stacks automatically by default, and integrates directly with IAM, Service Catalog, StackSets (for multi-account and multi-region rollouts), and AWS Control Tower.
Strengths
- No state file to manage. AWS tracks the stack state, so there is no backend to configure or lock.
- Automatic rollback. A failed update returns the stack to its last stable state.
- Deep AWS integration. New AWS services and features usually appear in CloudFormation resource types quickly, and it is the foundation for AWS Serverless Application Model (SAM) and CDK.
- Change sets. They provide a plan-like preview before any change is applied.
- Native governance. Drift detection, stack policies, and termination protection are built in.
Costs
- AWS-only. Multi-cloud estates need a second tool.
- Verbose templates. YAML or JSON can become long, though nested stacks and modules help.
- Slower feedback on some resources. Stack operations can take a long time, and rollbacks on large stacks can be painful to debug.
Example: the same VPC and encrypted S3 bucket in CloudFormation
AWSTemplateFormatVersion: "2010-09-09"
Description: Network and artifact storage for the production environment
Parameters:
Environment:
Type: String
Default: prod
AllowedValues: [dev, staging, prod]
VpcCidr:
Type: String
Default: 10.0.0.0/16
Resources:
MainVpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub "${Environment}-main-vpc"
- Key: Environment
Value: !Ref Environment
- Key: ManagedBy
Value: cloudformation
ArtifactsBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
BucketName: !Sub "${Environment}-acme-artifacts-${AWS::AccountId}"
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
Tags:
- Key: Environment
Value: !Ref Environment
Outputs:
VpcId:
Value: !Ref MainVpc
ArtifactsBucket:
Value: !Ref ArtifactsBucket
Deploy it with a change set so you can review the plan first:
aws cloudformation deploy \
--template-file network.yaml \
--stack-name prod-network \
--parameter-overrides Environment=prod \
--no-fail-on-empty-changeset \
--capabilities CAPABILITY_IAM
Note the DeletionPolicy: Retain on the bucket. Without it, deleting the stack would delete the bucket and its contents. This is one of the most important safety settings in CloudFormation and is easy to forget.
Side-by-Side Comparison
| Aspect | Terraform | CloudFormation |
|---|---|---|
| Scope | Multi-cloud, many providers | AWS only |
| Language | HCL | YAML or JSON |
| State | You manage it (usually S3 + DynamoDB) | AWS manages it |
| Preview | terraform plan |
Change sets |
| Failure handling | Partial state possible; you fix and re-apply | Automatic rollback by default |
| Reuse | Modules | Nested stacks, modules, CDK constructs |
| Drift detection |
terraform plan shows drift |
Built-in drift detection |
| Multi-account rollout | Requires pipeline or workspaces | StackSets |
Practical Guidance: Choosing Between Them
Choose Terraform when:
- Your infrastructure spans AWS and at least one other platform.
- Your team already uses HCL or wants a single tool across the organization.
- You want a rich module ecosystem and strong community tooling.
Choose CloudFormation when:
- You are all-in on AWS and want zero state management overhead.
- You need native multi-account and multi-region rollouts through StackSets or Control Tower.
- You want automatic rollback and AWS-supported governance features.
Many organizations use both: CloudFormation or Control Tower for the account baseline, and Terraform for application infrastructure. That split can work well as long as ownership boundaries are clear, so that no resource is managed by two tools at once.
Best Practices That Apply to Either Tool
- Keep everything in version control and require pull requests for infrastructure changes.
- Run plans in CI and attach the output to the pull request so reviewers see the exact impact.
-
Protect stateful resources with lifecycle rules in Terraform (
prevent_destroy) or deletion policies in CloudFormation (Retain). - Use consistent tagging through default tags or shared modules, so cost allocation and ownership are traceable.
- Scan templates before deployment with checkov, cfn-lint, or tfsec.
- Separate environments into different accounts or state files, and never share state between dev and production.
- Apply with least-privilege roles dedicated to the deployment pipeline, not with administrator credentials on a laptop.
- Detect and reconcile drift on a schedule, and treat manual console changes as exceptions that must be codified afterward.
Conclusion and Key Takeaways
- Infrastructure as Code turns infrastructure into reviewable, repeatable, versioned artifacts.
- Terraform offers multi-cloud reach and a rich module ecosystem, at the cost of managing state yourself.
- CloudFormation offers AWS-native integration, managed state, and automatic rollback, but it is limited to AWS.
- The same VPC and encrypted bucket can be expressed in both; the mental model is the same even though the syntax differs.
- Whichever you choose, plan before apply, protect stateful resources, and keep infrastructure changes in code review.
If you have questions or have run either tool at scale, I would like to hear what worked and what did not.
Top comments (0)