· nervico-team · cloud-architecture  · 12 min read

Infrastructure as Code: Terraform vs CDK vs Pulumi

Practical comparison of Terraform, AWS CDK, and Pulumi: real differences in productivity, learning curve, ecosystem, and when to choose each infrastructure as code tool.

Practical comparison of Terraform, AWS CDK, and Pulumi: real differences in productivity, learning curve, ecosystem, and when to choose each infrastructure as code tool.

If your infrastructure exists only in the AWS console, it does not really exist. It exists as tacit knowledge in the head of whoever created it. When that person goes on vacation, changes companies, or simply forgets why they configured something a certain way, your team inherits infrastructure that nobody fully understands.

Infrastructure as Code solves this problem. All infrastructure is defined in text files that are versioned in Git, reviewed in pull requests, and deployed automatically. If something breaks, you can rebuild it exactly as it was. If you need an environment identical to production, you create it by running a command.

The concept is clear. The hard decision is which tool to use. The three main options are Terraform, AWS CDK, and Pulumi. Each has a different philosophy, a different learning curve, and a different ecosystem. This article compares them with technical depth, real examples, and an honest recommendation.

Why Infrastructure as Code Is Not Optional

The Cost of Manual Infrastructure

Infrastructure created manually in the console has these problems:

It is not reproducible: If you need to create a second environment (staging, disaster recovery), you have to repeat every step manually. Each repetition introduces variations.

It has no history: You do not know who created a resource, when, or why. There is no change log. There is no way to roll back.

It is not reviewable: Infrastructure changes go through no review process. Someone can modify a production Security Group without anyone knowing.

It does not scale: With 5 resources, manual management is feasible. With 50, it is fragile. With 500, it is impossible.

The Concrete Benefits of IaC

  • Reproducibility: The same code produces the same infrastructure. Every time.
  • Versioning: Git has the complete history of every change. Who, when, why, which code review.
  • Review: Changes go through pull requests. Another engineer reviews before applying.
  • Automation: CI/CD applies changes automatically after approval.
  • Documentation: The code is the documentation. If the infrastructure is in Terraform, the Terraform file describes exactly what exists.

Terraform: The De Facto Standard

What Terraform Is

Terraform is an open source tool created by HashiCorp in 2014. It defines infrastructure in a declarative language called HCL (HashiCorp Configuration Language). It supports more than 3,000 providers: AWS, GCP, Azure, Cloudflare, GitHub, Datadog, PagerDuty, and virtually any service with an API.

How It Works

The Terraform workflow has three steps:

  1. terraform init: Downloads the required providers.
  2. terraform plan: Compares the desired state (the code) with the current state (the state file) and shows what it will create, modify, or destroy.
  3. terraform apply: Executes the changes.

The state file is the critical component. It is a JSON file that stores the current state of the infrastructure. Terraform compares it with your code to calculate what changes are needed. If the state file is corrupted or lost, Terraform does not know what exists and may attempt to recreate resources that already exist.

State storage: Never store the state file in Git. Use a remote backend:

  • S3 + DynamoDB (recommended on AWS): S3 stores the file, DynamoDB manages locking to prevent concurrent changes.
  • Terraform Cloud/Enterprise: Managed backend by HashiCorp with UI, collaboration, and governance.

Example: VPC with Subnets in Terraform

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name        = "production-vpc"
    Environment = "production"
  }
}

resource "aws_subnet" "public" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]

  map_public_ip_on_launch = true

  tags = {
    Name = "public-subnet-${count.index + 1}"
    Type = "public"
  }
}

resource "aws_subnet" "private" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 10)
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "private-subnet-${count.index + 1}"
    Type = "private"
  }
}

Terraform Strengths

True multi-cloud: The same language and workflow for AWS, GCP, Azure, and hundreds of other providers. If your strategy includes multi-cloud, Terraform is the only viable option among the three.

Massive ecosystem: More than 3,000 providers, thousands of public modules on the Terraform Registry, extensive community documentation.

Plan before apply: terraform plan shows exactly what will change before doing it. It is a safety net that prevents errors.

Maturity: 10 years of development. Most common problems have documented solutions.

Terraform Limitations

HCL is not a programming language: It does not have complex for loops, user-defined functions, classes, or inheritance. Workarounds with count, for_each, and dynamic blocks work but are verbose and hard to read.

State management is complex: The state file is a single point of failure. State locking, state migrations, importing existing resources: all require specific knowledge.

Limited drift detection: Terraform only detects drift when you run terraform plan. If someone modifies a resource in the console, Terraform does not know until the next plan.

New AWS service adoption speed: Terraform providers are maintained by the community (or HashiCorp). When AWS launches a new service, it can take weeks or months to be available in Terraform. CDK has immediate access.

AWS CDK: Infrastructure in Languages You Already Know

What CDK Is

AWS CDK (Cloud Development Kit) lets you define infrastructure in TypeScript, Python, Java, C#, or Go. Instead of a domain-specific language like HCL, you write code in a language your team already knows. CDK generates CloudFormation templates that AWS executes.

