Embracing CDK: Use Case to Move from CloudFormation YAML to CDK?



This content originally appeared on DEV Community and was authored by Clariza Look

The Dilemma Every Engineering Team Faces

Picture this scenario: Your team has been successfully running AWS infrastructure for years using CloudFormation YAML templates. They work, they’re familiar, and everyone knows how to maintain them. Meanwhile, your newer projects are built with AWS CDK, complete with comprehensive unit testing, type safety, and modern development practices.

Sound familiar? You’re not alone. Many engineering teams find themselves at this crossroads, managing a hybrid infrastructure landscape where legacy CloudFormation templates coexist with modern CDK applications. The question isn’t whether both approaches work—they do. The question is whether this fragmentation is serving your team’s long-term interests.

Understanding the Players: CloudFormation vs CDK

Before diving into the migration story, let’s clarify what we’re talking about.

What is AWS CloudFormation?

AWS CloudFormation is Amazon’s native Infrastructure as Code (IaC) service that allows you to define your AWS resources using JSON or YAML templates. Think of it as a blueprint that tells AWS exactly what infrastructure to create.

Here’s a simple CloudFormation YAML example creating an S3 bucket:

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Simple S3 bucket example'

Resources:
  MyS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: my-example-bucket
      VersioningConfiguration:
        Status: Enabled
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true

Outputs:
  BucketName:
    Description: 'Name of the S3 bucket'
    Value: !Ref MyS3Bucket
    Export:
      Name: MyS3BucketName

CloudFormation Characteristics:

  • Declarative: You describe what you want, not how to create it
  • YAML/JSON syntax: Human-readable but can become verbose
  • AWS native: Built specifically for AWS resources
  • Template-based: Static files that AWS reads and executes

What is AWS CDK?

AWS CDK (Cloud Development Kit) is a newer approach that lets you define cloud infrastructure using familiar programming languages like TypeScript, Python, Java, or C#. Instead of writing YAML or JSON, you write actual code.

Here’s the same S3 bucket example using CDK (TypeScript):

import { Stack, StackProps } from 'aws-cdk-lib';
import { Bucket, BlockPublicAccess, BucketAccessControl } from 'aws-cdk-lib/aws-s3';
import { CfnOutput } from 'aws-cdk-lib';
import { Construct } from 'constructs';

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

    // Create S3 bucket with sensible defaults
    const myBucket = new Bucket(this, 'MyS3Bucket', {
      bucketName: 'my-example-bucket',
      versioned: true,
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL
    });

    // Export bucket name for other stacks
    new CfnOutput(this, 'BucketName', {
      value: myBucket.bucketName,
      exportName: 'MyS3BucketName',
      description: 'Name of the S3 bucket'
    });
  }
}

CDK Characteristics:

  • Programmatic: Uses actual programming languages with IDE support
  • Higher-level abstractions: Smart defaults and best practices built-in
  • Type safety: Compile-time error checking
  • Testable: Full unit testing capabilities like any other code
  • Generates CloudFormation: CDK compiles down to CloudFormation templates

The Key Differences at a Glance

Aspect CloudFormation YAML AWS CDK
Language YAML/JSON markup TypeScript, Python, Java, C#
IDE Support Basic syntax highlighting Full IntelliSense, autocomplete, refactoring
Testing Limited to template validation Full unit testing with mocking
Linting cfn-lint for template validation ESLint, Prettier, language-specific linters
Reusability Copy/paste templates Object-oriented inheritance and composition
Learning Curve Learn YAML syntax + AWS resources Use existing programming skills
Error Detection Runtime (during deployment) Compile-time + runtime

A More Complex Example: The Difference Becomes Clear

Let’s look at a more realistic example—creating an API Gateway that triggers a Lambda function and stores data in an S3 bucket. This shows how the complexity gap widens with real infrastructure.

CloudFormation YAML approach:

AWSTemplateFormatVersion: '2010-09-09'
Description: 'API Gateway with Lambda and S3'

Resources:
  # S3 Bucket for data storage
  DataBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: my-api-data-bucket
      VersioningConfiguration:
        Status: Enabled
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  # IAM Role for Lambda execution
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: S3Access
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                  - s3:DeleteObject
                Resource: !Sub '${DataBucket}/*'

  # Lambda Function
  ApiLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: api-handler
      Runtime: nodejs18.x
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: |
          exports.handler = async (event) => {
            return {
              statusCode: 200,
              body: JSON.stringify({ message: 'Hello from Lambda!' })
            };
          };
      Environment:
        Variables:
          BUCKET_NAME: !Ref DataBucket

  # Permission for API Gateway to invoke Lambda
  LambdaInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref ApiLambdaFunction
      Action: lambda:InvokeFunction
      Principal: apigateway.amazonaws.com
      SourceArn: !Sub '${ApiGateway}/*/*'

  # API Gateway REST API
  ApiGateway:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: MyAPI
      Description: API for Lambda integration
      EndpointConfiguration:
        Types:
          - REGIONAL

  # API Gateway Resource
  ApiResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref ApiGateway
      ParentId: !GetAtt ApiGateway.RootResourceId
      PathPart: data

  # API Gateway Method
  ApiMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref ApiGateway
      ResourceId: !Ref ApiResource
      HttpMethod: POST
      AuthorizationType: NONE
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiLambdaFunction.Arn}/invocations'

  # API Gateway Deployment
  ApiDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn:
      - ApiMethod
    Properties:
      RestApiId: !Ref ApiGateway
      StageName: prod

