AWS Cloud Development Kit (CDK) vs. Terraform

spacelift_team

Spacelift team

Posted on November 25, 2024

AWS Cloud Development Kit (CDK) vs. Terraform

In this post we will compare two very different tools for infrastructure as code (IaC): AWS Cloud Development Kit and HashiCorp Terraform.

How do Terraform and CDK differ? AWS CDK is an open-source framework that enables users to define cloud infrastructure using general-purpose programming languages, natively integrating with AWS services, whereas Terraform is a cloud-agnostic provisioning tool that manages infrastructure declaratively using HCL, making it more versatile for multicloud environments.

What is Infrastructure as Code?

Infrastructure as code (IaC) is the practice of defining your infrastructure (your virtual networks, Kubernetes clusters, DNS records, and more) in a configuration language, a Bash script, or even using a proper programming language.

You configure your cloud infrastructure in code, you apply it, and then your infrastructure comes to life. This contrasts starkly with a manual approach of clicking your way through a graphical user interface to create the resources your applications depend on.

infrastructure as code diagram

In the world of IaC, there are two distinct types: declarative and imperative.

Declarative IaC is all about the destination. The code you write describes the desired end state. The order of the code blocks does not matter. What matters is how infrastructure resources are related to one another and how those relationships are expressed. 

From the expression of these relationships, the IaC tool can form a DAG (Directed Acyclic Graph) which determines the order resources are created. Failing to properly specify an existing relationship between two resources generally leads to an error. In this blog post, we will look at Terraform, an early player in the world of declarative IaC.

Imperative IaC takes the journey to the destination into account. The code you write follows steps in a certain order. Shifting blocks of code around would generally mean these steps are performed in the wrong order, which would break the code. In this blog post, we will look at AWS CDK, a pioneer in the world of imperative IaC.

These benefits are common to all types of IaC:

  • Write code once, apply it many times. You can create repeatable modules of infrastructure that work the same way each time you apply it.
  • Keeping your IaC under source control gives you an audit trail of all changes made throughout your infrastructure's lifecycle.
  • Changes to your infrastructure follow the same workflow as application code. You can build and test your code and use the same peer-review process as for the rest of your application code.
  • Your infrastructure code documents the infrastructure underlying your applications. This documentation is always up to date, unlike a diagram in a wiki.

Read more: Business Benefits of Infrastructure as Code

What is AWS CDK?

AWS CDK is an evolution of AWS CloudFormation, the original tool for IaC on AWS. You can use a few different languages for AWS CDK:

  • JavaScript
  • TypeScript
  • Python
  • Java
  • C#
  • Go

There is no functional difference between the different languages, so your choice of language does not limit your features. Under the hood, AWS CDK uses a tool named jsii. It allows for class libraries written in JavaScript/TypeScript to be used from other languages, enabling the AWS CDK team to provide many languages with the same functionality.

AWS CDK uses the imperative approach to IaC. However, the purpose of the AWS CDK code is to generate AWS CloudFormation templates. 

AWS CDK features

  • Programmatic code: AWS CDK uses familiar programming languages to define cloud infrastructure.
  • Constructs: The CDK introduces constructs, which are pre-configured cloud components that can be composed to build higher-level abstractions, improving code reuse and modularity.
  • Imperative control: Unlike declarative IaC tools, CDK allows conditional logic, loops, and complex control flows to be used in code, giving developers flexibility and power in infrastructure definitions.
  • CloudFormation compatibility: CDK translates high-level constructs into CloudFormation templates, providing the power and reliability of CloudFormation with the flexibility of programming languages.
  • Built-in validation: CDK provides automatic checks and validations during synthesis, helping catch configuration errors early, ensuring smoother deployments, and minimizing runtime failures.

How does AWS CDK work?

To get started with AWS CDK you need a few prerequisites. The exact requirements depend on what language you plan on using for AWS CDK. See the AWS documentation for all the relevant details for the language you choose. For the following demonstration, Go will be the language of choice.

You can create a new AWS CDK project using the app project template through the following CDK CLI command:

$ cdk init app --language go
Applying project template app for go
... (details omitted) ...
Initializing a new git repository...
✅ All done!
Enter fullscreen mode Exit fullscreen mode

Infrastructure in AWS CDK comes in the form of stacks, the same concept as an AWS CloudFormation stack. This is how the state of your infrastructure is maintained. An AWS CDK application can consist of one or many stacks. The app template we initialized consists of a single stack.

The stack is initialized in the main function:

