For a team already committed to AWS, the Terraform versus CloudFormation question is narrower than it looks. Both provision the same underlying AWS resources through the same APIs, so the fight is not about capability. It is about who manages the state, how quickly new services get covered, what language you write, and whether you want to keep the door open to running somewhere other than AWS.
Both tools have a real case here. CloudFormation is a first-party AWS service that quietly removes an entire category of operational work. Terraform brings a better language, a much larger community, and multi-cloud reach that a given team may or may not ever use. The sections below walk through the concrete differences with working templates for both, and finish with a plain recommendation for each situation.
| CloudFormation | Terraform | |
|---|---|---|
| Vendor | AWS (first-party) | HashiCorp (now IBM) |
| Clouds | AWS only | AWS, GCP, Azure, and hundreds more via providers |
| Language | YAML or JSON (or CDK synth) | HCL |
| State | Managed by AWS, nothing to store | State file you store and lock yourself (S3 + DynamoDB, or a backend) |
| New-service coverage | Often on launch day | Usually close behind; occasionally ahead via Cloud Control |
| Rollback on failure | Automatic, built in | Manual; failed apply leaves partial state |
| Drift detection | Built-in (detect-stack-drift) | terraform plan shows drift on next run |
| Module ecosystem | Smaller (registry + CDK constructs) | Large public Registry, mature modules |
| License | Proprietary AWS service, no cost for core | BSL 1.1 |
This is the single biggest operational divergence.
CloudFormation has no state file. When you deploy a stack, AWS records what it created and tracks the mapping between your template and the live resources inside the CloudFormation service. There is nothing for you to store, encrypt, back up, or lock. Two engineers cannot corrupt state by running deploys at the same time, because the service serializes stack operations for you. For a team that has ever spent an afternoon recovering a mangled Terraform state file, this is a real relief.
Terraform keeps a state file that maps your HCL to real resource IDs. You are responsible for storing it somewhere durable, usually an S3 bucket, and for locking it so two apply runs do not clobber each other, historically with a DynamoDB table (Terraform 1.10+ can also lock via S3 directly). Get this wrong and you can end up with drifted or duplicated resources. Get it right and it is invisible, but getting it right is setup and discipline that CloudFormation does not ask of you.
The flip side: because Terraform owns its state, you can inspect it, move resources between states, import existing infrastructure, and refactor with terraform state mv. CloudFormation's state is opaque. Reorganizing which stack owns a resource is more awkward, though import and stack refactoring support have improved.
Here is the same small piece of infrastructure in both tools: a private S3 bucket and a Lambda function that has permission to read from it.
AWSTemplateFormatVersion: "2010-09-09"
Resources:
DataBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: my-app-data-bucket
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
ProcessorRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: read-bucket
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: s3:GetObject
Resource: !Sub "${DataBucket.Arn}/*"
Processor:
Type: AWS::Lambda::Function
Properties:
FunctionName: data-processor
Runtime: nodejs20.x
Handler: index.handler
Role: !GetAtt ProcessorRole.Arn
Code:
S3Bucket: my-deploy-artifacts
S3Key: processor.zip
Deploy it:
aws cloudformation deploy \ --template-file template.yaml \ --stack-name data-pipeline \ --capabilities CAPABILITY_IAM
If the Lambda creation fails, CloudFormation automatically rolls back the whole stack to its last good state. You do not get a half-created pipeline.
resource "aws_s3_bucket" "data" {
bucket = "my-app-data-bucket"
}
resource "aws_s3_bucket_public_access_block" "data" {
bucket = aws_s3_bucket.data.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_iam_role" "processor" {
name = "data-processor-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "read_bucket" {
role = aws_iam_role.processor.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.data.arn}/*"
}]
})
}
resource "aws_lambda_function" "processor" {
function_name = "data-processor"
runtime = "nodejs20.x"
handler = "index.handler"
role = aws_iam_role.processor.arn
s3_bucket = "my-deploy-artifacts"
s3_key = "processor.zip"
}
Deploy it:
terraform init terraform plan terraform apply
Two things stand out. First, HCL lets you reference resources with expressions like aws_s3_bucket.data.arn and use functions like jsonencode, which most engineers find more pleasant than CloudFormation's !GetAtt and !Sub intrinsic functions. Second, terraform plan shows you a precise diff before anything changes, which is better than CloudFormation change sets for readability, though change sets have closed much of that gap.
Notice also that Terraform split the public-access block into its own resource. This is common: the AWS provider tends to break AWS API surfaces into more granular resources than a single CloudFormation type, which is more verbose but often maps more directly to the underlying API calls.
A long-standing argument for CloudFormation is that AWS ships CloudFormation support for new services at or near launch, because it is a first-party product. This is largely true and matters if you adopt new AWS services aggressively.
The gap is narrower than it used to be. The AWS Terraform provider is actively maintained and usually adds new resources within weeks of a service GA. HashiCorp also ships the awscc provider built on the AWS Cloud Control API, which auto-generates resources from the same models CloudFormation uses, so some resource types appear there first. In practice, mainstream services are covered well by both. If you are an early adopter of niche AWS services on launch day, CloudFormation still has a slight edge.
Rollback. CloudFormation's automatic rollback on a failed deployment is a real advantage. A failed terraform apply leaves you with partially applied changes and state that no longer matches reality, and you clean it up yourself. CloudFormation returns the stack to its last healthy state without intervention.
Drift. Both handle out-of-band changes, differently. CloudFormation has explicit drift detection you trigger with aws cloudformation detect-stack-drift, which reports resources that were changed outside the stack. Terraform surfaces drift implicitly: the next terraform plan shows anything that no longer matches your configuration. Terraform's model means you see drift every time you plan, which many teams prefer. See our guide to Terraform drift for how to manage it.
Blast radius. CloudFormation stacks have hard limits (resource counts per stack, nested-stack depth) that push large estates toward many stacks with cross-stack references, which gets fiddly. Terraform's module system and workspaces give you more flexible ways to slice a large estate, at the cost of you designing that structure yourself.
HCL is purpose-built for infrastructure and most engineers ramp on it quickly. Raw CloudFormation YAML becomes hard to manage past a few hundred lines, which is exactly why AWS built the CDK: you write TypeScript, Python, Go, or Java, and it synthesizes CloudFormation templates. If you are staying on the CloudFormation engine, CDK is usually the better authoring experience. Note that CDK still deploys through CloudFormation, so every runtime tradeoff above (managed state, automatic rollback, AWS-only) still applies. For a deeper look see AWS CDK vs Terraform.
On modules, Terraform's public Registry is larger and more mature than CloudFormation's equivalents. For common patterns (a VPC, an EKS cluster, an RDS instance with sane defaults) there is usually a well-worn Terraform module. CloudFormation's reuse story runs through CDK constructs and the (smaller) public registry.
state mv, import, targeted plans).A common and reasonable answer is both: Terraform as the primary tool, with aws_cloudformation_stack or a CDK stack wrapped in for the handful of resources where CloudFormation coverage is ahead.
Both tools share the same shape: infrastructure lives in a separate language, in a separate codebase, applied by a separate tool, and your application code knows nothing about it. For an AWS-committed team whose "infrastructure" is mostly application-level primitives (a Postgres database, a queue, a cron job, object storage) there is a third option that skips the template layer entirely.
Encore is an open-source backend framework (around 12k GitHub stars, used in production by teams including Groupon) where you declare those resources directly in TypeScript or Go, and Encore Cloud provisions the real AWS services in your own account.
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { Bucket } from "encore.dev/storage/objects";
// Provisions RDS Postgres in your AWS account.
const db = new SQLDatabase("app", { migrations: "./migrations" });
// Provisions a private S3 bucket in your AWS account.
const uploads = new Bucket("uploads");
export const record = api(
{ method: "POST", path: "/events", expose: true },
async (req: { name: string }) => {
await db.exec`INSERT INTO events (name) VALUES (${req.name})`;
},
);
There is no S3 template, no IAM role YAML, and no state file. The bucket, database, and their permissions are derived from the code. Because the infrastructure lives in the application code, a coding agent reads it the same way it reads the rest of your program. Encore adds an MCP server and editor rules (encore llm-rules init) that give agents your schema, traces, and architecture as context, so agent-written backends follow consistent conventions and stay deployable.
This does not replace CloudFormation or Terraform for teams managing VPCs, networking, and IAM at scale, and that boundary is real. Those tools stay in place for anything outside Encore's backend primitives. What it removes is the template sprawl that accumulates around a typical backend, and it is most compelling on new projects where you are not already invested in an IaC estate.
Provision Postgres and object storage in your own AWS account without writing a single CloudFormation template.
The quick start walks through a first deploy in a few minutes.
If you are certain you will stay AWS-only and want zero state-file operations, CloudFormation is the lower-maintenance choice because AWS tracks state for you and rolls back failed deployments automatically. Terraform wins if you want the larger module ecosystem, a nicer language, or any chance of multi-cloud.
No. CloudFormation stores stack state inside the AWS service itself, so there is no state file to store, lock, or secure. Terraform keeps a state file that you must store in a backend such as S3 with DynamoDB locking.
Usually but not always. New AWS services often ship with CloudFormation support on or near launch day. The AWS Terraform provider is generally close behind, and some resources have historically landed in Terraform first through the AWS Cloud Control provider.
Yes. Teams commonly manage most infrastructure in Terraform and wrap a CloudFormation stack with the aws_cloudformation_stack resource when a service only has CloudFormation coverage, or use CDK (which compiles to CloudFormation) for parts of the estate.
CloudFormation itself has no charge for the core service and for AWS resource types. You pay for the AWS resources it provisions. Extension resource types and third-party registry types are billed per handler operation.
Most teams that stay on the CloudFormation engine now author with AWS CDK in TypeScript or Python and let it synthesize the templates, because raw YAML gets unwieldy past a few hundred lines. CDK still deploys through CloudFormation, so the runtime tradeoffs in this article still apply.