Outputs:
  ApiGatewayUrl:
    Description: API Gateway endpoint URL
    Value: !Sub 'https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/prod'
    Export:
      Name: ApiGatewayUrl

  BucketName:
    Description: S3 bucket name
    Value: !Ref DataBucket
    Export:
      Name: DataBucketName

  LambdaFunctionArn:
    Description: Lambda function ARN
    Value: !GetAtt ApiLambdaFunction.Arn
    Export:
      Name: LambdaFunctionArn

CDK approach (TypeScript):

import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';
import { RestApi, LambdaIntegration } from 'aws-cdk-lib/aws-apigateway';
import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda';
import { Bucket, BlockPublicAccess } from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

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

    // S3 Bucket for data storage
    const dataBucket = new Bucket(this, 'DataBucket', {
      bucketName: 'my-api-data-bucket',
      versioned: true,
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL
    });

    // Lambda Function
    const apiFunction = new Function(this, 'ApiLambdaFunction', {
      functionName: 'api-handler',
      runtime: Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: Code.fromInline(`
        exports.handler = async (event) => {
          return {
            statusCode: 200,
            body: JSON.stringify({ message: 'Hello from Lambda!' })
          };
        };
      `),
      environment: {
        BUCKET_NAME: dataBucket.bucketName
      }
    });

    // Grant Lambda permissions to access S3 bucket
    dataBucket.grantReadWrite(apiFunction);

    // API Gateway
    const api = new RestApi(this, 'ApiGateway', {
      restApiName: 'MyAPI',
      description: 'API for Lambda integration'
    });

    // Create API resource and method
    const dataResource = api.root.addResource('data');
    dataResource.addMethod('POST', new LambdaIntegration(apiFunction));

    // Outputs
    new CfnOutput(this, 'ApiGatewayUrl', {
      value: api.url,
      exportName: 'ApiGatewayUrl',
      description: 'API Gateway endpoint URL'
    });

    new CfnOutput(this, 'BucketName', {
      value: dataBucket.bucketName,
      exportName: 'DataBucketName',
      description: 'S3 bucket name'
    });

    new CfnOutput(this, 'LambdaFunctionArn', {
      value: apiFunction.functionArn,
      exportName: 'LambdaFunctionArn',
      description: 'Lambda function ARN'
    });
  }
}

The difference is striking:

  • CloudFormation: ~105 lines of YAML with manual IAM role creation, permissions, and resource linking
  • CDK: ~50 lines of TypeScript with automatic IAM role generation and permission management

The CDK version automatically creates the Lambda execution role, sets up the proper IAM policies for S3 access, configures API Gateway permissions, and handles the deployment stage. The dataBucket.grantReadWrite(apiFunction) line alone replaces 20+ lines of IAM policy configuration in CloudFormation.

The Hidden Costs of Infrastructure Fragmentation

Two Worlds, Double the Overhead

When your infrastructure spans both CloudFormation YAML and CDK, you’re essentially maintaining two different technology stacks:

  • Dual toolchains: Different testing frameworks, deployment pipelines, and development workflows
  • Split expertise: Team members specializing in either YAML or CDK, creating knowledge silos
  • Inconsistent practices: Different approaches to testing, monitoring, and troubleshooting
  • Onboarding complexity: New team members need to learn both approaches

The Testing Gap

Here’s where the pain really shows. Your CDK projects likely have:

  • Comprehensive unit tests validating resource configurations
  • Integration tests ensuring components work together
  • Automated validation in CI/CD pipelines
  • Rapid feedback loops for developers

Meanwhile, your CloudFormation YAML templates probably have:

  • Manual deployment processes
  • Limited or no automated testing
  • Post-deployment validation (if any)
  • Slower feedback cycles when issues arise

This isn’t just a technical debt issue—it’s a reliability and velocity problem.

The Strategic Case for CDK Migration

1. Standardization as a Force Multiplier

When every project follows the same patterns, magic happens:

Knowledge Transfer Accelerates: A developer who masters testing on one CDK project can immediately apply that knowledge to any other project. No context switching between YAML syntax and TypeScript/Python.

Best Practices Propagate: That clever testing pattern you developed for your API Gateway? It can be instantly applied across all projects, not just the CDK ones.

Tooling Investment Pays Off: Every improvement to your CDK testing framework, every CI/CD optimization, benefits your entire infrastructure portfolio.

2. Operational Excellence Through Consistency

Consider these operational benefits:

Unified Monitoring: All projects can leverage the same observability patterns, making it easier to understand system behavior across your entire infrastructure.

Consistent Debugging: When something goes wrong at 2 AM, you want your on-call engineer using familiar tools and approaches, regardless of which project is failing.

