A practitioner's comparison of Terraform and CloudFormation on AWS covering state, drift, new-service lag, rollback, and where CDK fits.
Every AWS team eventually hits this fork. You need infrastructure as code, and the two obvious candidates are HashiCorp Terraform and AWS CloudFormation. Both provision real resources from declarative files. Both are mature. The wrong choice will not sink you, but it will shape how your team works for years, so it is worth getting right the first time.
Here is the short version before we dig in: if you are all-in on AWS and want zero extra tooling, CloudFormation is a defensible default. If you touch more than one cloud, or you want the module ecosystem and the ergonomics, Terraform usually wins. The details below are where the decision actually lives.
This is the cleanest dividing line. CloudFormation only provisions AWS resources. That is not a weakness so much as a scope decision. If your entire estate is AWS and you have no plans to change that, being AWS-only is fine, and it buys you first-party integration.
Terraform speaks to hundreds of providers through one workflow: AWS, Cloudflare, Datadog, GitHub, PagerDuty, Kubernetes, and so on. Most real teams provision more than just AWS resources. You configure a DNS record in Cloudflare, a monitor in Datadog, and a bucket in AWS in the same plan. CloudFormation cannot do that. Once you have any non-AWS resource under management, Terraform stops being optional.
CloudFormation manages state for you. AWS tracks what each stack owns on the service side. There is no state file to store, lock, or corrupt. That is genuinely less operational burden, and it is the feature people underrate most when they first compare the two.
Terraform keeps its own state file, and you are responsible for it. In practice that means a remote backend such as S3 with a DynamoDB lock table (or the newer S3-native locking), so concurrent runs do not stomp each other. State can drift from reality, get corrupted, or leak secrets if you are careless. The upside is that the state file is inspectable and manipulable. You can terraform state mv a resource, import existing infrastructure, or surgically remove something. That level of control has saved many migrations.
Drift detection: CloudFormation has native drift detection that reports when someone changed a resource in the console out from under the stack. Terraform surfaces drift every time you run terraform plan, which is arguably better because you see it on every apply rather than having to ask.
This one flips the usual narrative. CloudFormation frequently supports brand-new AWS services and properties on or near launch day, because it is built by the same company shipping the services. The Terraform AWS provider is maintained separately, so there is often a lag of days to weeks before a new resource or attribute lands in a released provider version.
If you live on the bleeding edge of AWS announcements, CloudFormation gets you there sooner. For most teams the lag rarely bites, because you are not adopting a service the week it ships. But when it does bite, it is annoying, and Terraform users end up reaching for the aws-cloudcontrol provider or raw API calls as a stopgap.
CloudFormation rolls back automatically. If a stack update fails partway through, it reverts to the last known-good state by default. That is a real safety net and it is on by default with no extra work.
Terraform does not roll back. If an apply fails halfway, you are left in a partial state and you fix forward: read the error, adjust config, apply again. Some people love the automatic rollback, others find it maddening when a rollback itself gets stuck and leaves a stack in UPDATE_ROLLBACK_FAILED, which is a genuinely painful place to be. Neither model is strictly better. Terraform gives you control and expects competence; CloudFormation gives you guardrails and occasionally traps you inside them.
Here is the same S3 bucket in both tools. First, Terraform HCL:
resource "aws_s3_bucket" "logs" {
bucket = "acme-app-logs-prod"
tags = {
Environment = "prod"
Team = "platform"
}
}
resource "aws_s3_bucket_versioning" "logs" {
bucket = aws_s3_bucket.logs.id
versioning_configuration {
status = "Enabled"
}
}
Now the CloudFormation equivalent in YAML:
Resources:
LogsBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: acme-app-logs-prod
VersioningConfiguration:
Status: Enabled
Tags:
- Key: Environment
Value: prod
- Key: Team
Value: platform
HCL has real expressions, loops via for_each, conditionals, and interpolation that reads cleanly. CloudFormation YAML leans on intrinsic functions like !Ref, !Sub, and !GetAtt, which work but get verbose fast. For composition, CloudFormation offers nested stacks and StackSets. StackSets are legitimately strong for pushing the same stack across many accounts and regions in an AWS Organization, and Terraform has no exact one-to-one equivalent for that org-wide fan-out. Terraform counters with modules, which are more flexible for everyday reuse.
If you like CloudFormation's managed state and same-day service support but cannot stand YAML, the AWS CDK is the escape hatch. You write TypeScript, Python, or Go, and it synthesizes CloudFormation templates underneath. You get real programming constructs on top of the CloudFormation engine. It is a strong middle path for teams that want AWS-native deployment with actual code ergonomics. If you are weighing that route, our Pulumi vs AWS CDK piece goes deeper.
Terraform's Registry is a decisive advantage. There are well-maintained, community-vetted modules for VPCs, EKS clusters, RDS, and hundreds of other patterns. You pull in a battle-tested VPC module and skip a week of wiring subnets and route tables. CloudFormation has nothing equivalent at that scale. AWS Solutions and Quick Starts exist, but the breadth and momentum sit firmly with Terraform.
Reach for CloudFormation when your estate is entirely AWS, you value zero state management overhead, you deploy org-wide with StackSets, and you want new-service support on launch day. Reach for Terraform when you provision across multiple providers, you want the module ecosystem, you value HCL and inspectable state, or you want one workflow spanning your whole stack. Reach for CDK when you want CloudFormation's engine but real code.
For most teams shipping today, we reach for Terraform. The multi-provider reality of modern infrastructure, the Registry, and the ergonomics outweigh the state-management chore, which a proper S3 backend solves once and then fades into the background. We recommend CloudFormation, usually through CDK, when a team is strictly AWS-only, deploys across dozens of accounts with StackSets, and wants zero third-party tooling in the supply chain. Both are solid. The mistake is picking on vibes instead of matching the tool to how many clouds and accounts you actually run.
For the wider landscape beyond these two, see our guide to the best Infrastructure-as-Code tools.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
One bundles your whole toolchain, the other lets you build anything from parts. The right pick depends on how much glue you want to own.
GitHub Actions OIDC gets all the attention, but GitLab, Buildkite, and CircleCI issue the same signed tokens. Here's how to trust them without opening a hole.
Explore more articles in this category
The IaC landscape fractured after the Terraform license change. This is the map to what each tool is actually best at, and how to choose without regret.
Terragrunt keeps large Terraform setups DRY and orchestrated, but small teams often pay its learning curve for little return.
A practical comparison of Kubernetes-native continuous reconciliation against the classic CLI-driven, state-file IaC model for platform teams.
Evergreen posts worth revisiting.