func main() {
    // details left out for brevity ...

    NewNetworkStack(app, "NetworkStack", &NetworkStackProps{
        awscdk.StackProps{
            Env: env(),
       },
    })

    // details left out for brevity ...
}
Enter fullscreen mode Exit fullscreen mode

Part of the stack initialization takes a definition of an environment. An environment comprises the appropriate AWS account and AWS region. You can target different AWS environments in the same CDK application.

The stack content is defined in the NewNetworkStack function. The boilerplate code for this function is:

func NewNetworkStack(...) awscdk.Stack {
    var sprops awscdk.StackProps
    if props != nil {
        sprops = props.StackProps
    }
    stack := awscdk.NewStack(scope, &id, &sprops)

    // your infrastructure goes here

    return stack
}
Enter fullscreen mode Exit fullscreen mode

The NewNetworkStack function is where you define the infrastructure that goes into your stack. Our goal is to create a VPC with related resources, so we must import the EC2 package because this is where the VPC resources live:

import (
  // other packages omitted for brevity ...
  ec2 "github.com/aws/aws-cdk-go/awscdk/v2/awsec2"
)
Enter fullscreen mode Exit fullscreen mode

You should run the following command to download all the required packages your application is referencing:

$ go mod tidy
Enter fullscreen mode Exit fullscreen mode

With the EC2 package imported, we can configure the VPC resource:

// in the NewNetworkStack function
ec2.NewVpc(stack, jsii.String("vpc"), &ec2.VpcProps{})
Enter fullscreen mode Exit fullscreen mode

By default, the NewVpc function will create a VPC with a public and private subnet in each of the selected AWS region's availability zones, along with an internet gateway, NAT gateways, route tables, routes, and more. That single line of code does a lot of work!

The code you write with AWS CDK is synthesized into AWS CloudFormation templates. The resulting CloudFormation templates are then finally deployed to your AWS environment using CloudFormation as the deployment engine. 

Does that mean that AWS CDK is bound by all the limitations of the underlying CloudFormation framework? Not exactly. 

AWS CDK has some limitations, but it can go beyond AWS CloudFormation in some ways. This is usually done by using custom AWS Lambda functions that perform required tasks during deployment.

We can synthesize the AWS CDK code to a CloudFormation template in our terminal if we wish to see what the end result will be:

$ cdk synth
Resources:
  vpcA2121C38:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      # the rest of the template is omitted for brevity
Enter fullscreen mode Exit fullscreen mode

If you examine the resulting CloudFormation template, you see that the single line of CDK code created a lot of CloudFormation code.

To deploy the resources with AWS CDK, run the deploy command:

$ cdk deploy
Enter fullscreen mode Exit fullscreen mode

You must approve any IAM permissions, network port openings, and similar sensitive changes by answering y (yes) in a prompt. After accepting the coming changes, AWS CDK sends the synthesized CloudFormation templates up to AWS and streams the stack creation status to your terminal.

The deployment finishes eventually:

NetworkStack: deploying... [1/1]
NetworkStack: creating CloudFormation changeset...

 ✅  NetworkStack

✨  Deployment time: 152.43s

Stack ARN:
arn:aws:cloudformation:eu-west-1:<account id>:stack/NetworkStack/<stack id>

✨  Total time: 154.79s
Enter fullscreen mode Exit fullscreen mode

You can delete the stack using the destroy command and replying y (yes) at the prompt:

$ cdk destroy
Are you sure you want to delete: NetworkStack (y/n)? y
Enter fullscreen mode Exit fullscreen mode

Resources in AWS CDK are called constructs. Different types of construct have different abstraction levels:

  • The lowest level of construct is a direct mapping from the underlying CloudFormation resources.
  • An example of the highest level of construct is ApplicationLoadBalancedFargateService, this construct represents a large collection of underlying resources that are configured to create an ECS Fargate cluster with an Application Load Balancer (ALB).
  • Most constructs are somewhere in between. An example is the Vpc resource.

Most constructs have sensible default values, which means that you often do not have to configure many settings to get a working resource. We saw this above for the VPC resource.

If you like these types of abstractions, then the CDK is a good choice.

💡 You might also like:

What is Terraform?

Terraform is an infrastructure provisioning tool that allows users to manage infrastructure across multiple cloud platforms (like AWS, Microsoft Azure, Google Cloud Platform, and other cloud providers) using a declarative language called to define the infrastructure.

