AWS CDK to Terraform Converter

Turn an AWS CDK stack into Terraform HCL, with the IAM roles, policies and settings that CDK constructs create behind the scenes written out as explicit resources.

Your codeAWS CDK (TypeScript)
Drop the file to load it
Tab indents · Esc, Tab exits
ResultTerraform (HCL)
Your Terraform (HCL) will appear hereClick Convert to Terraform or press Ctrl Enter
main.tf
Don't paste secrets

How to convert AWS CDK to Terraform

To convert AWS CDK to Terraform, paste your CDK stack into the editor above and press Convert to Terraform. Each construct is expanded into the AWS provider resources it really creates, including execution roles and grant policies. Check the result against cdk synth, then adopt the deployed resources with Terraform import blocks.

  1. Paste the stackPaste the whole CDK stack class, with its imports and the App setup, into the AWS CDK (TypeScript) editor. You can also open a .ts or .js file up to 500 KB.
  2. Check for secretsBefore sending, the page scans for AWS keys, private keys and tokens in the code, including values hard-coded in environment blocks.
  3. Convert to TerraformPress Convert to Terraform or Ctrl + Enter. The HCL streams in as it's written, and you can stop it at any time.
  4. Compare with cdk synthRun npx cdk synth and check that every resource in the template has a Terraform block. Look for TODO: comments, then Download the result as main.tf.
  5. Import and planAdd import blocks for resources that already exist, then run terraform plan. It should only show imports, with no changes or replacements.
Four-step AWS CDK to Terraform workflow: paste the CDK stack, convert with hidden roles and policies written out, cross-check against cdk synth, then import live resources and confirm terraform plan shows no changes
The conversion is quick. Cross-checking and importing are where you spend your time.

Why convert CDK stacks to Terraform

Teams usually move CDK to Terraform when they standardize on one tool across AWS and other providers, or when a platform team owns Terraform modules and policy checks that every stack must pass. Terraform's plan output and state are also easier to reason about for some reviewers than a synthesized CloudFormation template.

The hard part is that CDK hides a lot. A single lambda.Function creates an execution role with a managed policy attached, and each grant* call writes an IAM policy. In Terraform every one of those is a resource you must declare. The converter writes them out for you, so you don't lose permissions or settings that were never visible in your CDK code.

It's also useful for auditing: converting a stack shows exactly which resources and permissions it deploys, in a format many security reviewers already read.

A CDK lambda.Function with Code.fromAsset and a bucket.grantRead call expanding into five Terraform blocks: archive_file, aws_lambda_function, aws_iam_role, a policy attachment for CloudWatch Logs and an aws_iam_role_policy for S3 reads
Two CDK statements become five Terraform blocks.

Why CDK code gets longer in Terraform

A few lines of CDK can stand for many resources. L2 constructs fill in defaults and create supporting resources: new lambda.Function() creates an IAM role with the AWSLambdaBasicExecutionRole managed policy, and bucket.grantRead(fn) adds a policy with the S3 read actions. Terraform has no such layer, so the converter declares each piece, such as aws_iam_role, aws_iam_role_policy_attachment and aws_s3_bucket_versioning.

Assets and names need attention too. lambda.Code.fromAsset("lambda") tells CDK to zip and upload a folder at deploy time. In Terraform you package it yourself, usually with an archive_file data source. CDK also generates physical names when you don't set them, while many Terraform resources expect one. When you import, use the names that are actually deployed.

Don't delete the CDK stack until RemovalPolicy.RETAIN is deployed on every resource you plan to import. Otherwise CloudFormation deletes the bucket, table or queue along with the stack. Resources without it, such as functions and roles, are deleted and must be recreated by Terraform.

Handover of deployed CDK resources to Terraform: set RemovalPolicy.RETAIN and deploy, destroy the stack while retained resources stay, add Terraform import blocks, then run terraform plan expecting no changes
Moving ownership of live resources from a CloudFormation stack to Terraform state.

AWS CDK to Terraform: quick reference

What common CDK features become in Terraform. The last rows are the ones people most often miss.

AWS CDK features and their Terraform equivalents
CDK featureCDK exampleTerraform
L2 constructnew s3.Bucket(this, "Uploads", { versioned: true })aws_s3_bucket plus aws_s3_bucket_versioning
Lambda functionnew lambda.Function(…)aws_lambda_function, aws_iam_role and a policy attachment for AWSLambdaBasicExecutionRole
Grantsbucket.grantRead(fn)aws_iam_role_policy with the actions the grant allows
Assetslambda.Code.fromAsset("lambda")data "archive_file" with filename and source_code_hash
Event sourcesfn.addEventSource(new SqsEventSource(q))aws_lambda_event_source_mapping plus the queue permissions
Removal policyremovalPolicy: RemovalPolicy.RETAINlifecycle { prevent_destroy = true }
Stack environmentenv: { region: "us-east-1" }provider "aws" { region = "us-east-1" }
Outputsnew CfnOutput(this, "Url", { value })output "url" { value = … }
Context and propsthis.node.tryGetContext("stage")variable "stage" {}
Generated namesNo bucketName setA fixed name, or bucket_prefix / name_prefix

