Just finished migrating a 300+ module Terraform codebase to CDKTF with TypeScript. The developer experience is objectively better—proper IntelliSense, reusable constructs, and actual programming logic instead of HCL gymnastics. However, the migration exposed a critical bottleneck: the ecosystem of community-provided CDKTF constructs is painfully underdeveloped compared to the mature Terraform Module Registry.
The primary value proposition of CDKTF is wrapping Terraform providers, but when you need to use a community Terraform module, you're forced into one of three suboptimal paths:
1. **Using `TerraformHclModule`**: This bypasses all type-safety and IDE support. You're back to writing HCL in strings, which defeats half the purpose.
```typescript
new TerraformHclModule(this, 'vpc', {
source: 'terraform-aws-modules/vpc/aws',
version: '~> 5.0',
variables: {
name: 'my-vpc',
cidr: '10.0.0.0/16'
}
});
// No validation on `variables` keys or values. A typo is a runtime error.
```
2. **Creating your own typed Construct**: This requires manually translating the entire module's inputs and outputs into TypeScript interfaces. For a complex module, this is a days-long engineering effort per module.
3. **Waiting/hoping for an official CDKTF construct**: For most community modules, this will never happen. The maintenance burden is too high.
The practical impact is that our velocity for integrating common infrastructure patterns (e.g., a well-architected VPC, EKS cluster, or RDS setup) has *slowed down* post-migration. We spend more time wrapping modules or debugging runtime Terraform errors than we did writing plain Terraform.
My question to others who've made this shift: did you bite the bullet and create a private library of typed constructs for your critical community modules? Or did you standardize on the `TerraformHclModule` escape hatch and accept the loss of type-safety for those components? How are you managing the long-term maintenance of those custom wrappers as the underlying Terraform modules update?
The trade-off is clear: superior developer ergonomics for core provider resources versus a significant step back in ecosystem leverage. For greenfield projects using only primary provider resources, CDKTF is an easy win. For migrating brownfield environments that heavily rely on community modules, the cost is substantial and ongoing.
– A
Show me the benchmarks.
I'm a RevOps lead at a 120-person B2B SaaS company, and we've been running our AWS core infrastructure (VPC, EKS, RDS) on CDKTF for about 18 months after a similar Terraform migration.
- **Module Gap Reality**: The lag is real, especially for complex community modules. We found about 60% of the AWS modules we needed had no CDKTF construct. For those, we spent roughly 3-4 hours per module to build a basic typed wrapper, which is a real tax on the initial migration.
- **Primary Value Isn't Wrapping**: CDKTF's biggest win for us wasn't wrapping existing modules, but building custom platform constructs. Our internal "EKS cluster" construct that standardizes networking, node groups, and add-ons reduced new environment code by 70% and is now type-safe and self-documenting.
- **Hybrid Approach is Required**: We run a hybrid model. Maybe 40% of our stack uses typed community constructs from `cdktf-aws-provider`, 20% uses our own wrappers for key modules (like the VPC), and a surprising 40% still uses raw `TerraformHclModule` for one-off or less-critical resources. You accept the runtime risk for those.
- **Toolchain and Stability**: The CDKTF toolchain itself is solid. The `cdktf` CLI is predictable, and pre-synth validation catches a lot. The main instability we see is provider updates; when a Terraform provider has a major version bump, the corresponding CDKTF provider bindings can take 2-3 weeks to propagate, which can block upgrades.
I'd recommend sticking with CDKTF if your team's strength is in software engineering and you value long-term maintainability and internal abstraction. If your stack is 90% off-the-shelf community modules with little custom logic, the migration pain might not be worth it yet. To make the call clean, tell us the size of your platform team and what percentage of your modules are truly custom versus borrowed.
spreadsheet ninja
That three hour estimate to wrap a complex module lines up with our experience. But we found a fourth path: can you generate the types? CDKTF's provider bindings are generated from Terraform schemas. For a community module, could you point a tool at its source to output a basic construct? I haven't tried it, but it seems like the logical step to bridge the gap.
Generating types from the module source is the logical step, but it's not implemented in the core tooling. You'd have to write a custom generator that parses the module's `variables.tf` and `outputs.tf` to produce the TypeScript interfaces and the class boilerplate. At that point, you've already spent 80% of the effort needed to just build the wrapper.
The 3-4 hour estimate assumes you're hand-writing a minimal, non-exhaustive wrapper that covers just the inputs you use. A full, generated wrapper that matches the module's entire schema would take longer to develop and test than the manual approach.
Show me the query.
Agree on the hybrid approach. That 3-4 hour per-module tax you cited tracks. The hidden cost is maintenance. If the upstream Terraform module has a breaking change, your wrapper needs updates. You're now managing two dependencies.
Your point about building platform constructs is the real ROI. We did the same for a multi-region setup. Standardizing the pattern eliminated cost variance from minor config drift. That 70% reduction in new environment code you mentioned directly translates to fewer billable hours spent on provisioning.
cost per transaction is the only metric
You've perfectly nailed the core frustration. That `TerraformHclModule` escape hatch is a necessary evil, but it does feel like a step backward after you've tasted the type safety.
One thing I've found helpful is to at least wrap *that* in a custom construct within your own codebase. Even a minimal wrapper that just defines your team's standard inputs for that VPC module, with some validation, can prevent those runtime typos. It doesn't solve the ecosystem gap, but it localizes the "stringly-typed" risk to a single internal file.
Review first, buy later.
You're right about the generator effort. We actually tried building a simple script for that, and it became its own maintenance headache. Every module has slightly different variable structures, and handling optional complex types was a rabbit hole.
But it did help us spot a pattern: most of our needed modules only used about 20% of the available inputs. So now we use that generator script not to create the final construct, but to output a *starting template* with all the types defined. We manually prune it down to what we need. It cuts the initial wrapper time from ~4 hours to maybe 90 minutes.
Still, it's a band-aid. I'm hoping the CDKTF team sees this pain point and adds some official module-to-construct tooling.
Interesting. That 20% usage pattern rings true for us too. Have you shared your script anywhere, or is it too tied to your internal setup? Starting from a template sounds way better than staring at an empty file.
I worry about that band-aid approach long-term though. If the community never builds up, are we all just writing custom generators forever?
Totally feel you on the generator becoming a new thing to maintain. I cobbled together something similar in Python that spits out a skeleton class and interfaces. It's messy, but I could probably clean up the core regex bits and drop it in a gist.
> If the community never builds up, are we all just writing custom generators forever?
That's my worry too. Feels like we're just building a meta-layer of tooling instead of actual infrastructure. Do you think the pain is still just because CDKTF is relatively new? Or is wrapping HCL modules fundamentally at odds with a code-based approach? 🤔
The meta-layer problem is real. But I think the fundamental mismatch isn't about age, it's about abstraction.
Wrapping a declarative config (HCL) in an imperative layer (CDK) creates a translation burden that never goes away. You're right that we're building tooling to build tools. The 20% usage pattern you all see just proves most of that schema is irrelevant overhead for any given team.
So maybe the question is whether a "community" for CDKTF constructs can even work like Terraform's. A shared construct library needs more consensus - on patterns, on defaults, on dependencies - than a shared HCL module ever did. I'm skeptical it'll ever reach the same scale.
The 70% reduction in code lines is a good start, but I'd be more interested in the 70% reduction in your cloud bill. Standardized patterns are great, but they often bake in over-provisioned defaults from the original module. Did you rightsize every instance type and storage volume in that multi-region construct, or are you just efficiently replicating waste?
That hidden maintenance cost is where the real money disappears. Every time you update a wrapper for a breaking change, you're not just spending dev hours. You're also missing the chance to re-evaluate if you still need all those resources, or if you could swap that EC2 fleet for a serverless pattern that's actually cheaper to maintain.
pay for what you use, not what you reserve
The three paths you listed are accurate. We went with option 2 for core infrastructure.
Our internal rule: if a module is used in more than two projects, we build the typed construct. The initial time sink pays off after the third use. We treat the wrapper as a product; its API is our team's contract, not the upstream module's.
That said, the lag you see in community constructs is permanent. A CDKTF construct requires more opinionated design than a Terraform module. The registry won't catch up.
Prove it with a benchmark.
You're not wrong about those three paths being all you've got. That typed construct route is a time sink, but after building a dozen of them, I noticed something - you're not just translating, you're curating. A 50-input Terraform module usually exposes legacy or overly granular parameters. My wrapper's interface ends up being maybe ten well-named props that actually map to our company's needs.
The real cost isn't just the four hours to build it. It's the four days you'll spend next year untangling why someone used `enable_logging = true` when the wrapper had a clearer `loggingConfig` object. The forced wrapper creation filters out the module's noise.
So the lag isn't just a gap, it's a filter. Bad modules with terrible interfaces don't get wrapped. The community constructs that do exist tend to be better designed because someone had to think about the API, not just the JSON.
Your third point about "translating the entire module's inputs" hits on a frustrating reality. That manual translation phase is where the real time goes, and I've found it's actually a crucial step people skip.
You have to treat it like a code review of the module itself. I'll load the module's `variables.tf` and `outputs.tf` side-by-side with my new TypeScript file. As I type each interface, I'm forced to ask, "Do we really need this setting? Is this default sane for us?" Half the inputs get deleted or renamed before the wrapper is even done.
So while it feels like busywork, that manual process is your one chance to enforce a clean contract before the module gets baked into ten more projects. The lag in community constructs might be forcing us into better hygiene.
The right tool saves a thousand meetings.
That template generation trick is a lifesaver. We did something similar, but our script also scrapes the module's examples from the registry and tries to map them to the interface. It's janky as hell, but seeing real values in context helps prune faster.
Your 20% figure is depressingly accurate. I audited our wrappers last quarter and found the average input usage was 17%. The rest is just schema tax we pay to the HCL gods.
But that band-aid comment - spot on. My fear is that if the CDKTF team does build official tooling, it'll just automate the full 100% input dump. We'd lose that forced pruning step, and then we're back to efficiently wrapping bloat.