Terraform is still the default infrastructure-as-code choice on a lot of teams. The AWS CDK is the one they adopt when the application language, the security baseline, and the tests should live in the same repository. The current release line is v2.273.0, published on October 8, 2026. The developer guide is still the v2 guide. There is no CDK v3 in that line.
The languages were already there
First-class languages are TypeScript, JavaScript, Python, Java, C#, and Go. Go is documented as stable. The CDK itself is written in TypeScript. The other languages are JSII bindings, published to npm, PyPI, Maven, NuGet, and the Go module proxy, with each language's usual names and package manager.
That removes HCL as a second language your application developers have to keep current. It does not remove the infrastructure model. You still learn constructs, tokens, and the CloudFormation template those constructs synthesize. Example code is still mostly TypeScript, and the Go bindings use pointers and composition where TypeScript uses classes. Pick the language your services already use, and budget a week for the CDK, not an afternoon because the syntax looks familiar.
A construct is the unit you share
The change in how a platform team works is the construct, not the choice of language. An L2 construct is a sensible default for one AWS resource. An L3 construct is your opinion: the VPC layout, encryption, flow logs, the three required tags from the tagging standard. You publish that as a versioned package. Application teams depend on it the way they depend on any other library. A bump of the package updates the baseline. A copied template does not.
Every stack still becomes a CloudFormation template. Template size, resource quotas, and rollback behavior stay with you. The library makes the baseline hard to fork. It does not make CloudFormation disappear.
Aspects are policy at synth time
An Aspect visits every construct in a scope during synthesis. Tags.of(scope) is an Aspect. A compliance Aspect is the same mechanism pointed at a rule: every bucket encrypted, every security group free of 0.0.0.0/0 on port 22, every resource carrying Environment, Team, and CostCenter. Call Annotations.of(node).addError and cdk synth fails.
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { IConstruct } from 'constructs';
class RequireEncryptedBuckets implements cdk.IAspect {
visit(node: IConstruct): void {
if (node instanceof s3.CfnBucket && node.bucketEncryption === undefined) {
cdk.Annotations.of(node).addError('Buckets require encryption');
}
}
}
Apply it once, on the app or the stage: Aspects.of(app).add(new RequireEncryptedBuckets()). Aspects do not cross a Stage boundary, so a stage that should be covered needs the Aspect on that stage. Libraries such as cdk-nag are this pattern already packaged as AWS Solutions checks, instead of every company writing its own visitor for IAM wildcards.
This is policy as code for CDK apps. It runs when the app is synthesized, in CI, before CloudFormation is submitted. It does not see a console click, a Terraform plan, or a stack that never imported the library. An SCP still belongs on the organization if the rule has to hold no matter which tool created the resource. Sentinel and OPA answer that same question for Terraform pipelines. An Aspect answers it for the CDK repository that opted in.
Where Terraform stays
CDK state is the CloudFormation stack. There is no state file you can hand to another tool. Leaving CDK means recreating or importing resources, not pointing a new binary at terraform.tfstate.
The provider ecosystem is the other hold. AWS CDK constructs map to AWS CloudFormation resources. A fleet that also provisions Azure and GCP, with a module library those teams already trust, keeps Terraform for that reason. CDK for Terraform is a separate project that puts the constructs programming model on top of Terraform providers. It is not the AWS CDK, and it does not turn an aws-cdk-lib stack into a multi-cloud plan.
Where the tests are the reason to switch
CDK apps can assert on the synthesized template with the same test runner as the application. Template.fromStack plus hasResourceProperties checks that the bucket, the key, and the tags exist, without deploying.
import { Template } from 'aws-cdk-lib/assertions';
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::S3::Bucket', {
BucketEncryption: {
ServerSideEncryptionConfiguration: [
{ ServerSideEncryptionByDefault: { SSEAlgorithm: 'aws:kms' } },
],
},
});
The match is partial by default. The test locks the properties you care about and allows the rest. That is the review comment that used to wait for a human reading a plan, moved to a pull request check. The test proves the template. cdk diff against the account still proves you are about to change what is deployed.
Adopt CDK when the team is on AWS, wants one language, and will publish a construct library with Aspects and assertions in CI. Keep Terraform when the state file and the non-AWS providers are the asset you cannot give up. The gap closes in the repositories that can make that trade. It does not close because a major version number changed.