Predictable Maintenance: Security updates, dependency management, and refactoring become routine when you’re working with a single technology stack.

3. The Compound Effect of Testing and Code Quality

Here’s what comprehensive testing and linting really delivers:

// CDK with TypeScript linting and testing
test('API Gateway integrates correctly with Load Balancer', () => {
  const template = Template.fromStack(stack);

  template.hasResourceProperties('AWS::ApiGateway::VpcLink', {
    TargetArns: [{ Ref: 'NetworkLoadBalancer' }]
  });
});

// ESLint catches issues before deployment
const api = new RestApi(this, 'MyApi', {
  restApiName: 'my-api',  // ESLint enforces naming conventions
  description: 'API for Lambda integration'  // Required by custom linting rules
});

CloudFormation Linting:

# Basic template validation
cfn-lint template.yml
# Checks for syntax errors and basic AWS resource validation

CDK Linting & Quality Tools:

# TypeScript compilation catches type errors
npm run build

# ESLint for code quality and style
npm run lint

# Prettier for consistent formatting
npm run format

# Unit tests with coverage
npm run test

# CDK-specific validation
cdk synth  # Validates and generates CloudFormation

When you can validate your infrastructure changes before deployment with comprehensive linting, type checking, and testing, you move faster. When you move faster with confidence, you innovate more. When you innovate more, you deliver better outcomes for your users.

Making the Migration Case: A Real-World Example

Let’s walk through a practical scenario. Imagine you have a legacy CloudFormation template managing your API infrastructure. It’s deployed manually, tested manually, and updated carefully.

Now consider the CDK equivalent:

Before: CloudFormation YAML

# 200+ lines of YAML
# Manual testing
# No type safety
# Documentation in separate files
# Manual parameter management

After: CDK with Testing

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

    // Self-documenting, type-safe infrastructure
    const cluster = new ecs.Cluster(this, 'ApiCluster', {
      vpc: props.vpc,
      containerInsights: true
    });

    const loadBalancer = new elbv2.NetworkLoadBalancer(this, 'ApiNLB', {
      vpc: props.vpc,
      internetFacing: false
    });

    // Outputs that dependent stacks can safely import
    new CfnOutput(this, 'LoadBalancerDNS', {
      value: loadBalancer.loadBalancerDnsName,
      exportName: `${this.stackName}-LoadBalancerDNS`
    });
  }
}

The CDK version isn’t just more testable—it’s more readable, more maintainable, and less error-prone.

The Migration Strategy That Actually Works

Phase 1: Parallel Implementation (Weeks 1-2)

Create the CDK equivalent while keeping your existing CloudFormation template running. This isn’t about big-bang replacement—it’s about building confidence.

Phase 2: Test Everything (Week 3)

Implement the comprehensive testing that makes CDK valuable:

  • Unit tests for resource configuration
  • Integration tests for component interaction
  • End-to-end tests for user-facing functionality

Phase 3: Validate in Non-Production (Weeks 4-5)

Deploy the CDK version alongside your existing infrastructure in non-production environments. Compare outputs, validate behavior, and build team confidence.

Phase 4: Production Cutover (Week 6)

With comprehensive testing and non-production validation complete, the production migration becomes a low-risk operation.

ROI: When Does Migration Pay Off?

The mathematics are compelling:

Initial Investment: 4-6 weeks of development time
Ongoing Benefits:

  • 50% faster infrastructure changes (thanks to testing confidence)
  • 75% reduction in deployment-related incidents
  • 100% improvement in developer onboarding speed for infrastructure work
  • Infinite improvement in sleep quality for your on-call engineers

But the real ROI isn’t just in time saved—it’s in opportunities created. When your infrastructure is reliable and rapidly changeable, your team can focus on building features instead of fighting deployment issues.

The Counterarguments (And Why They Don’t Hold Up)

“Our CloudFormation templates work fine”: Yes, they work. But working and optimal are different things. Your flip phone worked fine too, but you probably upgraded for good reasons.

“Migration is risky”: True, but so is maintaining fragmented infrastructure. The gradual migration approach outlined above minimizes risk while maximizing learning.

“We don’t have time”: You don’t have time not to do this. Every day you spend maintaining two different infrastructure approaches is time you’re not spending on features that matter to your users.

The Bottom Line

Infrastructure standardization isn’t just about choosing between CloudFormation and CDK—it’s about choosing between fragmentation and focus. When your team can channel all their infrastructure expertise into a single, well-tested approach, everyone wins.

Your legacy CloudFormation templates served you well, but they don’t have to define your future. The question isn’t whether you can afford to migrate to CDK—it’s whether you can afford not to.

The path forward is clear: embrace standardization, invest in testing, and build infrastructure that accelerates your team rather than slowing it down. Your future self (and your on-call schedule) will thank you.

Ready to start your CDK migration journey? Begin with a single, non-critical CloudFormation template and apply the gradual migration strategy outlined above. Small steps, big wins.


This content originally appeared on DEV Community and was authored by Clariza Look