In the modern cloud computing landscape, Infrastructure as Code (IaC) has revolutionized how organizations deploy and manage their resources. Rather than clicking through console interfaces or running manual commands, IaC allows engineers to define infrastructure in text files that can be versioned, tested, and deployed consistently. Two powerful contenders in this space are Terraform and AWS CloudFormation, each with distinct approaches and strengths.
This guide will help you understand when to choose each tool, with practical examples and considerations to inform your decision.
Before diving into specific use cases, it’s important to understand the fundamental differences between these tools.
HashiCorp’s Terraform is an open-source IaC tool that works across multiple cloud providers and services through its provider ecosystem. It uses HashiCorp Configuration Language (HCL), a declarative language designed specifically for defining infrastructure.
# Simple Terraform example defining an AWS EC2 instance
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "WebServer"
Environment = "Production"
}
}
CloudFormation is AWS’s built-in service for infrastructure automation, using either JSON or YAML templates to define resources within the AWS ecosystem.
# Equivalent CloudFormation example
Resources:
WebServer:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0c55b159cbfafe1f0
InstanceType: t2.micro
Tags:
- Key: Name
Value: WebServer
- Key: Environment
Value: Production
To make an informed choice, consider these critical differences:
Terraform supports multiple providers, including:
- AWS, Azure, Google Cloud
- Kubernetes, Docker
- GitHub, DataDog, and hundreds more
CloudFormation is limited to AWS services, with limited support for third-party resources through extensions.
Terraform maintains state files that track the real-world resources it manages:
- Can be stored locally or remotely (S3, Terraform Cloud, etc.)
- Requires careful handling for team collaboration
- Provides functionality like state locking and remote backends
CloudFormation manages state internally within the AWS service:
- No need to manage state files
- Built-in consistency and locking
- Limited visibility into the state details
Terraform uses HCL, which offers:
- More concise syntax than JSON/YAML
- First-class expressions and functions
- Consistent syntax across all providers
CloudFormation uses JSON or YAML:
- More verbose syntax, especially in JSON
- YAML improves readability
- CloudFormation-specific intrinsic functions (like
!Ref
,!GetAtt
)
Terraform uses a plan/apply workflow:
terraform plan
shows changes before applying- More granular control over resource updates
- Better handling of resource replacement
CloudFormation uses stack-based deployments:
- Change sets to preview updates
- Stack-level rollbacks on failure
- Automatic drift detection
Terraform shines in several specific scenarios:
If your organization uses multiple cloud providers or needs to manage resources beyond just AWS, Terraform provides a consistent workflow across all environments.
# AWS resources
provider "aws" {
region = "us-east-1"
}
resource "aws_s3_bucket" "data_lake" {
bucket = "enterprise-data-lake"
acl = "private"
}
# Azure resources
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "example" {
name = "example-resources"
location = "East US"
}
resource "azurerm_storage_account" "example" {
name = "examplestorageaccount"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
account_tier = "Standard"
account_replication_type = "LRS"
}
This Terraform configuration manages resources across both AWS and Azure, something impossible with CloudFormation alone.
Terraform’s module system makes it excellent for creating reusable infrastructure components.
# modules/vpc/main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = {
Name = var.vpc_name
Environment = var.environment
}
}
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.vpc_name}-public-${count.index}"
Environment = var.environment
}
}
# Usage in main configuration
module "production_vpc" {
source = "./modules/vpc"
vpc_cidr = "10.0.0.0/16"
vpc_name = "production"
environment = "production"
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
availability_zones = ["us-east-1a", "us-east-1b"]
}
module "staging_vpc" {
source = "./modules/vpc"
vpc_cidr = "10.1.0.0/16"
vpc_name = "staging"
environment = "staging"
public_subnet_cidrs = ["10.1.1.0/24", "10.1.2.0/24"]
availability_zones = ["us-east-1a", "us-east-1b"]
}
This modular approach allows for consistent infrastructure patterns with variations for different environments.
When you need to incorporate non-AWS resources into your infrastructure, Terraform’s provider ecosystem is invaluable.
provider "aws" {
region = "us-east-1"
}
provider "datadog" {
api_key = var.datadog_api_key
app_key = var.datadog_app_key
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "WebServer"
Service = "API"
}
}
resource "datadog_monitor" "cpu" {
name = "CPU Usage Monitor"
type = "metric alert"
message = "CPU usage is high on {{host.name}}"
query = "avg(last_5m):avg:system.cpu.user{service:API} by {host} > 90"
monitor_thresholds {
warning = 80
critical = 90
}
tags = ["service:API", "env:production"]
}
Here, Terraform seamlessly integrates AWS resources with Datadog monitoring—something that would require separate tools or custom resources in CloudFormation.
If your team already knows Terraform, leveraging that expertise even for AWS-only infrastructure may be more efficient than switching to CloudFormation.
module "data_lake" {
source = "./modules/data_lake"
bucket_name = "enterprise-data-lake"
environment = "production"
}
module "processing_cluster" {
source = "./modules/emr_cluster"
cluster_name = "data-processing"
instance_type = "m5.xlarge"
instance_count = 5
subnet_id = module.vpc.private_subnet_ids[0]
}
module "data_warehouse" {
source = "./modules/redshift"
cluster_identifier = "analytics"
database_name = "analytics"
master_username = var.redshift_username
master_password = var.redshift_password
node_type = "ra3.xlplus"
number_of_nodes = 3
}
This configuration leverages Terraform modules to create a complete data engineering infrastructure on AWS, using familiar Terraform patterns.
CloudFormation is the better choice in several scenarios:
For teams fully committed to AWS, CloudFormation offers the deepest integration with AWS services, often supporting new features before Terraform.
Resources:
ApiGateway:
Type: AWS::ApiGateway::RestApi
Properties:
Name: ServerlessAPI
Description: API for serverless application
LambdaFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Role: !GetAtt LambdaExecutionRole.Arn
Code:
S3Bucket: !Ref DeploymentBucket
S3Key: lambda-function.zip
Runtime: nodejs14.x
Environment:
Variables:
TABLE_NAME: !Ref DynamoDBTable
DynamoDBTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ServerlessAppData
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ApiGatewayDeployment:
Type: AWS::ApiGateway::Deployment
DependsOn: ApiMethod
Properties:
RestApiId: !Ref ApiGateway
StageName: prod
ApiResource:
Type: AWS::ApiGateway::Resource
Properties:
ParentId: !GetAtt ApiGateway.RootResourceId
PathPart: items
RestApiId: !Ref ApiGateway
ApiMethod:
Type: AWS::ApiGateway::Method
Properties:
HttpMethod: POST
ResourceId: !Ref ApiResource
RestApiId: !Ref ApiGateway
AuthorizationType: NONE
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunction.Arn}/invocations
LambdaApiGatewayPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref LambdaFunction
Principal: apigateway.amazonaws.com
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGateway}/*/POST/items
This CloudFormation template creates a complete serverless application with deep integration between API Gateway, Lambda, and DynamoDB, leveraging CloudFormation’s native understanding of these services.
If your team exclusively uses AWS services and tools like AWS CodePipeline, CloudFormation provides the most seamless integration.
Resources:
CodeRepository:
Type: AWS::CodeCommit::Repository
Properties:
RepositoryName: MyApplication
RepositoryDescription: Application code repository
BuildProject:
Type: AWS::CodeBuild::Project
Properties:
Name: MyApplicationBuild
ServiceRole: !GetAtt CodeBuildServiceRole.Arn
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
Source:
Type: CODEPIPELINE
BuildSpec: buildspec.yml
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
RoleArn: !GetAtt CodePipelineServiceRole.Arn
ArtifactStore:
Type: S3
Location: !Ref ArtifactBucket
Stages:
- Name: Source
Actions:
- Name: Source
ActionTypeId:
Category: Source
Owner: AWS
Provider: CodeCommit
Version: 1
Configuration:
RepositoryName: !GetAtt CodeRepository.Name
BranchName: main
OutputArtifacts:
- Name: SourceCode
- Name: Build
Actions:
- Name: BuildApp
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: 1
Configuration:
ProjectName: !Ref BuildProject
InputArtifacts:
- Name: SourceCode
OutputArtifacts:
- Name: BuildOutput
- Name: Deploy
Actions:
- Name: DeployToProduction
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: 1
Configuration:
StackName: MyApplicationStack
TemplatePath: BuildOutput::template.yml
Capabilities: CAPABILITY_IAM
InputArtifacts:
- Name: BuildOutput
This example creates a complete CI/CD pipeline using AWS services, with CloudFormation handling both the pipeline infrastructure and the application deployment.
CloudFormation’s built-in rollback capabilities and drift detection are valuable for teams requiring robust change management.
Resources:
MyDatabase:
Type: AWS::RDS::DBInstance
DeletionPolicy: Snapshot
UpdateReplacePolicy: Snapshot
Properties:
Engine: mysql
DBInstanceClass: db.t3.small
AllocatedStorage: 20
MasterUsername: admin
MasterUserPassword: !Ref DatabasePassword
BackupRetentionPeriod: 7
MultiAZ: true
WebServerGroup:
Type: AWS::AutoScaling::AutoScalingGroup
UpdatePolicy:
AutoScalingRollingUpdate:
MinInstancesInService: 1
MaxBatchSize: 1
PauseTime: PT5M
WaitOnResourceSignals: true
Properties:
LaunchConfigurationName: !Ref WebServerLaunchConfig
MinSize: 2
MaxSize: 5
DesiredCapacity: 2
VPCZoneIdentifier: !Ref Subnets
WebServerLaunchConfig:
Type: AWS::AutoScaling::LaunchConfiguration
Metadata:
AWS::CloudFormation::Init:
config:
files:
"/etc/cfn/cfn-hup.conf":
content: !Sub |
[main]
stack=${AWS::StackName} region=${AWS::Region} Properties: ImageId: ami-0c55b159cbfafe1f0 InstanceType: t2.micro SecurityGroups: – !Ref WebServerSecurityGroup UserData: Fn::Base64: !Sub | #!/bin/bash -xe /opt/aws/bin/cfn-signal -e $? –stack ${AWS::StackName} –resource WebServerGroup –region ${AWS::Region}
This stack uses CloudFormation’s rollback features and resource signals to ensure reliable deployment, with automatic rollback if the deployment fails.
For AWS-focused teams just starting with IaC, CloudFormation offers a gentler learning curve, especially with AWS console integration and managed state.
Resources:
WebAppBucket:
Type: AWS::S3::Bucket
Properties:
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: error.html
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
- DomainName: !GetAtt WebAppBucket.DomainName
Id: S3Origin
S3OriginConfig:
OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOAI}"
Enabled: true
DefaultRootObject: index.html
DefaultCacheBehavior:
TargetOriginId: S3Origin
ViewerProtocolPolicy: redirect-to-https
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # Managed-CachingOptimized
ViewerCertificate:
CloudFrontDefaultCertificate: true
CloudFrontOAI:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: OAI for WebApp
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref WebAppBucket
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
AWS: !Sub "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOAI}"
Action: 's3:GetObject'
Resource: !Sub "arn:aws:s3:::${WebAppBucket}/*"
Outputs:
WebsiteURL:
Description: URL for the website
Value: !Sub "https://${CloudFrontDistribution.DomainName}"
This straightforward CloudFormation template creates a complete static website hosting infrastructure with S3 and CloudFront, with outputs for easy reference.
Many organizations find value in combining both tools to leverage their respective strengths:
Use Terraform to manage the overall infrastructure across providers, while using CloudFormation for specific AWS service stacks.
# Terraform managing overall infrastructure
resource "aws_cloudformation_stack" "serverless_app" {
name = "serverless-application"
template_body = file("${path.module}/serverless-app.yaml")
parameters = {
Environment = "Production"
ApiStageName = "v1"
}
capabilities = ["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"]
}
resource "aws_s3_bucket" "data_lake" {
bucket = "global-data-lake"
acl = "private"
}
# Azure resources managed by Terraform
resource "azurerm_resource_group" "example" {
name = "example-resources"
location = "East US"
}
AWS Cloud Development Kit (CDK) generates CloudFormation templates but allows you to define them using familiar programming languages like TypeScript, Python, or Java.
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
export class DataPlatformStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Create an S3 bucket for data storage
const dataBucket = new s3.Bucket(this, 'DataBucket', {
versioned: true,
encryption: s3.BucketEncryption.S3_MANAGED,
lifecycleRules: [
{
transitions: [
{
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
transitionAfter: cdk.Duration.days(30),
},
{
storageClass: s3.StorageClass.GLACIER,
transitionAfter: cdk.Duration.days(90),
},
],
},
],
});
// Create a DynamoDB table for metadata
const metadataTable = new dynamodb.Table(this, 'MetadataTable', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'version', type: dynamodb.AttributeType.NUMBER },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
pointInTimeRecovery: true,
});
// Output the resource names
new cdk.CfnOutput(this, 'DataBucketName', {
value: dataBucket.bucketName,
description: 'The name of the S3 bucket',
});
new cdk.CfnOutput(this, 'MetadataTableName', {
value: metadataTable.tableName,
description: 'The name of the DynamoDB table',
});
}
}
This CDK code generates CloudFormation templates but offers a more developer-friendly experience.
To help you decide between Terraform and CloudFormation, consider this decision framework:
- Multi-cloud requirements
- Multiple cloud providers → Terraform
- AWS-only → Either tool (lean toward CloudFormation)
- Team skills and preferences
- Existing Terraform knowledge → Terraform
- AWS-focused team → Consider CloudFormation
- Developer-heavy team → Consider Terraform or AWS CDK
- Integration requirements
- Deep AWS service integration → CloudFormation
- Third-party services integration → Terraform
- AWS developer tools integration → CloudFormation
- State management preferences
- Prefer managed state → CloudFormation
- Need visibility and control over state → Terraform
- Workflow and process
- Need strong rollback guarantees → CloudFormation
- Prefer plan-then-apply workflow → Terraform
- Iterative development → Either tool
Let’s examine how this decision might play out in a real-world scenario:
Organization: A mid-sized financial services company Current infrastructure: Primarily AWS with some on-premises legacy systems Team: Mixed skills, with both infrastructure engineers and developers Requirements: Automated deployment for a new digital banking platform
- Multi-cloud needs: Currently AWS-focused, but potential future expansion to Azure for specific services
- Existing skills: Some engineers familiar with Terraform, developers comfortable with TypeScript
- Integration requirements: Need to integrate deeply with AWS services like Lambda, API Gateway, and DynamoDB
- Compliance needs: Strong audit trail and rollback capabilities required for financial regulations
The company decides on a hybrid approach:
- Use Terraform for foundational infrastructure (VPCs, networking, IAM, core databases)
- Use AWS CDK for application-specific AWS resources, leveraging developers’ TypeScript skills
- This approach:
- Prepares for potential multi-cloud expansion with Terraform
- Leverages AWS-native features through CloudFormation (generated by CDK)
- Allows developers to contribute directly to infrastructure code
- Maintains compliance with strong change management
Both Terraform and CloudFormation are powerful Infrastructure as Code tools with distinct strengths:
- Terraform excels in multi-cloud environments, offering a consistent workflow across providers and a rich ecosystem of integrations. Its modular approach and expressive HCL syntax make it ideal for complex infrastructure.
- AWS CloudFormation provides the deepest integration with AWS services, with built-in state management, rollback capabilities, and drift detection. It’s a natural choice for AWS-centric organizations.
The “right” choice depends on your specific requirements, team skills, and long-term infrastructure strategy. Many organizations find value in hybrid approaches that leverage the strengths of both tools.
Regardless of which tool you choose, adopting Infrastructure as Code represents a significant step forward in managing cloud resources efficiently, consistently, and securely.
Keywords: Terraform, AWS CloudFormation, Infrastructure as Code, IaC, cloud automation, AWS CDK, multi-cloud, DevOps, cloud infrastructure, HCL, YAML, AWS, HashiCorp, configuration management, cloud deployment
#Terraform #CloudFormation #InfrastructureAsCode #IaC #AWS #DevOps #CloudComputing #Automation #MultiCloud #AWSCDK #CloudDeployment #HashiCorp #CloudInfrastructure #DevOpsTools #TerraformVsCloudFormation