Infrastructure2025-03-20

Terraform vs CDK: Choosing Your IaC Tool

Choosing an Infrastructure as Code tool shapes how your team defines, deploys, and maintains cloud resources for years. Terraform and AWS CDK represent two fundamentally different approaches to the same problem: Terraform uses a declarative domain-specific language, while CDK uses general-purpose programming languages to generate CloudFormation. This guide compares them across the dimensions that matter in practice.

Language: HCL vs Programming Languages

Terraform uses HashiCorp Configuration Language (HCL), a declarative DSL designed specifically for infrastructure definition:

resource "aws_lambda_function" "api" {
  function_name = "api-handler"
  runtime       = "nodejs20.x"
  handler       = "index.handler"
  memory_size   = 512
  timeout       = 30

  filename         = data.archive_file.lambda_zip.output_path
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256

  role = aws_iam_role.lambda_role.arn

  environment {
    variables = {
      TABLE_NAME = aws_dynamodb_table.data.name
    }
  }
}

CDK uses TypeScript, Python, Java, Go, or C# to define the same infrastructure:

const fn = new lambda.Function(this, 'ApiHandler', {
  runtime: lambda.Runtime.NODEJS_20_X,
  handler: 'index.handler',
  memorySize: 512,
  timeout: Duration.seconds(30),
  code: lambda.Code.fromAsset('./lambda'),
  environment: {
    TABLE_NAME: table.tableName,
  },
});

table.grantReadWriteData(fn);

The CDK example is shorter because L2 constructs provide smart defaults and convenience methods like grantReadWriteData, which generates the correct IAM policy automatically. In Terraform, you write the IAM policy explicitly.

HCL advantages:

  • Declarative intent is immediately visible.
  • No programming knowledge required. Infrastructure engineers and DevOps teams can be productive quickly.
  • The plan output shows exactly what will change before applying.
  • Language constraints prevent overly clever abstractions.

Programming language advantages:

  • Full IDE support: autocomplete, refactoring, type checking.
  • Loops, conditionals, and functions work naturally instead of through HCL's limited constructs.
  • Existing testing frameworks apply directly.
  • Shared libraries through npm, PyPI, or other package managers.

State Management

Terraform maintains a state file that maps your configuration to real-world resources. This state file is the source of truth for what Terraform manages.

terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

Terraform state challenges:

  • State files must be stored securely (they contain sensitive data).
  • Concurrent modifications require locking (DynamoDB for S3 backend).
  • State drift occurs when resources are modified outside Terraform.
  • Large state files slow down planning and applying.

CDK delegates state management to CloudFormation, which tracks state internally within AWS. You never see or manage a state file. CloudFormation handles locking, drift detection, and rollbacks natively.

Terraform's approach gives you more control and visibility. You can import existing resources, move resources between state files, and manually fix state when things go wrong.

CDK's approach eliminates an entire category of operational concerns. You never lose a state file, never deal with state locks, and never accidentally expose secrets stored in state.

Provider Ecosystem

Terraform's provider model supports every major cloud and hundreds of SaaS services through a unified interface:

# In the same configuration
resource "aws_s3_bucket" "data" { }
resource "cloudflare_record" "api" { }
resource "datadog_monitor" "latency" { }
resource "github_repository" "app" { }

CDK is AWS-focused. While CDKTF (CDK for Terraform) brings CDK's programming model to Terraform providers, the ecosystem is smaller. If your infrastructure spans multiple clouds or includes significant non-AWS resources, Terraform provides a more unified experience.

For AWS-only shops, CDK's deeper integration with AWS services, CloudFormation's native rollback support, and L2 constructs that encode AWS best practices make it a strong choice.

Testing Approaches

Terraform testing has improved significantly with the native terraform test framework:

# tests/lambda.tftest.hcl
run "lambda_function_exists" {
  command = plan

  assert {
    condition     = aws_lambda_function.api.runtime == "nodejs20.x"
    error_message = "Lambda function should use Node.js 20"
  }
}

run "lambda_has_correct_memory" {
  command = plan

  assert {
    condition     = aws_lambda_function.api.memory_size == 512
    error_message = "Lambda function should have 512MB memory"
  }
}

CDK testing uses standard testing frameworks with a dedicated assertions library:

import { Template } from 'aws-cdk-lib/assertions';

test('Lambda function has correct configuration', () => {
  const template = Template.fromStack(stack);

  template.hasResourceProperties('AWS::Lambda::Function', {
    Runtime: 'nodejs20.x',
    MemorySize: 512,
    Timeout: 30,
  });
});

test('Lambda has DynamoDB read/write access', () => {
  const template = Template.fromStack(stack);

  template.hasResourceProperties('AWS::IAM::Policy', {
    PolicyDocument: {
      Statement: Match.arrayWith([
        Match.objectLike({
          Action: Match.arrayWith(['dynamodb:GetItem', 'dynamodb:PutItem']),
        }),
      ]),
    },
  });
});

CDK's testing story is stronger because you have access to the full testing ecosystem of your language: mocking, property-based testing, integration test frameworks, and CI tooling.

Learning Curve

Terraform has a lower initial learning curve. HCL is simple, the documentation is excellent, and the plan/apply workflow is intuitive. Most developers can write basic Terraform within a day. The complexity comes from understanding provider-specific resource configurations, managing state, and structuring modules for reuse.

CDK requires familiarity with both a programming language and CloudFormation concepts. The L2 constructs hide complexity but can be confusing when they make unexpected decisions. Debugging often requires understanding the generated CloudFormation template. The learning curve is steeper initially but levels off as the programming language skills pay dividends in code organization and reuse.

When to Use Each

Choose Terraform when:

  • Your infrastructure spans multiple cloud providers.
  • Your team includes infrastructure specialists who prefer declarative configuration.
  • You need to manage non-AWS resources (GitHub, Datadog, PagerDuty) alongside AWS.
  • You want explicit control over state management.
  • Your organization has existing Terraform modules and expertise.

Choose CDK when:

  • You are fully committed to AWS.
  • Your developers write application code in TypeScript or Python and want infrastructure in the same language.
  • You need complex logic: loops over dynamic data, conditional resources based on configuration, or custom constructs that encode organizational standards.
  • You want testing with standard frameworks and IDE tooling.
  • You value automatic IAM policy generation and security-by-default patterns.

Migration Paths

Terraform to CDK: Import existing resources into CloudFormation stacks using cdk import. This is resource-by-resource and can be tedious for large estates. Some teams run both tools in parallel, gradually migrating services to CDK as they're rewritten.

CDK to Terraform: Use cdk synth to generate CloudFormation templates, then use cf2tf to convert them to HCL. The output requires manual cleanup but provides a starting point. Alternatively, CDKTF lets you keep the CDK programming model while targeting Terraform providers.

Running both: Many organizations use Terraform for foundational infrastructure (networking, accounts, shared services) and CDK for application-specific infrastructure. This works well when the boundary is clear and teams don't need to frequently reference resources across tools.

Neither tool is universally better. The right choice depends on your team's skills, cloud strategy, and the complexity of your infrastructure. Both tools are mature, well-supported, and capable of managing production workloads reliably.

© 2025 DevPractical. Practical guides for modern software engineering.