Key features

  • Hidden resources written outExecution roles, managed policy attachments, versioning and public access blocks that CDK creates implicitly become explicit Terraform resources.
  • Grants become IAM policiesgrantRead, grantWrite and similar calls are expanded into aws_iam_role_policy or aws_iam_policy resources you can review line by line.
  • Assets packaged the Terraform wayCode.fromAsset() becomes an archive_file data source wired to filename and source_code_hash.
  • Current AWS provider styleS3 settings use the separate resources introduced in provider v4, such as aws_s3_bucket_versioning, instead of deprecated inline blocks.
  • Removal policies carried overRemovalPolicy.RETAIN becomes prevent_destroy, so stateful resources stay protected.
  • Gaps marked with TODOCustom resources, aspects and anything without a direct Terraform equivalent are flagged in TODO: comments.
  • Secret check before sendingKeys and tokens are detected in your browser before any code leaves it.
  • Stream, copy or downloadWatch the HCL arrive, then copy it or download it as main.tf.

Examples: AWS CDK converted to Terraform

Two stacks where CDK defaults matter. Choose Try this example to load the CDK code into the converter. Your output may be arranged differently.

A DynamoDB table with a stream and a GSI

Terraform needs every key attribute declared, including the index key, and CDK's default ALL projection must be stated. CDK generated a table name, so the Terraform version sets one: use the deployed name when you import.

AWS CDK (TypeScript)
import { Stack, StackProps, RemovalPolicy } from "aws-cdk-lib";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import { Construct } from "constructs";

export class OrdersStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const table = new dynamodb.Table(this, "Orders", {
      partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING },
      sortKey: { name: "sk", type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
      removalPolicy: RemovalPolicy.RETAIN,
    });

    table.addGlobalSecondaryIndex({
      indexName: "byCustomer",
      partitionKey: { name: "customerId", type: dynamodb.AttributeType.STRING },
    });
  }
}
Terraform (HCL)
resource "aws_dynamodb_table" "orders" {
  name             = "orders"
  billing_mode     = "PAY_PER_REQUEST"
  hash_key         = "pk"
  range_key        = "sk"
  stream_enabled   = true
  stream_view_type = "NEW_AND_OLD_IMAGES"

  attribute {
    name = "pk"
    type = "S"
  }

  attribute {
    name = "sk"
    type = "S"
  }

  attribute {
    name = "customerId"
    type = "S"
  }

  global_secondary_index {
    name            = "byCustomer"
    hash_key        = "customerId"
    projection_type = "ALL"
  }

  lifecycle {
    prevent_destroy = true
  }
}

A Lambda function triggered by an SQS queue

addEventSource quietly grants the function permission to consume the queue and creates the event source mapping. In Terraform, the role, the logging policy, the queue permissions, the package and the mapping are all separate blocks.

AWS CDK (TypeScript)
import { Stack, StackProps, Duration } from "aws-cdk-lib";
import * as sqs from "aws-cdk-lib/aws-sqs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { SqsEventSource } from "aws-cdk-lib/aws-lambda-event-sources";
import { Construct } from "constructs";

export class WorkerStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const queue = new sqs.Queue(this, "Jobs", {
      visibilityTimeout: Duration.seconds(180),
    });

    const worker = new lambda.Function(this, "Worker", {
      runtime: lambda.Runtime.PYTHON_3_12,
      handler: "app.handler",
      code: lambda.Code.fromAsset("worker"),
      timeout: Duration.seconds(30),
    });

    worker.addEventSource(new SqsEventSource(queue, { batchSize: 10 }));
  }
}
Terraform (HCL)
resource "aws_sqs_queue" "jobs" {
  name_prefix                = "jobs-"
  visibility_timeout_seconds = 180
}

data "archive_file" "worker" {
  type        = "zip"
  source_dir  = "${path.module}/worker"
  output_path = "${path.module}/build/worker.zip"
}

