Search…

CloudFormation and CDK

In this series (10 parts)
  1. Introduction to Infrastructure as Code
  2. Terraform fundamentals
  3. Terraform state management
  4. Terraform modules
  5. Terraform in CI/CD
  6. Ansible fundamentals
  7. Ansible roles and best practices
  8. Packer for machine images
  9. CloudFormation and CDK
  10. Managing drift and compliance

Terraform is cloud-agnostic. CloudFormation is AWS-native. That distinction matters more than most people think. CloudFormation integrates directly with AWS services at a level no third-party tool can match. Stack events appear in the AWS console. Rollbacks happen automatically on failure. IAM policies can restrict who creates or updates which stacks. If your infrastructure lives entirely in AWS, CloudFormation deserves serious consideration.

The stack model

A CloudFormation stack is a collection of AWS resources managed as a single unit. You define resources in a template. CloudFormation reads the template, determines what needs to change, and applies changes in the correct order based on resource dependencies.

flowchart TD
  A[Template YAML/JSON] --> B[CloudFormation Service]
  B --> C{Stack exists?}
  C -->|No| D[Create stack]
  C -->|Yes| E[Create change set]
  E --> F[Review changes]
  F --> G[Execute change set]
  D --> H[Provision resources]
  G --> H
  H --> I[Stack complete]
  H -->|Failure| J[Automatic rollback]

CloudFormation manages resource lifecycles through stacks. Failed updates roll back automatically.

Creating a stack:

aws cloudformation create-stack \
  --stack-name my-vpc \
  --template-body file://vpc.yml \
  --parameters ParameterKey=Environment,ParameterValue=production

Deleting a stack removes every resource it created. This is powerful and dangerous in equal measure.

Template structure

A CloudFormation template has six sections. Only Resources is required.

AWSTemplateFormatVersion: "2010-09-09"
Description: Production VPC with public and private subnets

Parameters:
  Environment:
    Type: String
    Default: production
    AllowedValues:
      - production
      - staging
      - development

  VpcCidr:
    Type: String
    Default: "10.0.0.0/16"

Mappings:
  SubnetConfig:
    production:
      PublicA: "10.0.1.0/24"
      PublicB: "10.0.2.0/24"
      PrivateA: "10.0.10.0/24"
      PrivateB: "10.0.20.0/24"

Conditions:
  IsProduction: !Equals [!Ref Environment, production]

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub "${Environment}-vpc"

  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !FindInMap [SubnetConfig, !Ref Environment, PublicA]
      AvailabilityZone: !Select [0, !GetAZs ""]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${Environment}-public-a"

  PublicSubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !FindInMap [SubnetConfig, !Ref Environment, PublicB]
      AvailabilityZone: !Select [1, !GetAZs ""]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${Environment}-public-b"

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${Environment}-igw"

  GatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${Environment}-public-rt"

  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: GatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway

  NatGatewayEIP:
    Type: AWS::EC2::EIP
    Condition: IsProduction
    Properties:
      Domain: vpc

  NatGateway:
    Type: AWS::EC2::NatGateway
    Condition: IsProduction
    Properties:
      AllocationId: !GetAtt NatGatewayEIP.AllocationId
      SubnetId: !Ref PublicSubnetA

Outputs:
  VpcId:
    Description: VPC identifier
    Value: !Ref VPC
    Export:
      Name: !Sub "${Environment}-VpcId"

  PublicSubnets:
    Description: Public subnet IDs
    Value: !Join [",", [!Ref PublicSubnetA, !Ref PublicSubnetB]]
    Export:
      Name: !Sub "${Environment}-PublicSubnets"

Parameters accept input at deploy time. Mappings act as lookup tables. Conditions control whether resources are created. Outputs expose values for other stacks to consume via cross-stack references.

Change sets

Directly updating a stack is risky. Change sets let you preview what CloudFormation will do before it does anything.

# Create a change set
aws cloudformation create-change-set \
  --stack-name my-vpc \
  --template-body file://vpc.yml \
  --change-set-name add-private-subnets

# Review the change set
aws cloudformation describe-change-set \
  --stack-name my-vpc \
  --change-set-name add-private-subnets

# Execute if changes look correct
aws cloudformation execute-change-set \
  --stack-name my-vpc \
  --change-set-name add-private-subnets

The describe output shows which resources will be added, modified, or replaced. Replacement is the critical detail. Some property changes force CloudFormation to delete and recreate a resource. Changing a subnet CIDR, for example, requires replacement and will destroy anything running in the old subnet.

Stack drift

Drift occurs when someone modifies a resource outside of CloudFormation. An engineer manually changes a security group rule in the console. Now the actual state differs from the template state. CloudFormation does not know about the change until you check.

aws cloudformation detect-stack-drift --stack-name my-vpc

After detection completes:

aws cloudformation describe-stack-resource-drifts \
  --stack-name my-vpc \
  --stack-resource-drift-status-filters MODIFIED DELETED

The output shows exactly which properties drifted and what the expected versus actual values are. Fix drift by updating the template to match reality or by reapplying the template to overwrite manual changes.

