DEV Community

Cover image for Terraform vs AWS CDK: Why I Chose CDK for Autowired.ai (And When I Wouldn't)
Yoganand Govind
Yoganand Govind

Posted on

Terraform vs AWS CDK: Why I Chose CDK for Autowired.ai (And When I Wouldn't)

I've used both tools in production. Terraform for different platforms — multi-team, multi-cloud, years of accumulated state. AWS CDK with TypeScript to build Autowired.ai solo — six stacks, one table, a fully async AI document processing pipeline.

Both are legitimate choices. The answer depends on factors that have nothing to do with which syntax you prefer.

Here's the honest comparison, grounded in where each tool has actually been better or worse for me.

What You're Actually Choosing Between

Terraform and CDK solve the same problem at different abstraction levels.

Terraform operates at the resource level. You write HCL describing the exact AWS resources you want, their configuration, and dependencies. Terraform plan shows precisely what will change before you apply it. The mapping between your code and deployed infrastructure is direct.

AWS CDK operates at the construct level. You write TypeScript (or Python, Java, or Go) that synthesises CloudFormation templates. CDK has three construct levels: L1 (CloudFormation resource wrappers), L2 (opinionated constructs with defaults), and L3 (patterns provisioning multiple resources together). CDK generates CloudFormation, which manages the actual resources.

The extra abstraction layer in CDK is both its advantage and its complication. It's worth understanding exactly where each side shows up.

The Architecture Diagram

Architecture

Where CDK Has the Edge
Type Safety Across the Infrastructure Layer

Autowired.ai is TypeScript throughout — Lambda handlers, API layer, database access layer, and the CDK infrastructure code. This means:

  • Lambda environment variables are typed interfaces. If you add a required env var to a handler, the CDK Lambda definition must be updated, or the TypeScript compiler fails.
  • Cross-stack references are typed exports. ProcessingStack exposes a documentProcessingQueue: sqs.Queue, not a string ARN. The importing stack gets a typed object.
  • Refactoring infrastructure is safer. Renaming a construct propagates through the type system, not grep-and-replace across HCL.

This mattered more than I expected as the stack grew. Six CDK stacks with dozens of resources and explicit cross-stack dependencies — TypeScript catches a class of configuration errors before cdk deploy ever runs.

Loops, Conditions, and Computed Values

In Autowired.ai's StorageStack, S3 event notifications for document uploads need to handle multiple file extensions. In CDK:

const supportedExtensions = [
  ".pdf", ".PDF", ".png", ".PNG", ".jpg", ".JPG",
  ".jpeg", ".JPEG", ".tiff", ".TIFF", ".tif", ".TIF",
];

for (const ext of supportedExtensions) {
  this.documentsBucket.addEventNotification(
    s3.EventType.OBJECT_CREATED,
    new s3n.LambdaDestination(this.s3IngestionLambda),
    { prefix: "s3-ingestion/", suffix: ext }
  );
}
Enter fullscreen mode Exit fullscreen mode

In Terraform, this is a for_each — workable, but the iteration semantics diverge enough from general programming that engineers less familiar with HCL tend to get it wrong.

The more significant case: computed ARNs. Autowired.ai has a circular dependency between StorageStack (which needs the Step Functions ARN to wire S3 event notifications) and ProcessingStack (which needs the S3 bucket). CDK cross-stack exports would make this a compile error. The solution: compute the state machine ARN deterministically from the naming convention at synthesis time:
const stateMachineArn = arn:aws:states:${this.region}:${this.account}:stateMachine:autowire-batch-processing-${stage};

CDK can evaluate this at synthesis. In Terraform, equivalent computed values often require data sources and depends_on, and aren't always available at plan time.

L2 Constructs Encode Best Practices

CDK's L2 constructs package AWS recommendations as defaults you inherit without writing them explicitly:

new sqs.Queue(this, "DocumentProcessingDLQ", {
  queueName: `autowire-document-dlq-${stage}`,
  retentionPeriod: cdk.Duration.days(14),
});
Enter fullscreen mode Exit fullscreen mode

Creating a queue with L2 automatically configures managed encryption and sensible defaults. In Terraform, all of this is explicit configuration — which means it's also configuration you can forget to add.

Where Terraform Has the Edge
Plan Predictability

Terraform's plan output is the most readable change artifact in infrastructure-as-code. Before applying, you see exactly what will be created, modified, or destroyed. The plan is reproducible across machines and reviewable in pull requests.

CDK synthesises CloudFormation and uses its change set mechanism — more verbose, less readable. cdk diff gives a high-level summary but doesn't show exact CloudFormation property changes until you synthesize and compare templates.

For teams doing rigorous infrastructure change review, Terraform's plan is meaningfully more reviewable.

Multi-Cloud and Non-AWS Resources

Terraform's provider ecosystem covers AWS, GCP, Azure, Kubernetes, Datadog, PagerDuty, GitHub, and hundreds more under one configuration model. If your infrastructure spans multiple cloud providers or requires managing SaaS configuration as code, Terraform is the right tool.

CDK is AWS-native. CDK for Terraform (CDKTF) exists but adds another abstraction layer and isn't as mature as either tool independently.

For Autowired.ai, everything lives in AWS. CDK is the right fit. In enterprise environments where I managed AWS infrastructure alongside Datadog configuration and GitHub team permissions, Terraform's unified model was meaningfully better.

Team Breadth

HCL has a narrower semantics than TypeScript. An engineer who understands resource, data, module, variable, and output can read most Terraform configurations.

CDK requires understanding TypeScript, the construct model, L1/L2/L3 levels, how synthesis generates CloudFormation, and how CloudFormation applies changes. On a team where only one or two engineers own infrastructure, CDK's higher ceiling is worth the learning curve. When every engineer is expected to contribute infrastructure changes, Terraform's shallower curve can reduce bottlenecks.

Drift Detection

Terraform's terraform import and drift workflows are mature. If a resource was manually changed in the AWS console, Terraform plan surfaces the drift with a clear plan to reconcile.

CDK's drift story runs through CloudFormation's drift detection, but remediation means re-running cdk deploy to synthesise and push the CDK-defined state. For organisations with manual change processes or compliance requirements around drift documentation, this is a real operational difference.

The Decision Framework

Choose CDK if:

Your stack is AWS-only and you want L2 construct defaults embedded in your definitions
TypeScript type safety across Lambda env vars and cross-stack references matters
Infrastructure has significant loops, conditionals, or computed values
Infra is maintained by the same engineers writing application code

Choose Terraform if:

Your infrastructure spans multiple clouds or requires non-AWS resources (Datadog, GitHub, Kubernetes)
Broad team ownership and a shallower learning curve reduces operational bottlenecks
Readable, reviewable plan artifacts are a change management requirement
You need mature drift detection and import workflows

The hybrid that often makes sense: Terraform for shared infrastructure (networking, IAM baseline, account-level config) and CDK for per-service application infrastructure (Lambda + SQS + DynamoDB). Shared infrastructure changes slowly and benefits from plan predictability. Application infrastructure changes frequently and benefits from type safety and tight integration with application code.

What I'd Change in My CDK Implementation

Separate config from construct logic more explicitly. Configuration values—timeouts, memory sizes, queue parameters—are mixed in with construct definitions across Autowired.ai's stacks. Extracting these to per-stack configuration objects makes design decisions visible and environment-specific overrides more structured.

More opinionated L3 constructs for repeated patterns. The pattern Lambda + SQS trigger + DLQ + CloudWatch alarm appears multiple times across stacks. Wrapping it in a custom L3 construct would reduce duplication and make the pattern explicitly named. I deferred this in the initial build – worth the refactor as the codebase matures.

Document stack deployment ordering explicitly. CDK infers some cross-stack dependencies, but DatabaseStack → StorageStack → ProcessingStack → APIStack isn't expressed in code — it's implicit knowledge. A comment in the infra directory documenting deployment order and the reasons for it saves the next engineer real confusion.

The Operational Realities Neither Tool Eliminates

Both require:

  • A CI/CD pipeline running plan/synth on pull requests and apply/deploy on merge
  • Access control preventing direct console modifications to managed resources
  • A tagging strategy for cost allocation and resource ownership

Terraform state is a critical artifact. Store it in S3 with versioning and DynamoDB state locking. Losing the state file means Terraform no longer knows what it manages — recovery requires importing every resource back into state.

CDK's equivalent is the CloudFormation stack. When a stack gets into a failed rollback state (common during development), recovery often requires manual intervention in the CloudFormation console. There's no CDK equivalent to Terraform state mv for surgical state manipulation.

Both have sharp edges. The tool your team knows better is generally the safer choice.

Lessons That Actually Mattered

L2 constructs before L1. When I first used CDK, I defaulted to L1 because the CloudFormation properties felt familiar. L2 constructs encode encryption, IAM scoping, and sensible defaults I would have had to write manually at L1. Learn L2 first.

CDK synthesis failures surface differently than Terraform plan failures. CDK errors often appear as CloudFormation stack events rather than synthesis-time errors. Getting comfortable reading CloudFormation event logs is a prerequisite for operating CDK in production — not optional.

Version your CDK libraries deliberately. CDK releases frequently, and L2 construct APIs change between major versions. Pin in package.json and treat CDK upgrades as a deliberate infrastructure change, not a routine dependency bump.

The deployment pipeline matters more than the tool. A well-run Terraform pipeline beats a carelessly managed CDK setup. Infrastructure changes through code review, diff/plan review, and automated deployment is the discipline that matters — the language is secondary.

Top comments (0)