Run a CDK for Terraform (CDKTF) stack inside your AWS CDK app — keep one CLI, one synth, and freely pass values between the two worlds.
This library provides a tiny adapter that lets you mount a cdktf.TerraformStack beneath a normal aws-cdk-lib.Stack, plus helpers to shuttle tokens/strings back and forth.
- You love AWS CDK’s app/project structure but need providers or resources only available via Terraform (e.g., Google, Cloudflare, Datadog).
- You already have AWS CDK resources in production and you want not to migrate from CloudFormation.
- You must pass values both directions (CDK → CDKTF or CDKTF → CDK).
- Unlike
@cdktf/aws-cdk, which runs everything through Terraform,cdktf-in-aws-cdklets the AWS CDK parts deploy via CloudFormation as usual — only the Terraform stack deploys via Terraform. That means no migration or rewriting of existing CDK code: you just augment it with Terraform where needed.
Comparison with @cdktf/aws-cdk (AWS Adapter for CDKTF)
| Feature | cdktf-in-aws-cdk (this repository) |
@cdktf/aws-cdk |
|---|---|---|
| Deployment engine for AWS CDK | AWS CDK uses CloudFormation (as originally) | Full Terraform execution—AWS CDK constructs get synthesized into Terraform |
| Reusing existing CDK stacks | No migration needed—CloudFormation remains | Requires migrating to Terraform (risky if migrating live resources) |
| Terraform usage | Only the CDKTF portion goes through Terraform | Entire stack—including former CDK bits—runs via Terraform |
| Migration safety | Safer—nothing to do! | Risky—need to migrate from CloudFormation to Terraform |
npm i cdktf-in-aws-cdk
# and your usual deps
npm i aws-cdk-lib constructs cdktf @cdktf/provider-aws @cdktf/provider-googleWorks with TypeScript/Node projects using CDK v2 and recent CDKTF versions.
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { TerraformStack as CdktfTerraformStack } from 'cdktf';
import { provider, cloudRunV2Service, dataGoogleIamRole, cloudRunV2ServiceIamBinding } from '@cdktf/provider-google';
import { TerraformStackAdapter } from 'cdktf-in-aws-cdk';
class OtpTerraformStack extends CdktfTerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
new provider.GoogleProvider(this, "google", {});
const service = new cloudRunV2Service.CloudRunV2Service(this, "otp-service", {
name: "opentripplanner",
location: "us-central1",
template: {
containers: [{
image: "ghcr.io/opentripplanner/opentripplanner:2.5.0",
ports: { containerPort: 8080 },
resources: { limits: { cpu: "1", memory: "4Gi" } },
}],
},
});
const invokerRole = new dataGoogleIamRole.DataGoogleIamRole(this, "invoker-role", {
name: "roles/run.invoker"
});
new cloudRunV2ServiceIamBinding.CloudRunV2ServiceIamBinding(this, "invoker-binding", {
name: service.name,
location: service.location,
project: service.project,
role: invokerRole.name,
members: ["allUsers"],
});
}
}
export class OtpCdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// 👇 Create an adapter and use its .app as the CDKTF scope
const adapter = new TerraformStackAdapter(this, "OtpTerraformStack");
new OtpTerraformStack(adapter.app, 'OTP');
}
}
new OtpCdkStack(new cdk.App(), 'OpenTripPlanner');What’s happening?
TerraformStackAdapteris a CDKConstruct.- It exposes a CDKTF
Appatadapter.app. - You instantiate your normal
cdktf.TerraformStackunder that app. - From CDK’s perspective, the CDKTF part is “just another construct subtree.”
- CDK → CDKTF: Convert CDK tokens (like
${TOKEN:0}) into CDKTF tokens (like${TFTOKEN:0}). - CDKTF → CDK: Vice versa.
import * as aws from '@cdktf/provider-aws';
import * as cdk from 'aws-cdk-lib';
import * as cdktf from 'cdktf';
import * as constructs from 'constructs';
import * as path from 'path';
import {
TerraformStackAdapter,
tokenStringFromAwsToTerraform,
tokenStringFromTerraformToAws,
} from 'cdktf-in-aws-cdk';
class TfLambdaStack extends cdktf.TerraformStack {
public apiEndpoint: string;
constructor(scope: constructs.Construct, name: string, config: {
path: string, handler: string, runtime: string, version: string,
}) {
super(scope, name);
new aws.provider.AwsProvider(this, 'aws', { region: 'ap-northeast-1' });
// package lambda code as a Terraform asset
const asset = new cdktf.TerraformAsset(this, 'lambda-asset', {
path: path.resolve('.', config.path),
type: cdktf.AssetType.ARCHIVE,
});
const bucket = new aws.s3Bucket.S3Bucket(this, 'bucket', {
bucketPrefix: `learn-cdktf-${name}`,
});
const lambdaArchive = new aws.s3Object.S3Object(this, 'lambda-archive', {
bucket: bucket.bucket,
key: `${config.version}/${asset.fileName}`,
source: asset.path,
});
const role = new aws.iamRole.IamRole(this, 'lambda-exec', {
name: `learn-cdktf-${name}`,
assumeRolePolicy: JSON.stringify({
Version: '2012-10-17',
Statement: [{ Action: 'sts:AssumeRole', Principal: { Service: 'lambda.amazonaws.com' }, Effect: 'Allow' }],
}),
});
new aws.iamRolePolicyAttachment.IamRolePolicyAttachment(this, 'lambda-managed-policy', {
policyArn: 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
role: role.name,
});
const lambdaFunc = new aws.lambdaFunction.LambdaFunction(this, 'handler', {
functionName: `learn-cdktf-${name}`,
s3Bucket: bucket.bucket,
s3Key: lambdaArchive.key,
handler: config.handler,
runtime: config.runtime,
role: role.arn,
});
const api = new aws.apigatewayv2Api.Apigatewayv2Api(this, 'api', {
name: name,
protocolType: 'HTTP',
target: lambdaFunc.arn,
});
new aws.lambdaPermission.LambdaPermission(this, 'invoke', {
functionName: lambdaFunc.functionName,
action: 'lambda:InvokeFunction',
principal: 'apigateway.amazonaws.com',
sourceArn: `${api.executionArn}/*/*`,
});
this.apiEndpoint = api.apiEndpoint;
}
}
class AwsStack extends cdk.Stack {
constructor(scope: constructs.Construct, id: string) {
super(scope, id);
const versionParam = new cdk.CfnParameter(this, 'Version', {
type: 'String',
default: 'v1',
});
const tf = new TerraformStackAdapter(this, 'TfStack');
const tfStack = new TfLambdaStack(tf.app, 'lambda-hello-world', {
path: './lambda-hello-world/dist',
handler: 'index.handler',
runtime: 'nodejs20.x',
// CDK token → plain string for CDKTF
version: tokenStringFromAwsToTerraform(versionParam.valueAsString),
});
// CDKTF value → CDK output
new cdk.CfnOutput(this, 'ApiEndpoint', {
value: tokenStringFromTerraformToAws(tfStack.apiEndpoint),
});
}
}
new AwsStack(new cdk.App(), 'aws-stack');Mounts a CDKTF app inside your CDK stack.
-
Constructor:
new TerraformStackAdapter(scope: Construct, id: string) -
Properties
app: cdktf.App— use this as the scope for yourcdktf.TerraformStackinstances.
You can create multiple adapters if you want to logically separate CDKTF apps.
Converts a CDK token/string into a CDKTF token.
Typical use: pass API Gateway URL to Cloudflare.
Converts a CDKTF-produced string back into something CDK can wire into outputs, other constructs, etc.
Typical use: surface a CDKTF-generated endpoint/ARN as a CfnOutput.
Note: Currently, only string value helpers are implemented. Support for other data types (numbers, lists, maps, etc.) is planned but not yet available.
-
Providers & Backends Configure CDKTF providers and state backends as you normally would inside your
TerraformStack. If you don’t set a backend, CDKTF uses its defaults (often local). -
Multiple stacks It’s fine to create several
TerraformStacks under the sameTerraformStackAdapter.app, or use several adapters. -
Parameters / Context Use CDK
CfnParameter, SSM Parameters, or context to feed values into CDKTF, then convert withtokenStringFromAwsToTerraform. -
Outputs For simple values, just keep them as
publicproperties on your CDKTF stack and convert them back withtokenStringFromTerraformToAwsto export viaCfnOutput. -
Ordering Because everything lives in the same construct tree, CDK-style synthesis ordering generally “just works.” If you have cross-dependencies (e.g., CDKTF needs an ARN created by CDK), pass it in through the constructor using the token converters.
-
“My CDKTF value is empty in CDK output.” Ensure you’re using
tokenStringFromTerraformToAws(...)when wiring CDKTF properties back into CDK. -
“Provider can’t find credentials.” CDKTF still uses provider-native auth. Since Terraform deployment happens inside a Lambda function, you must configure credentials properly in your provider configuration (e.g., in
AwsProviderparameters) rather than relying on environment variables during synth.
MIT © Contributors to cdktf-in-aws-cdk