flowchart LR
  A[Template state] --> B{Matches actual?}
  B -->|Yes| C[No drift]
  B -->|No| D[Drift detected]
  D --> E[Update template to match]
  D --> F[Reapply template to fix]

Drift detection compares template definitions against live resource configurations.

Enter CDK

CloudFormation templates are verbose. The VPC example above is over 80 lines and it only has two public subnets and partial private subnet support. The AWS Cloud Development Kit (CDK) lets you define the same infrastructure in a real programming language.

CDK is not a replacement for CloudFormation. It generates CloudFormation templates. You write TypeScript, Python, Java, or Go. CDK synthesizes that code into a CloudFormation template and deploys it.

Constructs

CDK organizes resources into constructs at three levels:

  • L1 (Cfn resources): Direct one-to-one mapping to CloudFormation resources. Prefixed with Cfn. No abstraction.
  • L2 (Curated constructs): Sensible defaults, convenience methods, and proper security configurations. The sweet spot for most use cases.
  • L3 (Patterns): High-level constructs that combine multiple resources into common architectures.
// cdk/lib/vpc-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';

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

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

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

    new cdk.CfnOutput(this, 'VpcId', {
      value: this.vpc.vpcId,
      exportName: 'ProductionVpcId',
    });
  }
}

These 30 lines of TypeScript produce a VPC with public subnets, private subnets, a NAT gateway, route tables, an internet gateway, and all the wiring between them. The L2 Vpc construct handles the details that took 80+ lines of raw CloudFormation.

A more complete CDK application

// cdk/lib/app-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns';
import { Construct } from 'constructs';

interface AppStackProps extends cdk.StackProps {
  vpc: ec2.Vpc;
}

export class AppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: AppStackProps) {
    super(scope, id, props);

    const cluster = new ecs.Cluster(this, 'Cluster', {
      vpc: props.vpc,
    });

    // L3 pattern: ALB + Fargate service in one construct
    new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'Service', {
      cluster,
      desiredCount: 2,
      taskImageOptions: {
        image: ecs.ContainerImage.fromRegistry('nginx:alpine'),
        containerPort: 80,
      },
      publicLoadBalancer: true,
    });
  }
}
// cdk/bin/app.ts
import * as cdk from 'aws-cdk-lib';
import { VpcStack } from '../lib/vpc-stack';
import { AppStack } from '../lib/app-stack';

const app = new cdk.App();

const vpcStack = new VpcStack(app, 'VpcStack', {
  env: { account: '123456789012', region: 'us-east-1' },
});

new AppStack(app, 'AppStack', {
  env: { account: '123456789012', region: 'us-east-1' },
  vpc: vpcStack.vpc,
});

Deploy with:

cdk synth    # generate CloudFormation templates
cdk diff     # preview changes (like a change set)
cdk deploy   # deploy all stacks

The cdk diff command is the CDK equivalent of a change set. Always run it before deploying.

CDK vs Terraform

Both tools define infrastructure as code. The differences are in philosophy and ecosystem.

AspectCDKTerraform
LanguageTypeScript, Python, Java, GoHCL
StateManaged by CloudFormationSelf-managed or Terraform Cloud
Cloud supportAWS onlyMulti-cloud
Abstraction levelL1/L2/L3 constructsModules
Drift detectionBuilt into CloudFormationterraform plan
RollbackAutomatic on failureManual
EcosystemAWS Construct LibraryTerraform Registry
Learning curveLower for developersLower for ops engineers

CDK wins when your team writes TypeScript daily and deploys exclusively to AWS. You get type checking, IDE autocomplete, unit tests with Jest, and the full power of a programming language for loops, conditions, and abstractions.

Terraform wins when you deploy to multiple clouds or need a tool that the broader DevOps community knows. HCL is simpler than TypeScript for engineers who do not write application code.

CDK L2/L3 constructs dramatically reduce boilerplate compared to raw CloudFormation.

CDK testing

CDK stacks are code, which means you can unit test them:

// test/vpc-stack.test.ts
import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { VpcStack } from '../lib/vpc-stack';

test('VPC is created with correct CIDR', () => {
  const app = new cdk.App();
  const stack = new VpcStack(app, 'TestVpcStack');
  const template = Template.fromStack(stack);

  template.resourceCountIs('AWS::EC2::VPC', 1);
  template.hasResourceProperties('AWS::EC2::VPC', {
    EnableDnsSupport: true,
    EnableDnsHostnames: true,
  });
});

test('NAT Gateway is created', () => {
  const app = new cdk.App();
  const stack = new VpcStack(app, 'TestVpcStack');
  const template = Template.fromStack(stack);

  template.resourceCountIs('AWS::EC2::NatGateway', 1);
});

Run tests with:

npx jest

This catches misconfiguration before anything touches AWS. No cloud credentials needed. No cost incurred.

What comes next

You now understand CloudFormation stacks, change sets, drift detection, and CDK constructs. The next article covers managing drift and compliance, where you will learn systematic approaches to detecting drift across your entire fleet, enforcing policy as code with OPA, and scanning your IaC templates for security issues before they reach production.

Start typing to search across all content
navigate Enter open Esc close