It emerged from the idea of creating a "CloudFormation for everything" during the very early days of HashiCorp, the organization behind Terraform. The idea of using a single tool to create infrastructure spanning multiple target platforms (cloud providers, on-premise infrastructure, SaaS products, and more) is mesmerizing.

Terraform features

  • Provider ecosystem: To get started with Terraform, you have to download a single binary for your system architecture. However, before you can do anything, you also need providers. The Terraform binary is the core. It does many things (like parsing the language and orchestrating the work of the providers). 

The providers are the plugins that allow Terraform to work with a target system (e.g., AWS). The AWS provider for Terraform is arguably the most successful provider for Terraform ever, with over 3.3 billion downloads at the time of writing.

  • Declarative code: Terraform code is written in the HashiCorp Configuration Language (HCL). This domain-specific language is used in many tools in the HashiCorp suite. It is relatively easy to learn and has many features that make it easy to work with compared to traditional JSON or YAML configuration languages.

However, you can also write Terraform configurations using JSON. JSON is a good choice for code intended to be read programmatically, but it is less suited for human interaction. For that, HCL is a better choice.

  • State management: Terraform maintains a state file that represents the current state of the infrastructure. This state allows Terraform to track resource changes, identify what needs to be updated, and perform diffs during deployment.
  • Modular infrastructure: Terraform supports the creation of reusable modules, which allow users to package and share sets of resources, improving code reusability and enabling consistent deployment patterns across projects.

How does Terraform work?

In the previous section, we saw how we can create a fully configured AWS VPC with a single line of Go code using AWS CDK. In the following paragraphs, we will create the corresponding infrastructure in Terraform, and we will see that we need a lot more than a single line to do it.

We begin by specifying the Terraform provider our configuration needs (AWS), as well as configuring the provider. 

This is a similar step to importing packages into an AWS CDK application. If we want to target multiple AWS environments (either accounts or regions), we must configure the AWS provider multiple times.

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "5.67.0"
    }
  }
}

variable "aws_region" {
  type    = string
  default = "eu-west-1"
}

provider "aws" {
  region = var.aws_region
}
Enter fullscreen mode Exit fullscreen mode

Next, we create the VPC resource and an internet gateway attached to this VPC:

resource "aws_vpc" "this" {
  cidr_block = "10.0.0.0/16"
  tags = {
    Name = "spacelift-${var.aws_region}"
  }
}

resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id
  tags = {
    Name = "spacelift-${var.aws_region}"
  }
}
Enter fullscreen mode Exit fullscreen mode

The internet gateway resource is used to route traffic to and from the internet.

We want to create one public and one private subnet for each availability zone in the selected region. So we must have a way to find the availability zones for our selected region. This can be done using the availability zones data source:

data "aws_availability_zones" "all" {}
Enter fullscreen mode Exit fullscreen mode

Now we can create the subnets, both public and private:

resource "aws_subnet" "public" {
  count      = length(data.aws_availability_zones.all.names)
  vpc_id     = aws_vpc.this.id
  cidr_block = cidrsubnet(aws_vpc.this.cidr_block, 8, count.index)

  availability_zone = data.aws_availability_zones.all.names[count.index]

  tags = {
    Name = "spacelift-${var.aws_region}-public-${count.index}"
  }
}

resource "aws_subnet" "private" {
  count      = length(data.aws_availability_zones.all.names)
  vpc_id     = aws_vpc.this.id
  cidr_block = cidrsubnet(aws_vpc.this.cidr_block, 8, 128 + count.index)

  availability_zone = data.aws_availability_zones.all.names[count.index]

  tags = {
    Name = "spacelift-${var.aws_region}-private-${count.index}"
  }
}
Enter fullscreen mode Exit fullscreen mode

The difference between a public and a private subnet is whether there is a direct route to the internett. These details are configured in route tables attached to the subnets.

Resources in the private subnets must still be able to reach the internet, but the routing goes via Network Address Translation (NAT) gateways. We can create the NAT gateways with corresponding elastic IP addresses:

resource "aws_eip" "natgw" {
  count = length(data.aws_availability_zones.all.names)
  tags = {
    Name = "spacelift-${var.aws_region}-${count.index}"
  }
}

resource "aws_nat_gateway" "all" {
  count         = length(data.aws_availability_zones.all.names)
  subnet_id     = aws_subnet.public[count.index].id
  allocation_id = aws_eip.natgw[count.index].id
  tags = {
    Name = "spacelift-${var.aws_region}-${count.index}"
  }
}
Enter fullscreen mode Exit fullscreen mode