How It Works

  1. Write code: Define your infrastructure using CDK classes and constructs in your chosen language.
  2. cdk synth: CDK synthesizes the code into a CloudFormation template (JSON/YAML).
  3. cdk deploy: Uploads the template to CloudFormation, which executes the changes.

CDK does not manage state directly. CloudFormation does. Each CDK stack corresponds to a CloudFormation stack, and CloudFormation maintains the state.

Example: VPC with Subnets in CDK (TypeScript)

import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class NetworkStack extends Stack {
  public readonly vpc: ec2.Vpc;

  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    this.vpc = new ec2.Vpc(this, 'ProductionVpc', {
      maxAzs: 2,
      natGateways: 1,
      subnetConfiguration: [
        {
          name: 'Public',
          subnetType: ec2.SubnetType.PUBLIC,
          cidrMask: 24,
        },
        {
          name: 'Private',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
          cidrMask: 24,
        },
        {
          name: 'Isolated',
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
          cidrMask: 24,
        },
      ],
    });
  }
}

Notice the difference: CDK creates the VPC, subnets, route tables, Internet Gateway, NAT Gateway, and all associations with a single construct. In Terraform, you would need to define each resource individually.

Constructs: The Key Abstraction

CDK organizes infrastructure in three levels of abstraction:

L1 (CfnResources): Direct 1:1 mapping to CloudFormation. Maximum control, minimum abstraction. Equivalent to Terraform resources.

L2 (Higher-level constructs): Constructs with sensible defaults and helper methods. The VPC example above is L2. It automatically configures Security Groups, route tables, and gateways with best practices.

L3 (Patterns): Constructs that combine multiple resources for common patterns. For example, ApplicationLoadBalancedFargateService creates an ECS cluster, a Fargate service, an ALB, target groups, listener rules, and auto-scaling in a single construct.

CDK Strengths

Real languages: Autocomplete, type checking, refactoring, unit tests. Your IDE knows what properties exist. Type errors are caught before deployment.

Powerful abstractions: L2 and L3 constructs encapsulate best practices. A junior developer can create a solid architecture without knowing every CloudFormation detail.

Immediate access to new AWS services: CDK is generated from the CloudFormation specification. When AWS launches a new service, it is available in CDK the same day.

Unit tests: You can test your infrastructure with Jest, pytest, or JUnit. Verify that your VPC has the correct number of subnets, that your S3 bucket has encryption enabled, that your Lambda has the right timeout.

Code sharing: Create custom construct libraries for your organization. A “MyCompanyRestApi” construct that includes API Gateway, Lambda, DynamoDB, CloudWatch alarms, and compliance tags. Reusable by all teams.

CDK Limitations

Total CloudFormation dependency: CDK generates CloudFormation. CloudFormation limits are CDK limits: 500 resources per stack, long rollback times, debugging errors at the CloudFormation level (not the CDK level).

AWS only: CDK is exclusive to AWS. If you need to manage resources on GCP, Azure, or third-party services, you need another tool.

CloudFormation drift: If someone modifies a resource outside CDK/CloudFormation, CloudFormation can enter an inconsistent state. Drift detection exists but is not perfect.

Construct learning curve: Even though you use a language you know, CDK constructs have their own API that you need to learn. Documentation is extensive but not always clear.

Pulumi: The Emerging Competitor

What Pulumi Is

