A one-year, greenfield platform build at Anywhere Real Estate. Internally, the enterprise ran a mixture. Most teams built on AWS CDK, because the enterprise mandated it. A few sub-teams used Terraform — the tool I actually knew, from Cigna, where state lived the ordinary way, in S3.
The honest answer to how I decided: I went with CDK, because that’s what the enterprise had mandated. Not a feature comparison. A mandate — and the real question was never Terraform or CDK, it was how to live inside it.
Which one should I use
Terraform or CDK, which one should I use — the question sounds like a feature bake-off. Live inside a mandate for a year. It stops looking like one.
CDK’s appeal is ergonomic, not architectural. “As you’re going through and programming it and building up with it, it’s nice to see within the actual code how things look and feel.” Terraform in HCL is code too, but “syntactically it’s very different than just JavaScript” — CDK gave me actual Node.js underneath the resources, and that felt good to write.
What nobody put on the table while we were deciding was how CDK works underneath. It compiles to CloudFormation. CDK deploys through CloudFormation stack operations, and CloudFormation, in my experience, “is awful, or at least in my experience has always been awful.” It “gets stuck for one reason or another, and it’s fairly slow.” Its exports also stop at the Region boundary — a separate note covers the shape that held.
Nothing on this page is from that build. The diagram and the code below are reference, written from the docs.
Terraform’s counter-virtue is that it keeps state right in its own place — we normally used it with the state held in S3 buckets. The trade runs the other way too: state can corrupt.
Living inside the mandate
The mandate wasn’t arbitrary. The enterprise had a history of outsourcing, and outsourcing means churn — engineers going in and out, “the tribal knowledge kept on leaving.” The CDK harness existed so incoming engineers wouldn’t have to actually know CDK: boilerplate by convention that worked, for the use case, about ninety percent of the time. I’ll grant the premise — boilerplate that eases onboarding for a short-tenure team is a rational answer to churn. It just didn’t fit a team that already knew CDK and needed Lambdas with their own IAM roles, which was absolutely not the use case the harness covered.
So the first thing I did was strip it: no Lambda support, no per-function IAM, so I got rid of the library and went pure CDK. There were “some grumblings that we weren’t following the corporate mandates,” but when I explained what I needed — I even offered to file a PR against their harness — the platform team that owned the mandate told me something I didn’t expect: “No, go ahead and build it out yourself.”
Going bare never bit me. We still had the harness’s source, so anything worth baking back in, we could pull from there. The only real friction was tagging conventions, required as you promote up toward prod — not that big a deal.
The rule
Once working, infrastructure doesn’t get touched much. There’s a lot of pain up front, whichever tool you pick — incremental changes after that are not a big deal. So the choice was never going to come down to a feature matrix.
It comes down to who holds the mandate, and what you can live inside once it’s made. CDK’s ergonomic appeal is real. It also compiles straight down to CloudFormation, and it inherits every one of CloudFormation’s failure modes. Pick the tool you can live inside — you’re going to be living inside it a lot longer than you spent deciding.
Underneath
A CDK app is TypeScript, JavaScript, Python, Java, C#, or Go that synthesizes into a CloudFormation
template — the
AWS CDK Developer Guide’s apps concept
walks through how constructs assemble into that template. Running cdk deploy hands the
template to CloudFormation, which runs it as a
stack operation — create, update, or
rollback, the same primitives CloudFormation has always had. Terraform skips that step. It records what it
manages directly in its own
state file, commonly kept in an
S3 backend so a team can share
and lock it. Same job, two different systems of record — one borrowed from CloudFormation, one Terraform
owns outright.
A Lambda function with its own execution role, first with CDK’s Function and Role constructs, then with Terraform’s aws_lambda_function and aws_iam_role resources.
// inside a Stack constructor
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as iam from "aws-cdk-lib/aws-iam";
const role = new iam.Role(this, "ExampleRole", {
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AWSLambdaBasicExecutionRole",
),
],
});
new lambda.Function(this, "ExampleFunction", {
runtime: lambda.Runtime.NODEJS_22_X,
handler: "index.handler",
code: lambda.Code.fromAsset("lambda"),
role,
});
resource "aws_iam_role" "example" {
name = "example-lambda-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
}]
})
}
resource "aws_lambda_function" "example" {
function_name = "example-function"
role = aws_iam_role.example.arn
handler = "index.handler"
runtime = "nodejs22.x"
filename = "lambda.zip"
}
More field notes
Terraform state split by change velocity, not only by environment
Terraform, Amazon S3, Amazon DynamoDB, AWS Systems Manager Parameter Store
Newer · Aug 2026
The ladder climbed before nuke-and-reimport, in dev and never in prod
Terraform, Amazon S3, AWS Lambda, Amazon VPC
Older · Aug 2026
Start
I’ll tell you in about a day whether I’m the right person. The first conversation is fit, not a free architecture review.