We need route tables for both public and private subnets and must attach them to each of the subnets. We start with the public route table, which can be reused for each public subnet. The public route table has a single route, sending all traffic to the internet via the internet gateway.

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.this.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.this.id
  }

  tags = {
    Name = "spacelift-${var.aws_region}-public"
  }
}

resource "aws_route_table_association" "public" {
  count          = length(data.aws_availability_zones.all.names)
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}
Enter fullscreen mode Exit fullscreen mode

The association between a route table and a subnet is created as a separate resource.

For the private subnets, we want to route all traffic via NAT Gateways. Routing traffic in the same availability zone means that each private subnet must have its own route table that uses the NAT gateway in the same availability zone as the subnet.

resource "aws_route_table" "private" {
  count  = length(data.aws_availability_zones.all.names)
  vpc_id = aws_vpc.this.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.all[count.index].id
  }

  tags = {
    Name = "spacelift-${var.aws_region}-private-${count.index}"
  }
}

resource "aws_route_table_association" "private" {
  count          = length(data.aws_availability_zones.all.names)
  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private[count.index].id
}
Enter fullscreen mode Exit fullscreen mode

That was it. If you have never worked with Terraform or AWS before, you now know you need deep knowledge of how the underlying VPC resources in AWS work to succeed in setting this up. 

You do not always have to build the full infrastructure from scratch. There is a large library of available modules you can use.

We could have replaced all the infrastructure defined above with the AWS VPC module. However, we still need to configure the module with a few inputs, such as the names of the availability zones we want to use and CIDR ranges for all public and private subnets.

locals {
  vpc_cidr = "10.0.0.0/16"
  azs      = length(data.aws_availability_zones.all.names)
  public_cidrs = [
    for i in range(local.azs) : cidrsubnet(local.vpc_cidr, 8, i)
  ]
  private_cidrs = [
    for i in range(local.azs) : cidrsubnet(local.vpc_cidr, 8, 128+i)
  ]
}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.13.0"

  cidr            = local.vpc_cidr
  azs             = data.aws_availability_zones.all.names
  private_subnets = local.private_cidrs
  public_subnets  = local.public_cidrs

  enable_nat_gateway = true

  tags = {
    Name = "spacelift"
  }
}
Enter fullscreen mode Exit fullscreen mode

To create the resources (either using the explicit resources we created above or using the VPC module) with Terraform, you must first initialize the configuration:

$ terraform init
Enter fullscreen mode Exit fullscreen mode

The initialization command downloads the required provider binaries. This is similar to downloading the required packages for an AWS CDK application. Next, you can run a plan operation to have Terraform tell you what will happen if you apply the configuration:

$ terraform plan
Enter fullscreen mode Exit fullscreen mode

Finally, you apply the configuration:

$ terraform apply
Enter fullscreen mode Exit fullscreen mode

The operation takes 1-2 minutes to complete.

Once you are satisfied with your testing, you can destroy the resources using the destroy command:

$ terraform destroy
Enter fullscreen mode Exit fullscreen mode

Although this is a small, simple infrastructure, we have learned a little about what makes up a Terraform configuration and how the Terraform workflow works.

We defined many relationships between different resources in the configuration. If we fail to express these relationships correctly, we might end up in a situation where Terraform tries to create a resource that depends on a different resource that has not been created yet.

Errors like these are not always apparent before you try to apply the configuration. Although AWS CDK is not immune to these types of problems, they are usually more easily avoided when you write the code in a certain order and build the infrastructure successively.

AWS CDK vs. Terraform table comparison

The following table provides a high-level overview of the technical differences between AWS CDK and Terraform.

Image description

Differences between AWS CDK and Terraform

Apart from the technical differences, there are a few other important areas to consider. The following subsections cover these.

1. Existing technical expertise

Your organization probably already has one or more areas of technical expertise. AWS CDK offers a smoother learning curve for developers familiar with its supported programming languages. If most of your applications are written using the .NET/C# ecosystem, or another language available in AWS CDK, then you will have an advantage when using the CDK in that same language.

Learning a new framework and language can be a challenge. This depends on your preexisting skills, the size of your organization, and the available budget for training. It is also a question of priority and balancing learning with daily work.

2. Infrastructure footprint

Your existing infrastructure footprint should also influence your decision.

Unless your organization is at a very early stage of the cloud journey, you probably have considerable existing infrastructure. Perhaps you have already used either AWS CDK or Terraform for much of your infrastructure.

