Terraform vs CloudFormation 2024: Complete Infrastructure as Code Comparison Guide
Comprehensive comparison of Terraform and AWS CloudFormation with 6+ real code examples. Master state management, multi-cloud strategies, module patterns, CI/CD integration, drift detection, cost analysis, and migration strategies to choose the right IaC tool.
Introduction: The IaC Decision That Shapes Your Cloud Strategy
Infrastructure as Code (IaC) is no longer optional—it's fundamental to modern cloud operations. Yet choosing between Terraform and AWS CloudFormation remains one of the most impactful decisions teams make when adopting cloud infrastructure management. Both tools have evolved significantly since their inception, each offering distinct advantages for different organizational needs.
This comprehensive guide provides a deep dive into both tools, comparing them across architecture, syntax, state management, team workflows, cost implications, and migration strategies. Whether you're evaluating for a new project, migrating existing infrastructure, or establishing enterprise-wide standards, this guide will help you make the right choice.
Part 1: Understanding Infrastructure as Code Fundamentals
Why Infrastructure as Code Matters
Infrastructure as Code transforms how teams manage cloud resources by enabling:
- Repeatability and Consistency: Define infrastructure once, deploy identically across environments
- Version Control: Track infrastructure changes like application code with full audit trails
- Team Collaboration: Enable multiple engineers to work on infrastructure safely through review processes
- Disaster Recovery: Quickly rebuild entire environments from code after failures
- Cost Control: Identify unused resources, prevent configuration drift, and optimize spending
- Compliance Automation: Embed security policies and compliance checks into your infrastructure code
Without IaC, teams resort to manual configuration, leading to "snowflake servers," inconsistent deployments, and security gaps.
The Three Types of IaC Tools
Before comparing Terraform and CloudFormation, understand the landscape:
-
Declarative IaC (What should exist): You define the desired end state; the tool figures out how to achieve it. Both Terraform and CloudFormation are primarily declarative.
-
Imperative IaC (How to create it): You script the exact steps to create infrastructure. Examples: Ansible, custom shell scripts.
-
Templated IaC (Structured templates): Mix declarative and imperative approaches. CloudFormation uses JSON/YAML templates with intrinsic functions.
Part 2: AWS CloudFormation Deep Dive
CloudFormation Architecture and Core Concepts
AWS CloudFormation is Amazon's managed Infrastructure as Code service, launched in 2011. It treats infrastructure as templates that deploy as logical units called "stacks."
Key Architecture Components:
- Templates: JSON or YAML files describing AWS resources and their properties
- Stacks: Logical units of resources deployed from a single template
- Stack Policies: Rules governing what changes are allowed to stack resources
- Change Sets: Preview infrastructure changes before applying them
- Drift Detection: Identify manual changes that deviate from the template definition
Native State Management:
CloudFormation manages state internally within AWS. There's no separate state file to version or secure—a significant advantage over Terraform for teams uncomfortable managing state files.
CloudFormation Syntax: YAML and JSON
CloudFormation templates have four main sections:
AWSTemplateFormatVersion: '2010-09-09'
Description: Complete example of CloudFormation template structure
Metadata:
AWS::CloudFormation::Init:
config: {}
Parameters:
EnvironmentName:
Type: String
Default: production
Mappings:
RegionMap:
us-east-1:
AMI: ami-12345678
Conditions:
IsProduction: !Equals [!Ref EnvironmentName, 'production']
Resources:
MyResource:
Type: AWS::S3::Bucket
Properties:
BucketName: my-bucket
Outputs:
BucketName:
Value: !Ref MyResource
Export:
Name: MyBucketName
Part 3: HashiCorp Terraform Deep Dive
Terraform Architecture and Core Concepts
Terraform, released in 2014, pioneered multi-cloud infrastructure provisioning with a provider-agnostic approach. It separates configuration from state and operations.
Key Architecture Components:
- Configuration Files: HCL (HashiCorp Configuration Language) files describing desired infrastructure
- State File: JSON file tracking your actual infrastructure for comparison with configuration
- Providers: Plugins that interface with cloud platforms (AWS, Azure, Google Cloud, etc.)
- Modules: Reusable packages of Terraform configurations
- Workspaces: Named state file variants for managing multiple environments
Explicit State Management:
Terraform maintains a state file (local or remote) that maps your configuration to actual resources. This enables accurate change planning but requires careful state management practices.
Terraform Syntax: HashiCorp Configuration Language
HCL provides readable, JSON-compatible syntax optimized for infrastructure:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
variable "environment" {
type = string
default = "production"
description = "Environment name"
}
locals {
common_tags = {
Environment = var.environment
ManagedBy = "Terraform"
}
}
resource "aws_s3_bucket" "example" {
bucket = "my-bucket-${var.environment}"
tags = local.common_tags
}
output "bucket_name" {
value = aws_s3_bucket.example.id
description = "Name of the S3 bucket"
}
Part 4: Side-by-Side Code Examples (8 Real-World Scenarios)
Example 1: Simple S3 Bucket with Versioning
CloudFormation (YAML):
Resources:
ApplicationBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: my-app-bucket-prod
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
Outputs:
BucketName:
Value: !Ref ApplicationBucket
Export:
Name: AppBucketName
Terraform (HCL):
resource "aws_s3_bucket" "application" {
bucket = "my-app-bucket-prod"
tags = {
Name = "Application Bucket"
Environment = "production"
}
}
resource "aws_s3_bucket_versioning" "application" {
bucket = aws_s3_bucket.application.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "application" {
bucket = aws_s3_bucket.application.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "application" {
bucket = aws_s3_bucket.application.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
output "bucket_name" {
value = aws_s3_bucket.application.id
description = "Name of the application S3 bucket"
}
Key Differences:
- CloudFormation uses intrinsic functions and YAML structure; Terraform separates concerns into multiple resources
- Terraform requires explicit resource creation for versioning and encryption; CloudFormation nests them in properties
- Terraform references are explicit (aws_s3_bucket.application.id); CloudFormation uses intrinsic functions (!Ref)
Example 2: VPC with Subnets and NAT Gateway
CloudFormation (YAML):
Resources:
ProductionVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
PublicSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref ProductionVPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: true
PrivateSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref ProductionVPC
CidrBlock: 10.0.2.0/24
AvailabilityZone: !Select [0, !GetAZs '']
InternetGateway:
Type: AWS::EC2::InternetGateway
AttachIGW:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref ProductionVPC
InternetGatewayId: !Ref InternetGateway
EIP:
Type: AWS::EC2::EIP
DependsOn: AttachIGW
Properties:
Domain: vpc
NATGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt EIP.AllocationId
SubnetId: !Ref PublicSubnet
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref ProductionVPC
PublicRoute:
Type: AWS::EC2::Route
DependsOn: AttachIGW
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
AssociatePublicSubnet:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet
RouteTableId: !Ref PublicRouteTable
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref ProductionVPC
PrivateRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NATGateway
AssociatePrivateSubnet:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet
RouteTableId: !Ref PrivateRouteTable
Terraform (HCL):
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_vpc" "production" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "production-vpc"
}
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.production.id
cidr_block = "10.0.1.0/24"
availability_zone = data.aws_availability_zones.available.names[0]
tags = {
Name = "public-subnet"
}
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.production.id
cidr_block = "10.0.2.0/24"
availability_zone = data.aws_availability_zones.available.names[0]
tags = {
Name = "private-subnet"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.production.id
tags = {
Name = "main-igw"
}
}
resource "aws_eip" "nat" {
domain = "vpc"
tags = {
Name = "nat-eip"
}
depends_on = [aws_internet_gateway.main]
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public.id
tags = {
Name = "main-nat"
}
depends_on = [aws_internet_gateway.main]
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.production.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "public-rt"
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.production.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
tags = {
Name = "private-rt"
}
}
resource "aws_route_table_association" "private" {
subnet_id = aws_subnet.private.id
route_table_id = aws_route_table.private.id
}
Key Differences:
- CloudFormation uses !Select and !GetAZs intrinsic functions; Terraform uses data sources and interpolation
- CloudFormation requires explicit dependency management with DependsOn; Terraform infers dependencies automatically
- Terraform's approach is more modular, separating each resource concern; CloudFormation nests related configuration
Example 3: EC2 Instance with IAM Role
CloudFormation (YAML):
Resources:
EC2Role:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
EC2InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Path: /
Roles:
- !Ref EC2Role
EC2Instance:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0c55b159cbfafe1f0
InstanceType: t3.medium
IamInstanceProfile: !Ref EC2InstanceProfile
SubnetId: !Ref PrivateSubnet
MetadataOptions:
HttpTokens: required
HttpPutResponseHopLimit: 1
Tags:
- Key: Name
Value: ApplicationServer
Terraform (HCL):
resource "aws_iam_role" "ec2_role" {
name = "ec2-instance-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy_attachment" "ssm_policy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
role = aws_iam_role.ec2_role.name
}
resource "aws_iam_instance_profile" "ec2_profile" {
role = aws_iam_role.ec2_role.name
}
resource "aws_instance" "application" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
subnet_id = aws_subnet.private.id
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
metadata_options {
http_tokens = "required"
http_put_response_hop_limit = 1
http_endpoint = "enabled"
}
tags = {
Name = "ApplicationServer"
}
}
Example 4: RDS PostgreSQL Database
CloudFormation (YAML):
Resources:
DBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: Subnet group for RDS
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
RDSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for RDS
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
CidrIp: 10.0.0.0/16
RDSDatabase:
Type: AWS::RDS::DBInstance
DeletionPolicy: Snapshot
Properties:
DBInstanceIdentifier: production-db
DBInstanceClass: db.t3.small
Engine: postgres
EngineVersion: '15.3'
MasterUsername: postgres
MasterUserPassword: !Sub 'arn:aws:secretsmanager:us-east-1:123456789012:secret:db-password'
AllocatedStorage: '100'
StorageType: gp3
StorageEncrypted: true
MultiAZ: true
DBSubnetGroupName: !Ref DBSubnetGroup
VPCSecurityGroups:
- !Ref RDSSecurityGroup
BackupRetentionPeriod: 30
PreferredBackupWindow: 03:00-04:00
PreferredMaintenanceWindow: sun:04:00-sun:05:00
EnableCloudwatchLogsExports:
- postgresql
Outputs:
DBEndpoint:
Value: !GetAtt RDSDatabase.Endpoint.Address
Terraform (HCL):
resource "aws_db_subnet_group" "main" {
name = "main-db-subnet-group"
subnet_ids = [aws_subnet.private1.id, aws_subnet.private2.id]
}
resource "aws_security_group" "rds" {
name = "rds-security-group"
vpc_id = aws_vpc.production.id
description = "Security group for RDS"
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_rds_cluster" "main" {
cluster_identifier = "production-db-cluster"
engine = "aurora-postgresql"
engine_version = "15.3"
database_name = "appdb"
master_username = "postgres"
master_password = var.db_password
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
storage_encrypted = true
backup_retention_period = 30
preferred_backup_window = "03:00-04:00"
enabled_cloudwatch_logs_exports = ["postgresql"]
skip_final_snapshot = false
final_snapshot_identifier = "production-db-final-snapshot"
}
Example 5: Lambda Function with Environment Variables
CloudFormation (YAML):
Resources:
LambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
LambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: process-events
Runtime: python3.11
Role: !GetAtt LambdaRole.Arn
Handler: index.lambda_handler
Timeout: 60
MemorySize: 256
Environment:
Variables:
ENVIRONMENT: production
LOG_LEVEL: INFO
API_ENDPOINT: https://api.example.com
Code:
ZipFile: |
import json
def lambda_handler(event, context):
return {
'statusCode': 200,
'body': json.dumps('Hello from Lambda!')
}
Outputs:
FunctionArn:
Value: !GetAtt LambdaFunction.Arn
Terraform (HCL):
resource "aws_iam_role" "lambda_role" {
name = "lambda-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy_attachment" "lambda_basic" {
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
role = aws_iam_role.lambda_role.name
}
resource "aws_lambda_function" "process_events" {
filename = "lambda_function.zip"
function_name = "process-events"
role = aws_iam_role.lambda_role.arn
handler = "index.lambda_handler"
timeout = 60
memory_size = 256
runtime = "python3.11"
environment {
variables = {
ENVIRONMENT = "production"
LOG_LEVEL = "INFO"
API_ENDPOINT = "https://api.example.com"
}
}
}
output "function_arn" {
value = aws_lambda_function.process_events.arn
}
Example 6: IAM User with Access Keys and Policies
CloudFormation (YAML):
Resources:
ApplicationUser:
Type: AWS::IAM::User
Properties:
UserName: app-deploy-user
Policies:
- PolicyName: S3DeployAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
Resource: arn:aws:s3:::deployment-bucket/*
- Effect: Allow
Action:
- s3:ListBucket
Resource: arn:aws:s3:::deployment-bucket
UserAccessKey:
Type: AWS::IAM::AccessKey
Properties:
UserName: !Ref ApplicationUser
Outputs:
AccessKeyId:
Value: !Ref UserAccessKey
SecretAccessKey:
Value: !GetAtt UserAccessKey.SecretAccessKey
Terraform (HCL):
resource "aws_iam_user" "app_deploy" {
name = "app-deploy-user"
}
resource "aws_iam_user_policy" "s3_deploy_access" {
name = "s3-deploy-access"
user = aws_iam_user.app_deploy.name
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
]
Resource = "arn:aws:s3:::deployment-bucket/*"
},
{
Effect = "Allow"
Action = [
"s3:ListBucket"
]
Resource = "arn:aws:s3:::deployment-bucket"
}
]
})
}
resource "aws_iam_access_key" "app_deploy" {
user = aws_iam_user.app_deploy.name
}
output "access_key_id" {
value = aws_iam_access_key.app_deploy.id
}
output "secret_access_key" {
value = aws_iam_access_key.app_deploy.secret
sensitive = true
}
Example 7: Auto Scaling Group with Launch Template
CloudFormation (YAML):
Resources:
LaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateName: app-launch-template
LaunchTemplateData:
ImageId: ami-0c55b159cbfafe1f0
InstanceType: t3.small
KeyName: my-key-pair
UserData: !Base64 |
#!/bin/bash
yum update -y
yum install -y amazon-cloudwatch-agent
TagSpecifications:
- ResourceType: instance
Tags:
- Key: Name
Value: AppServer
AutoScalingGroup:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
AutoScalingGroupName: app-asg
VPCZoneIdentifier:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
LaunchTemplate:
LaunchTemplateId: !Ref LaunchTemplate
Version: !GetAtt LaunchTemplate.LatestVersionNumber
MinSize: 2
MaxSize: 10
DesiredCapacity: 3
HealthCheckType: ELB
HealthCheckGracePeriod: 300
Tags:
- Key: Name
Value: AppAutoScaled
PropagateAtLaunch: true
ScaleUpPolicy:
Type: AWS::AutoScaling::ScalingPolicy
Properties:
AutoScalingGroupName: !Ref AutoScalingGroup
PolicyType: TargetTrackingScaling
TargetTrackingConfiguration:
PredefinedMetricSpecification:
PredefinedMetricType: ASGAverageCPUUtilization
TargetValue: 70.0
Terraform (HCL):
resource "aws_launch_template" "app" {
name_prefix = "app-launch-"
image_id = "ami-0c55b159cbfafe1f0"
instance_type = "t3.small"
key_name = "my-key-pair"
user_data = base64encode(<<-EOF
#!/bin/bash
yum update -y
yum install -y amazon-cloudwatch-agent
EOF
)
tag_specifications {
resource_type = "instance"
tags = {
Name = "AppServer"
}
}
}
resource "aws_autoscaling_group" "app" {
name = "app-asg"
vpc_zone_identifier = [aws_subnet.private1.id, aws_subnet.private2.id]
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
min_size = 2
max_size = 10
desired_capacity = 3
health_check_type = "ELB"
health_check_grace_period = 300
tag {
key = "Name"
value = "AppAutoScaled"
propagate_at_launch = true
}
}
resource "aws_autoscaling_policy" "scale_up" {
name = "app-scale-up"
scaling_adjustment = 1
adjustment_type = "ChangeInCapacity"
cooldown = 300
autoscaling_group_name = aws_autoscaling_group.app.name
}
resource "aws_cloudwatch_metric_alarm" "cpu_high" {
alarm_name = "app-cpu-high"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 300
statistic = "Average"
threshold = 70
alarm_actions = [aws_autoscaling_policy.scale_up.arn]
}
Example 8: DynamoDB Table with Global Secondary Index
CloudFormation (YAML):
Resources:
ApplicationTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: application-data
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
- AttributeName: GSI1PK
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
GlobalSecondaryIndexes:
- IndexName: GSI1
Keys:
- AttributeName: GSI1PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
Projection:
ProjectionType: ALL
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
Tags:
- Key: Environment
Value: production
Outputs:
TableArn:
Value: !GetAtt ApplicationTable.Arn
Terraform (HCL):
resource "aws_dynamodb_table" "application" {
name = "application-data"
billing_mode = "PAY_PER_REQUEST"
hash_key = "PK"
range_key = "SK"
attribute {
name = "PK"
type = "S"
}
attribute {
name = "SK"
type = "S"
}
attribute {
name = "GSI1PK"
type = "S"
}
global_secondary_index {
name = "GSI1"
hash_key = "GSI1PK"
range_key = "SK"
projection_type = "ALL"
}
stream_specification {
stream_view_type = "NEW_AND_OLD_IMAGES"
}
tags = {
Environment = "production"
}
}
output "table_arn" {
value = aws_dynamodb_table.application.arn
}
Part 5: Advanced Comparisons
State Management: The Critical Difference
CloudFormation State:
- Managed entirely by AWS
- No state file to manage or protect
- State consistency guaranteed by AWS
- Drift detection via describe-stack-resource-drifts API
Terraform State:
- Stored locally by default (dangerous in teams)
- Must be stored remotely (S3, Terraform Cloud, etc.)
- State locking prevents concurrent modifications
- Team collaboration requires robust state management
State management is arguably Terraform's biggest operational burden, but also its greatest strength for complex multi-cloud scenarios.
Modules vs Nested Stacks
CloudFormation Nested Stacks:
Resources:
NetworkStack:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.amazonaws.com/bucket/network.yaml
Parameters:
VPCCidr: 10.0.0.0/16
Nested stacks create dependencies between templates but remain single CloudFormation stacks at the top level.
Terraform Modules:
module "network" {
source = "./modules/network"
vpc_cidr = "10.0.0.0/16"
environment = var.environment
}
Modules are more flexible—they can come from local paths, Git repositories, Terraform Registry, or HTTP sources. They're also more composable.
Multi-Cloud and Multi-Region
Terraform Excels at:
- Single HCL codebase managing AWS, Azure, GCP simultaneously
- Consistent provider interface across clouds
- Switching providers with minimal code changes
CloudFormation:
- AWS-only, requiring separate tools for other clouds
- Excellent for AWS-specific features (StackSets for multi-region)
- Limited by AWS service availability
For true multi-cloud initiatives, Terraform is significantly easier to manage.
CI/CD Pipeline Integration
CloudFormation Integration:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::CodePipeline-CloudFormation
Description: Deploying via CodePipeline
Parameters:
ChangeSetType:
Type: String
Default: CREATE
CloudFormation integrates directly with AWS CodePipeline, CodeDeploy, and CodeBuild.
Terraform Integration:
# Typical Terraform CI/CD workflow
# Plan stage
terraform plan -out=tfplan
# Review/Approval
# Manual approval in CI/CD system
# Apply stage
terraform apply tfplan
Terraform works with any CI/CD system (GitLab CI, GitHub Actions, Jenkins, CircleCI, etc.) but requires more manual workflow configuration.
Testing and Validation
CloudFormation Validation:
# Syntax validation
aws cloudformation validate-template --template-body file://template.yaml
# Best practices analysis
cfn-lint template.yaml
Terraform Testing:
# Syntax and validation
terraform validate
# Format checking
terraform fmt -check
# Complex testing with Terratest (Go)
go test -v
Terraform enables more sophisticated testing through frameworks like Terratest, enabling pre-deployment validation of infrastructure behavior.
Drift Detection and Remediation
CloudFormation Drift Detection:
# Detect drift
aws cloudformation detect-stack-drift --stack-name my-stack
# View drift details
aws cloudformation describe-stack-resource-drifts --stack-name my-stack
CloudFormation provides native drift detection showing exactly what manual changes were made.
Terraform Drift Detection:
# Refresh state
terraform refresh
# Plan shows drift
terraform plan
# Remediate by applying
terraform apply
Terraform's terraform plan command shows drift but requires re-applying to fix it, which can be dangerous in production.
Part 6: Migration Strategies
Migrating from CloudFormation to Terraform
Step 1: Inventory Your Stacks
aws cloudformation list-stacks --query 'StackSummaries[?StackStatus!=DELETE_COMPLETE].StackName' --output table
Step 2: Convert Templates
Use tools like cf2tf or manual conversion:
cf2tf convert --template my-template.yaml > main.tf
Step 3: Import Resources
# Write Terraform resource blocks
resource "aws_s3_bucket" "example" {
# Properties will be filled in by import
}
# Import existing resource
terraform import aws_s3_bucket.example my-bucket-name
Step 4: Incremental Migration
Migrate stacks one-at-a-time, testing thoroughly before moving to the next.
Migrating from Terraform to CloudFormation
This is significantly more challenging due to CloudFormation's limitations. Generally:
- Keep Terraform managing multi-cloud resources
- Use CloudFormation only for AWS-specific infrastructure
- Integrate both tools in your CI/CD pipeline
Part 7: Cost Considerations
CloudFormation Costs:
- Completely free for use
- Pay only for AWS resources deployed
- No additional operational overhead
Terraform Costs:
- Terraform Core: Free
- Terraform Cloud/Enterprise: $20-70/month per user for remote state, team management
- Self-hosted state backend (S3): ~$1-5/month for typical workloads
For small teams, Terraform state management costs are minimal. For enterprise deployments, Terraform Cloud/Enterprise adds significant expense.
Part 8: When to Choose Each Tool
Choose CloudFormation When:
- AWS-Only Environment: Your organization uses only AWS
- Native AWS Features Matter: You need deep AWS service integration
- Managed State: You want AWS handling state completely
- Change Sets: You require preview capabilities before applying changes
- Control Tower: You're using AWS Control Tower for governance
- Team Unfamiliar with Code: Team members prefer JSON/YAML over HCL
- Minimal Operational Overhead: Small teams wanting minimal infrastructure tooling
Choose Terraform When:
- Multi-Cloud Requirements: Managing AWS, Azure, and GCP simultaneously
- Provider Ecosystem: You need providers CloudFormation doesn't support
- Complex Module Reuse: Extensive code reusability across projects
- Existing Terraform Investment: Team already experienced with Terraform
- Advanced Testing: Requiring sophisticated pre-deployment validation
- Team Preference: Engineers prefer HCL syntax and Terraform workflows
- Long-term Flexibility: Anticipating multi-cloud needs in the future
Choose Both:
Many organizations successfully use:
- CloudFormation for core AWS infrastructure (VPCs, security groups, IAM)
- Terraform for application-level resources and multi-cloud integrations
- Complementary workflows in CI/CD pipelines
Working with Warqline
We are a cloud engineering consultancy and an official AWS and Google Cloud partner. If you are running this in production and want a second pair of eyes, we scope work in a free 45-minute technical call: you describe what you are running and what worries you, and we tell you what we would look at first.
Conclusion: Making Your IaC Tool Decision
Both Terraform and CloudFormation are mature, production-ready Infrastructure as Code tools. The best choice depends on your specific organizational context:
- Terraform is superior for multi-cloud scenarios, team flexibility, and organizations already invested in the Terraform ecosystem
- CloudFormation is superior for AWS-only organizations that want minimal operational overhead and AWS-integrated workflows
Consider:
- Your current cloud strategy (AWS-only vs. multi-cloud)
- Team expertise and preferences
- Organizational standardization requirements
- Long-term infrastructure vision
Whatever you choose, implement strong version control, testing, and approval processes. The tool matters less than the discipline of treating infrastructure as code with the same rigor as application code.
For organizations uncertain about their choice, remember that modern CI/CD pipelines can integrate both tools. You're not locked into a single decision—you can evolve your tooling as your infrastructure needs mature.
Start your infrastructure optimization journey with Warqline, which assesses and recommends improvements regardless of which IaC tool you use.