resource "aws_iam_role" "worker" {
  name_prefix = "worker-"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Action    = "sts:AssumeRole"
      Principal = { Service = "lambda.amazonaws.com" }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "worker_logs" {
  role       = aws_iam_role.worker.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy" "worker_sqs" {
  role = aws_iam_role.worker.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = [
        "sqs:ReceiveMessage",
        "sqs:ChangeMessageVisibility",
        "sqs:GetQueueUrl",
        "sqs:DeleteMessage",
        "sqs:GetQueueAttributes",
      ]
      Resource = aws_sqs_queue.jobs.arn
    }]
  })
}

resource "aws_lambda_function" "worker" {
  function_name    = "worker"
  role             = aws_iam_role.worker.arn
  runtime          = "python3.12"
  handler          = "app.handler"
  filename         = data.archive_file.worker.output_path
  source_code_hash = data.archive_file.worker.output_base64sha256
  timeout          = 30
}

resource "aws_lambda_event_source_mapping" "worker_jobs" {
  event_source_arn = aws_sqs_queue.jobs.arn
  function_name    = aws_lambda_function.worker.arn
  batch_size       = 10
}

Tips for better results

  • Compare against cdk synth. The synthesized template lists every resource CDK really creates. Make sure each one has a Terraform block in the output.
  • Paste the whole stack. Include the imports and the App setup so the converter sees the region, the environment and every construct.
  • Review IAM carefully. grant* methods expand to specific action lists. Check the Terraform policies are no broader, and no narrower, than what CDK granted.
  • Plan before you apply. After importing, terraform plan should show no changes. Anything else means an argument differs from what's deployed.
  • Convert one stack at a time. Give each converted stack its own Terraform root module or state, which keeps imports and rollbacks manageable.

Common mistakes to avoid

  • Destroying the stack too early. Deploy RemovalPolicy.RETAIN on stateful resources first. cdk destroy without it deletes your data.
  • Dropping the logging permissions. CDK attaches AWSLambdaBasicExecutionRole to every function role it creates. If the Terraform role lacks it, the function can't write to CloudWatch Logs.
  • Letting Terraform pick new names. CDK-generated names like OrdersStack-OrdersTable1A2B3C must be used as-is in import blocks. A different name forces a replacement.
  • Missing custom resources. Options such as autoDeleteObjects: true deploy a Lambda-backed custom resource. Terraform has no equivalent, so decide whether you still need that behavior.
  • Forgetting the package step. Terraform won't zip your Lambda code on its own. Keep the archive_file data source, or build the zip in CI, and set source_code_hash so code changes redeploy.

Frequently asked questions

How do I convert AWS CDK to Terraform?

Paste your CDK stack into the converter above and press Convert to Terraform. The output declares every resource the constructs create, including IAM roles and policies. Compare it with cdk synth, then adopt deployed resources with import blocks and confirm terraform plan shows no changes.

Will Terraform take over my existing CDK resources?

Not automatically. The CloudFormation stack still owns them. Set RemovalPolicy.RETAIN and deploy, remove the resources from the stack, then bring them into Terraform with import blocks (Terraform 1.5 or later) and check the plan. For the trade-offs, see AWS CDK vs Terraform for existing AWS stacks.

Why is the Terraform output much longer than my CDK code?

CDK constructs hide resources and defaults, such as execution roles, IAM policies and bucket settings. Terraform declares each of them, so the same infrastructure takes more lines.

What happens to Lambda asset code?

CDK zips and uploads asset folders for you. In Terraform you need your own packaging step, for example an archive_file data source that zips the folder, referenced by filename and source_code_hash on the function.

Can I convert CDK written in Python?

This converter expects TypeScript or JavaScript. For a Python stack, first convert it with the CDK Python to TypeScript converter, then paste the result here.

Does it work with CDK v1 code?

Paste it and see, but v1 reached end-of-support on June 1, 2023, and its @aws-cdk/* packages differ from v2's aws-cdk-lib. Results are most reliable with v2 code.

Can I get a CloudFormation template instead?

Yes, and you don't need a converter for that: cdk synth prints the CloudFormation template your stack deploys. This tool is for when you want Terraform to manage the infrastructure.

Is my Terraform or CDK code stored?

No. The code is sent to an AI model hosted on NVIDIA's API only to produce the conversion, and ChatWithCloud doesn't store or log it. Your latest draft stays in your browser. Before converting, replace account IDs you consider sensitive, and any secrets in variable defaults, with placeholders. For the full picture, see whether it's safe to paste AWS code into an AI converter.

Is it free, and how much can I paste?

Yes. Each conversion takes up to 60,000 characters, and you can open files up to 500 KB. Without an account you get 6 conversions a minute and 60 a day. A free account raises that to 12 a minute and 200 a day and lists your recent conversions on your dashboard. The guide to ChatWithCloud free converter limits shows how far they stretch for a real migration.

Related tools and next steps