Pulumi is an Infrastructure as Code tool that, like CDK, lets you use real programming languages (TypeScript, Python, Go, C#, Java). Unlike CDK, Pulumi does not depend on CloudFormation. It has its own execution engine and supports multiple cloud providers.

How It Works

  1. Write code: Similar to CDK, you define infrastructure in your chosen language.
  2. pulumi preview: Equivalent to terraform plan. Shows what will change.
  3. pulumi up: Applies changes directly against provider APIs.

Pulumi manages its own state, which can be stored in:

  • Pulumi Cloud: Managed backend (free for individual use, paid for teams).
  • S3, Azure Blob, GCS: Self-managed backends.
  • Local: For development only.

Example: VPC with Subnets in Pulumi (TypeScript)

import * as aws from '@pulumi/aws';
import * as awsx from '@pulumi/awsx';

const vpc = new awsx.ec2.Vpc('production-vpc', {
  cidrBlock: '10.0.0.0/16',
  numberOfAvailabilityZones: 2,
  natGateways: { strategy: 'Single' },
  subnetStrategy: 'Auto',
  subnetSpecs: [
    { type: awsx.ec2.SubnetType.Public, cidrMask: 24 },
    { type: awsx.ec2.SubnetType.Private, cidrMask: 24 },
    { type: awsx.ec2.SubnetType.Isolated, cidrMask: 24 },
  ],
});

export const vpcId = vpc.vpcId;
export const publicSubnetIds = vpc.publicSubnetIds;
export const privateSubnetIds = vpc.privateSubnetIds;

Pulumi Strengths

Multi-cloud with real languages: The combination that neither Terraform nor CDK offers. Real programming languages with multi-cloud support.

No CloudFormation: Pulumi interacts directly with provider APIs. No 500-resource stack limits, no CloudFormation rollback delays, no extra abstraction layer.

Ecosystem Crosswalk (awsx): High-level components similar to CDK L3 constructs. Less mature but under active development.

Policy as Code: Pulumi CrossGuard lets you define compliance policies in code that are evaluated before each deployment. “No S3 bucket can be public” is expressed as an executable policy.

Pulumi Limitations

Lower adoption: Terraform has a 10-year ecosystem. CDK has AWS backing. Pulumi is newer and has fewer community resources, fewer examples, and fewer engineers who know it.

Managed backend push: Pulumi steers users toward its managed cloud service. Self-managed backends work but require more configuration.

Less mature documentation: Compared to Terraform or CDK, Pulumi documentation has more gaps, especially for advanced use cases.

Hiring: Finding engineers with Pulumi experience is significantly harder than finding those with Terraform experience.

Direct Comparison

CriterionTerraformAWS CDKPulumi
LanguageHCLTS, Python, Java, C#, GoTS, Python, Go, C#, Java
Multi-cloudNativeNo (AWS only)Native
State managementOwn (S3+DynamoDB)CloudFormationOwn (Pulumi Cloud/S3)
Market adoptionDominantGrowingEmerging
Learning curveMedium (HCL)Low-MediumLow-Medium
Unit testsLimitedNativeNative
High-level abstractionsModulesL2/L3 ConstructsCrosswalk (awsx)
New AWS service accessWeeks/monthsImmediateDays/weeks
EcosystemMassiveLargeGrowing
DebuggingPlan + stateCloudFormation eventsPreview + logs

When to Choose Each Tool

Choose Terraform When

  • Your strategy is multi-cloud or will be in the future.
  • Your team already knows Terraform. The cost of switching to CDK or Pulumi rarely justifies itself if the team is productive with Terraform.
  • You need to manage resources outside AWS: DNS on Cloudflare, monitoring on Datadog, repositories on GitHub. Terraform has providers for everything.
  • You prefer a declarative language that describes the final state without complex programming logic.

Choose CDK When

  • Your infrastructure is 100% AWS and there are no multi-cloud plans.
  • Your team consists of developers (not operations) who know TypeScript or Python well. CDK lets developers manage infrastructure without learning HCL.
  • You need powerful abstractions: CDK L2 and L3 constructs are the most mature and complete on the market.
  • You want infrastructure unit tests integrated into your CI pipeline.
  • You need immediate access to new AWS services the day they launch.

Choose Pulumi When

  • You need multi-cloud with real languages. It is the only tool that offers both.
  • CloudFormation limits are a problem for your architecture (large stacks, slow deployments).
  • Your team wants a modern experience with a product that evolves rapidly.
  • Policy as Code is a requirement for your organization.

Common IaC Patterns

Project Structure

Regardless of the tool, organize your infrastructure in layers:

infrastructure/
  networking/       # VPC, subnets, NAT, VPN
  security/         # IAM roles, policies, Security Groups
  data/             # RDS, DynamoDB, ElastiCache, S3
  compute/          # ECS, Lambda, EC2
  monitoring/       # CloudWatch, alarms, dashboards
  cicd/             # CodePipeline, GitHub Actions config

Each layer is an independent module (Terraform), stack (CDK), or project (Pulumi). Changes in networking should not require redeploying compute.

GitOps for Infrastructure

The ideal workflow:

  1. An engineer creates a branch and modifies the infrastructure code.
  2. They open a pull request. CI runs plan / synth / preview and posts the result as a comment on the PR.
  3. Another engineer reviews the change and the resources that will be created, modified, or destroyed.
  4. After approval, merging to main triggers automatic deployment.

This workflow ensures every infrastructure change is reviewed, documented, and reversible.

Environment Management

Do not copy code for each environment. Use parameterization:

  • Terraform: Variables and workspaces (or separate directories with shared modules).
  • CDK: Stack props and Context values.
  • Pulumi: Stack configurations and config files.

The code is the same. The parameters change: names, instance sizes, replica counts, domains.

Importing Existing Infrastructure

If you already have manually created infrastructure, all three tools allow importing:

  • Terraform: terraform import for individual resources, or tools like terraformer for bulk import.
  • CDK: cdk import (experimental) or CloudFormation resource import.
  • Pulumi: pulumi import with automatic code generation.

Importing is never perfect. Expect to invest time adjusting the generated code to match your style and structure.

Conclusion

Infrastructure as Code is not optional. It is a requirement for any team that wants to operate infrastructure professionally. The tool you choose matters less than the fact that you choose one and use it consistently.

If you have a DevOps team that already knows Terraform, stick with Terraform. If you have a TypeScript development team managing AWS infrastructure, CDK is probably the most productive option. If you need multi-cloud with the power of a programming language, evaluate Pulumi.

What is not acceptable is using none of them. Every resource created manually in the console is operational debt that someone will have to pay.

At NERVICO, we help teams adopt Infrastructure as Code with the right tool for their context. Request a free audit and we will evaluate the state of your infrastructure and the most appropriate IaC strategy.

Back to Blog

Related Posts

View All Posts »