This is a good position to be in because you will likely have had good or bad experiences with either framework. When starting a new project, you have the chance to reevaluate your previous choice and either try something new or expand your use of your chosen framework.

3. Developer experience

Since the DevOps movement has merged traditional developer tasks with traditional operations tasks, developers are no longer expected to only write application code. Developers coming from an application background will probably prefer the imperative approach using AWS CDK in their favorite language.

Modern IDEs generally provide far better support for traditional programming languages than for configuration languages such as HCL or JSON. This is especially true for statically typed languages such as Go. However, this can vary from IDE to IDE.

The surrounding ecosystem for traditional programming languages is usually far more evolved. If you need to do a lot of string manipulations, filesystem I/O operations, or any other complex data processing as part of your IaC, you will likely have better support and an easier time using the ecosystem for one of the languages in the AWS CDK.

4. Use-case

When it comes to use cases, there is one clear difference between the two tools:

  • AWS CDK targets the AWS ecosystem.
  • Terraform can target any system with an existing Terraform provider. You can also extend it to target custom systems by building your own providers.

This means that if your infrastructure spans multiple clouds, on-premise infrastructure, or third-party SaaS platforms, you will be more successful using Terraform. In this case, Terraform allows you to have a single workflow for all of your infrastructure, no matter what it is.

Even if your organization is using AWS as its sole cloud provider, chances are you are using other external systems that Terraform could work with. For example, you may be using a different DNS provider to Amazon Route53 or a different CDN solution to Amazon CloudFront.

However, if all or most of your infrastructure is on AWS, CDK could be a better choice. Even if this means you need a different tool to automate the remaining infrastructure.

Which is better, CDK or Terraform?

Both tools are powerful and effective for IaC, but AWS CDK is often the preferred choice for AWS-focused, developer-driven environments that prioritize advanced code reuse and flexibility within a single cloud environment. On the other hand, Terraform excels in supporting teams managing complex, multi-cloud infrastructures thanks to its mature ecosystem, extensive cloud compatibility, and state management features.

In the end, to make an informed decision, you need to consider all your application requirements, workforce skill sets, current cloud environment, technical requirements, license concerns, and more.

Note: New versions of Terraform are placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that expands on Terraform's existing concepts and offerings. It is a viable alternative to HashiCorp's Terraform, being forked from Terraform version 1.5.6.

Enhance your IaC workflow with Spacelift

Spacelift is an infrastructure orchestration platform that increases your infrastructure deployment speed without sacrificing control. With Spacelift, you can provision, configure, and govern with one or more automated workflows that orchestrate Terraform, OpenTofu, Terragrunt, Pulumi, CloudFormation, Ansible, and Kubernetes. 

You don't need to define all the prerequisite steps for installing and configuring the infrastructure tool you are using, nor the deployment and security steps, as they are all available in the default workflow.

With Spacelift, you get:

  • Policies (based on Open Policy Agent) -- You can control how many approvals you need for runs, what kind of resources you can create, and what kind of parameters these resources can have, and you can also control the behavior when a pull request is open or merged.
  • Multi-IaC workflows -- Combine Terraform with Kubernetes, Ansible, and other IaC tools such as OpenTofu, Pulumi, and CloudFormation, create dependencies among them, and share outputs.
  • Self-service infrastructure via Blueprints, or Spacelift's Kubernetes operator, enabling your developers to do what matters -- developing application code while not sacrificing control.
  • Integrations with any third-party tools -- You can integrate with your favorite third-party tools and even build policies for them. For example, see how to Integrate security tools in your workflows using Custom Inputs.
  • Creature comforts such as contexts (reusable containers for your environment variables, files, and hooks), and the ability to run arbitrary code.
  • Drift detection and optional remediation.

Spacelift was built with DevOps/platform engineers in mind, but it has become the go-to platform for software engineers because it allows them to increase their velocity with self-service infrastructure that implements all the organization's guardrails. It greatly enhances collaboration among engineers, offering them a central location to make infrastructure-related decisions.

If you want to learn more about what you can do with Spacelift, check out this article or book a demo with our engineering team to discuss your options in more detail. 

Key points

In short, AWS CDK lets you define AWS infrastructure with code in general-purpose languages, while Terraform uses HCL for declarative, cloud-agnostic provisioning, ideal for multi-cloud setups.

Written by Mattias Fjellström

💖 💪 🙅 🚩
spacelift_team
Spacelift team

Posted on November 25, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related