algonote(en)

There's More Than One Way To Do It

Yukkuri AWS CDK (TypeScript)

This is a hands-on guide for AWS CDK created with ChatGPT. It may contain errors.

🕹️ Chapter 1: Introduction to Infrastructure as Code and AWS CDK


Scene 1: What is IaC? Why Use It?

Reimu: Hey Marisa, have you ever manually clicked around the AWS Console to build your infrastructure?

Marisa: Of course! I’ve done it countless times — creating S3 buckets, configuring IAM roles… click, click, click! But it always feels like a trap. I forget which setting I used last time.

Reimu: Exactly. That’s why Infrastructure as Code (IaC) was born. Instead of manually configuring everything, we define infrastructure in code — like this:

// infra.ts
import * as cdk from 'aws-cdk-lib';
import { Bucket } from 'aws-cdk-lib/aws-s3';

export class SimpleInfraStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    new Bucket(this, 'MyFirstBucket', {
      versioned: true,
    });
  }
}

Marisa: Whoa, that’s TypeScript! So this single file can create an S3 bucket?

Reimu: Yup! When you run cdk deploy, it synthesizes this code into CloudFormation templates and deploys it. It’s version-controlled, reproducible, and way safer than manual clicks.


Scene 2: From CloudFormation → CDK v1 → CDK v2

Marisa: So, IaC has been around for a while, right? What did people use before CDK?

Reimu: They used AWS CloudFormation, which is declarative — you describe resources in YAML or JSON. For example:

Resources:
  MyBucket:
    Type: AWS::S3::Bucket
    Properties:
      VersioningConfiguration:
        Status: Enabled

Marisa: Ugh, YAML… hard to maintain when projects get big.

Reimu: Exactly. Then AWS CDK v1 appeared — it introduced imperative IaC using programming languages like TypeScript, Python, Java. But v1 had multiple package imports — @aws-cdk/aws-s3, @aws-cdk/aws-lambda, and so on.

Marisa: Yeah, that’s messy. What about CDK v2?

Reimu: CDK v2 consolidated everything into one library: aws-cdk-lib. Now you just import once — it’s simpler, stable, and backward-compatible.

import * as cdk from 'aws-cdk-lib';
import { Bucket } from 'aws-cdk-lib/aws-s3';

Marisa: Ah, clean and unified! So no more version mismatch chaos between libraries.


Scene 3: Benefits and Trade-Offs of CDK v2

Reimu: Let’s summarize the benefits of CDK v2.

Benefit Description
🧠 Code Reuse You can create reusable Constructs just like functions or classes.
🔄 Version Stability Unified aws-cdk-lib reduces dependency issues.
🧩 Integration with Modern Dev Tools TypeScript gives you type checking, IntelliSense, and tests.
🧮 Deterministic Deployments CDK still uses CloudFormation under the hood — safe and predictable.

Marisa: Sounds great. Any trade-offs?

Trade-off Explanation
⏱️ Synthesis Overhead CDK generates CloudFormation templates before deployment, so builds can be slower.
📜 Hidden Abstractions Sometimes, it’s not obvious what the generated template looks like.
🧩 Learning Curve You need to understand both AWS and programming constructs.

Reimu: Yup, so CDK v2 is best when you want scalable, maintainable IaC with full TypeScript power.


Scene 4: Hands-On — Deploy Your First Stack

Marisa: Okay, I want to try it! What are the steps?

Reimu: Let’s go step by step.

# 1. Install the AWS CDK CLI
npm install -g aws-cdk

# 2. Create a new project
mkdir cdk-intro && cd cdk-intro
cdk init app --language typescript

# 3. Install dependencies
npm install

# 4. Bootstrap your AWS account (only once)
cdk bootstrap

# 5. Add a simple S3 bucket to lib/cdk-intro-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Bucket } from 'aws-cdk-lib/aws-s3';

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

    new Bucket(this, 'DemoBucket', {
      versioned: true,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });
  }
}
# 6. Deploy it!
cdk deploy

Marisa: That’s all?! It’s so clean! And it will automatically create CloudFormation templates behind the scenes?

Reimu: Exactly. You can also check them with:

cdk synth

It outputs the JSON CloudFormation template that CDK generates.


Scene 5: Wrap-Up

Marisa: So, CDK v2 gives us the power of programming languages, with all AWS services in one package. It’s like coding your infrastructure instead of drawing diagrams.

Reimu: That’s the spirit! In the next chapter, we’ll set up the environment properly and explore project structure. But for now — you’ve already built your first IaC app with TypeScript and CDK v2!

Marisa: Sweet! Time to commit this to GitHub before I break something!


End of Chapter 1: You learned

  • The concept and value of Infrastructure as Code (IaC)
  • The evolution from CloudFormation → CDK v1 → CDK v2
  • The benefits and trade-offs of CDK v2
  • How to deploy your first TypeScript CDK app

🧩 Chapter 2: Installing and Configuring Your CDK Environment

— with Yukkuri Reimu & Yukkuri Marisa


Scene 1: Prerequisites — Setting Up the Basics

Reimu: Alright Marisa, before we dive into CDK, we need our environment ready. CDK v2 for TypeScript needs a few things first.

Marisa: Lemme guess — Node.js and TypeScript, right?

Reimu: Exactly! You’ll also need the AWS CLI for authentication and access. Let’s verify step by step.


🧱 1. Check Node.js and npm

node -v
npm -v

Reimu: You’ll need Node.js >= 16.x (ideally 18 or 20). If you don’t have it:

# macOS (using Homebrew)
brew install node

# Ubuntu / Debian
sudo apt install nodejs npm

🪄 2. Install TypeScript Globally

npm install -g typescript
tsc -v

Marisa: So this compiles our CDK TypeScript code into JavaScript, right?

Reimu: Exactly. CDK runs on Node, so everything gets transpiled behind the scenes.


🧭 3. Install and Configure AWS CLI

# Install AWS CLI v2
brew install awscli  # or see AWS docs for other OS

# Configure your credentials
aws configure

Then enter your keys:

AWS Access Key ID: <your-key-id>
AWS Secret Access Key: <your-secret-key>
Default region name: ap-northeast-1
Default output format: json

Marisa: So this writes to ~/.aws/credentials and ~/.aws/config?

Reimu: Right. CDK uses those automatically during deployment.


Scene 2: Installing the CDK CLI and Creating a Project

Marisa: Okay, environment ready! What’s next?

Reimu: Now install the AWS CDK CLI globally.

npm install -g aws-cdk
cdk --version

Marisa: Nice. Now let’s create a project!

mkdir cdk-hands-on
cd cdk-hands-on
cdk init app --language typescript

Reimu: That command scaffolds a full TypeScript project — including build scripts, CDK App, and Stack templates.


Scene 3: Understanding the Project Structure

Marisa: Let’s peek inside the new project.

tree -L 2

Output:

.
├── bin/
│   └── cdk-hands-on.ts
├── lib/
│   └── cdk-hands-on-stack.ts
├── node_modules/
├── cdk.json
├── package.json
├── tsconfig.json
└── README.md

Reimu: Here’s how it works:

Path Description
bin/ Entry point for your app (cdk.App)
lib/ Your infrastructure stacks (e.g., S3, Lambda, etc.)
cdk.json Config file that tells CDK how to run your app
tsconfig.json TypeScript compiler options
package.json Dependencies, scripts, build commands

Marisa: So bin/cdk-hands-on.ts creates the app, and lib/ defines what’s inside the stack?

Reimu: Exactly. Check this out:

// bin/cdk-hands-on.ts
import * as cdk from 'aws-cdk-lib';
import { CdkHandsOnStack } from '../lib/cdk-hands-on-stack';

const app = new cdk.App();
new CdkHandsOnStack(app, 'CdkHandsOnStack', {});

and

// lib/cdk-hands-on-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Bucket } from 'aws-cdk-lib/aws-s3';

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

    new Bucket(this, 'SampleBucket', {
      versioned: true,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });
  }
}

Marisa: Cool — I can already see the structure coming together.


Scene 4: Bootstrapping Your AWS Environment

Reimu: Before we deploy, AWS CDK needs a bootstrap stack — that’s an S3 bucket and IAM roles used internally by CDK.

cdk bootstrap

Output example:

 ⏳  Bootstrapping environment aws://123456789012/ap-northeast-1...
 ✅  Environment successfully bootstrapped!

Marisa: So it just creates the “CDKToolkit” stack in CloudFormation?

Reimu: Exactly. You only need to do it once per account/region.


Scene 5: Build and Deploy Commands

Marisa: Alright, time to deploy?

Reimu: Almost. Let’s build our TypeScript code first.

npm run build

This runs the tsc compiler and outputs .js files to dist/.

Now deploy:

cdk deploy

Reimu: The CDK will synthesize CloudFormation templates and deploy your stack. You can preview what it’ll create with:

cdk synth

Marisa: Let’s check if it works!

aws s3 ls

If you see your new bucket, it worked!


Scene 6: Clean Up

Reimu: When you’re done testing, don’t forget to destroy it to avoid charges.

cdk destroy

Marisa: So that removes the stack and all the AWS resources? Nice — IaC cleanup is just one command.


Scene 7: Recap

What we learned in this chapter:

Topic Key Command
Install Node.js & TypeScript brew install node, npm install -g typescript
Configure AWS CLI aws configure
Install CDK CLI npm install -g aws-cdk
Create a new project cdk init app --language typescript
Bootstrap environment cdk bootstrap
Build & deploy npm run build, cdk deploy
Clean up cdk destroy

Marisa: Nice — our environment’s ready for action! Next time, let’s dive into Apps, Stacks, and Constructs — the heart of CDK.

Reimu: Exactly. Now that the setup’s complete, the real IaC adventure begins!

⚙️ Chapter 3: CDK Fundamentals — Apps, Stacks, and Constructs

— with Yukkuri Reimu & Yukkuri Marisa


Scene 1: Understanding the Core Concepts

Reimu: Alright Marisa, it’s time to understand the core building blocks of AWS CDK: App → Stack → Construct.

Marisa: Yeah, I’ve seen those terms, but they sound kinda abstract. Can you explain them like… building a Lego castle?

Reimu: Perfect analogy! Think of CDK as a big box of Lego pieces.

  • The App is the whole castle project.
  • Each Stack is a section — like the tower or the gate.
  • Each Construct is an individual Lego piece — like a wall or window.

Let’s visualize it:

App
 ├─ Stack: NetworkStack
 │    ├─ Construct: VPC
 │    └─ Construct: Subnets
 └─ Stack: ComputeStack
      ├─ Construct: Lambda Function
      └─ Construct: API Gateway

Marisa: So App groups Stacks, and each Stack is made up of multiple Constructs. It’s hierarchical — like an object tree!

Reimu: Exactly! And that hierarchy is what CDK synthesizes into a CloudFormation template.


Scene 2: The aws-cdk-lib Single Package in CDK v2

Marisa: In CDK v1, I remember having to install a bunch of separate packages, like @aws-cdk/aws-s3, @aws-cdk/aws-lambda... Is that still the case?

Reimu: Nope! CDK v2 simplified everything. Now, you only need one single package:

npm install aws-cdk-lib constructs

Reimu: Then you can import everything from that unified library.

import * as cdk from 'aws-cdk-lib';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';

Marisa: Nice — so we don’t have to worry about version mismatches between packages anymore?

Reimu: Exactly. All AWS service modules are included and versioned together in aws-cdk-lib. This is one of the major advantages of CDK v2.


Scene 3: Hands-On — Writing Your First Simple Stack

Reimu: Let’s create our first real stack using both S3 and Lambda. We’ll start from a fresh project created in the previous chapter.

Marisa: So in lib/cdk-hands-on-stack.ts, we define our stack, right?

Reimu: Exactly! Let’s open it and replace the contents with this:

// lib/cdk-hands-on-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda';

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

    // 1. Create an S3 bucket
    const bucket = new Bucket(this, 'MyDemoBucket', {
      versioned: true,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // 2. Create a Lambda function
    const lambda = new Function(this, 'MyDemoLambda', {
      runtime: Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: Code.fromInline(`
        exports.handler = async function(event) {
          console.log("Lambda triggered!", event);
          return { statusCode: 200, body: "Hello from Lambda!" };
        };
      `),
      environment: {
        BUCKET_NAME: bucket.bucketName,
      },
    });

    // 3. Grant Lambda permission to write to S3
    bucket.grantWrite(lambda);
  }
}

Scene 4: Linking It All Together in the App Entry Point

Marisa: Okay, but where does this stack get created?

Reimu: Good question! That happens in the entry point file under bin/.

// bin/cdk-hands-on.ts
import * as cdk from 'aws-cdk-lib';
import { CdkHandsOnStack } from '../lib/cdk-hands-on-stack';

const app = new cdk.App();
new CdkHandsOnStack(app, 'CdkHandsOnStack', {
  env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
});

Marisa: So this app is like the main() function — it creates our stack instance!

Reimu: Exactly. When you run cdk deploy, it executes this file and builds the dependency tree.


Scene 5: Synthesizing and Deploying

Reimu: Now that we have our stack, let’s test it out.

npm run build
cdk synth

This command outputs a CloudFormation template based on your Constructs.

Example output (simplified):

{
  "Resources": {
    "MyDemoBucket": { "Type": "AWS::S3::Bucket" },
    "MyDemoLambda": { "Type": "AWS::Lambda::Function" }
  }
}

Marisa: So CDK auto-generates all the JSON I don’t want to write manually. Sweet!

Reimu: Exactly. And now we deploy it for real:

cdk deploy

You’ll see output like this:

✨  Deploying stack CdkHandsOnStack
✅  CdkHandsOnStack deployed successfully
Outputs:
CdkHandsOnStack.MyDemoBucketName = cdkhandsonstack-mydemobucket1234

Marisa: Awesome! So the Lambda and S3 bucket are live in AWS now?

Reimu: Yep — all created and connected via IaC code. You can confirm with:

aws s3 ls
aws lambda list-functions

Scene 6: Visualizing the App → Stack → Construct Hierarchy

Reimu: Let’s visualize what we’ve just built:

graph TD
  A[App: cdk.App()] --> B[Stack: CdkHandsOnStack]
  B --> C1[Construct: S3 Bucket]
  B --> C2[Construct: Lambda Function]
  C1 --> C3[Permission: grantWrite()]

Marisa: That makes it clear! The App owns the Stack, and the Stack owns multiple Constructs — it’s just nested objects.

Reimu: Exactly. This mental model helps you reason about scope — every Construct belongs to a parent.


Scene 7: Extra Tip — Naming and Scopes

Reimu: Every Construct in CDK takes (scope, id, props) as arguments. For example:

new Bucket(this, 'MyBucket', { versioned: true });
  • this = parent construct (scope)
  • 'MyBucket' = unique logical ID within that scope
  • { ... } = properties

Marisa: So if I have nested constructs, the logical ID becomes something like ParentStack/MyBucket/Resource?

Reimu: Exactly. CDK automatically generates logical IDs for CloudFormation, but you can override them if needed.


Scene 8: Clean-Up

Reimu: When you’re done experimenting, clean up to avoid extra AWS costs.

cdk destroy

Output:

Are you sure you want to delete: CdkHandsOnStack (y/n)? y
✅  CdkHandsOnStack: destroyed

Marisa: One command cleanup — I love IaC even more now!


Scene 9: Recap

You’ve Learned in Chapter 3

Concept Description
App The root of your CDK application (entry point)
Stack A deployable unit — translates to a CloudFormation stack
Construct A building block that defines one or more AWS resources
aws-cdk-lib The single unified package for all AWS services
Scope/ID/Props The three key parameters for every Construct
S3 + Lambda Example Your first real IaC with CDK v2 and TypeScript

Marisa: So far we’ve set up our environment and built our first working stack. What’s next, Reimu?

Reimu: Next chapter, we’ll go deeper — how to structure TypeScript projects and use Construct patterns effectively! Prepare for Chapter 4: TypeScript Best Practices for CDK Projects.

Marisa: I’m ready! Bring on more code blocks — I’m starting to love this magic. 🪄

💡 Chapter 4: TypeScript Best Practices for CDK Projects

— with Yukkuri Reimu & Yukkuri Marisa


Scene 1: Project Layout and Folder Structure

Reimu: Now that we understand Apps, Stacks, and Constructs, it’s time to make our CDK project cleaner and scalable. Many beginners throw everything into one file, but… that’s a nightmare later.

Marisa: Heh, guilty as charged. My lib/ folder looks like spaghetti. What’s the “best practice” structure?

Reimu: Let’s start with a clean modular layout.

cdk-hands-on/
├── bin/
│   └── cdk-hands-on.ts          # Entry point (creates the App)
├── lib/
│   ├── stacks/
│   │   ├── storage-stack.ts     # Example: S3, DynamoDB
│   │   ├── compute-stack.ts     # Example: Lambda, ECS
│   │   └── network-stack.ts     # Example: VPC, Subnets
│   ├── constructs/
│   │   ├── lambda-with-s3.ts    # Custom reusable construct
│   │   └── static-site.ts
│   └── utils/
│       └── iam-helper.ts        # Helper for IAM policies
├── package.json
├── tsconfig.json
└── cdk.json

Marisa: So we split stacks by domain, and reusable pieces go into constructs/ — I like that. Why have utils/ though?

Reimu: To hold helper functions or constants that are not AWS resources, like IAM policy builders or naming utilities.


Scene 2: Naming Conventions

Reimu: Naming is also important. Follow PascalCase for classes and kebab-case for files.

Item Convention Example
Stack class PascalCase StorageStack
Construct class PascalCase LambdaWithS3
File name kebab-case lambda-with-s3.ts
ID string PascalCase or simple words "MyLambda"

Marisa: Got it. So the folder name says what, and the class name says who.


Scene 3: Using Types, Interfaces, and Props

Reimu: In TypeScript, always define interfaces for props when you create reusable Constructs. Let’s build an example: a custom construct combining Lambda + S3.

// lib/constructs/lambda-with-s3.ts
import { Construct } from 'constructs';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda';

export interface LambdaWithS3Props {
  readonly bucketName?: string;
  readonly functionName?: string;
}

export class LambdaWithS3 extends Construct {
  public readonly bucket: Bucket;
  public readonly lambda: Function;

  constructor(scope: Construct, id: string, props: LambdaWithS3Props = {}) {
    super(scope, id);

    this.bucket = new Bucket(this, 'Bucket', {
      bucketName: props.bucketName,
      versioned: true,
    });

    this.lambda = new Function(this, 'Lambda', {
      runtime: Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: Code.fromInline(`
        exports.handler = async function(event) {
          console.log("Hello from Lambda!");
        };
      `),
      functionName: props.functionName,
      environment: {
        BUCKET_NAME: this.bucket.bucketName,
      },
    });

    this.bucket.grantReadWrite(this.lambda);
  }
}

Marisa: Nice! So LambdaWithS3Props lets me customize each instance with different bucket names.

Reimu: Exactly — that’s how we make constructs reusable and type-safe.

Now, use it in a stack:

// lib/stacks/compute-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { LambdaWithS3 } from '../constructs/lambda-with-s3';

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

    new LambdaWithS3(this, 'AppHandler', {
      bucketName: 'demo-app-bucket',
      functionName: 'DemoLambdaHandler',
    });
  }
}

Scene 4: Modularizing and Reusing Constructs

Reimu: Here’s a trick: If you modularize Constructs like that, you can reuse them across different stacks or even publish them as npm packages.

Marisa: So I could have a “LambdaWithS3” construct in multiple apps? Like a library of my company’s infrastructure patterns?

Reimu: Exactly. That’s called a higher-level construct (L3). It encapsulates AWS best practices and common configurations.

You can even make a simple constructs/index.ts to re-export them all:

// lib/constructs/index.ts
export * from './lambda-with-s3';
export * from './static-site';

Then import it cleanly:

import { LambdaWithS3 } from '../constructs';

Scene 5: CDK Idioms in TypeScript

Marisa: Are there any idiomatic TypeScript tricks specific to CDK?

Reimu: Definitely! Here are some must-know patterns.

✅ Use readonly for public properties

It ensures immutability after creation.

public readonly lambda: Function;

✅ Prefer const + arrow functions

const bucket = new Bucket(this, 'DataBucket', {
  versioned: true,
});

✅ Use enums for fixed values

export enum EnvType {
  DEV = 'dev',
  PROD = 'prod',
}

✅ Define context-aware logic

You can use CDK context (from cdk.json) to switch configs:

{
  "context": {
    "env": "dev"
  }
}

Then use it in code:

const envType = this.node.tryGetContext('env');
if (envType === 'prod') {
  new Bucket(this, 'ProdBucket', { versioned: true });
}

✅ Avoid hard-coded ARNs or regions

Use dynamic references:

const region = cdk.Stack.of(this).region;
const account = cdk.Stack.of(this).account;

Marisa: Those small things make the code look clean and safer for real deployments.


Scene 6: Common Mistakes to Avoid

❌ Anti-Pattern ✅ Best Practice
Writing everything in one file Split into stacks and constructs
Using any for props Define interface for type safety
Duplicating logic Extract helper functions or constructs
Hardcoding values Use context or environment variables
Ignoring naming conventions Follow PascalCase / kebab-case consistency

Reimu: If you follow these rules, your project stays maintainable even when it grows to dozens of stacks.


Scene 7: Summary

You Learned in Chapter 4

Topic Key Takeaways
Layout Organize by domain: stacks/, constructs/, utils/
Naming Class = PascalCase, File = kebab-case
Types & Props Use interfaces for reusable constructs
Modularization Build L3 constructs and export via index.ts
TypeScript Idioms Use readonly, context, enums, and clean imports

Marisa: I see — CDK isn’t just about writing infra code. It’s also about structuring it like a software project!

Reimu: Exactly. That’s what makes CDK special: you apply software engineering principles to infrastructure.

Marisa: Nice! What’s next?

Reimu: Next time, we’ll explore AWS resources hands-on — starting with S3, DynamoDB, and RDS in Chapter 5: Storage & Data. Get ready for more TypeScript and AWS magic!

🗄️ Chapter 5: Storage & Data — S3, DynamoDB, and RDS

— with Yukkuri Reimu & Yukkuri Marisa


Scene 1: The Storage Trio of AWS

Reimu: Today, Marisa, we’re diving into AWS storage services — the holy trinity: S3, DynamoDB, and RDS.

Marisa: Ah, the classic three! S3 for objects, DynamoDB for NoSQL, and RDS for good old SQL.

Reimu: Exactly! We’ll create each using CDK v2, with production-ready settings — versioning, encryption, indexes, and all that jazz.


Part 1 — 🪣 S3 Buckets with Lifecycle, Versioning, and Encryption

Scene 2: Creating a Secure, Versioned S3 Bucket

Reimu: Let’s start simple — an S3 bucket with versioning and encryption.

// lib/stacks/storage-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Bucket, BucketEncryption, LifecycleRule } from 'aws-cdk-lib/aws-s3';

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

    const bucket = new Bucket(this, 'DataArchiveBucket', {
      versioned: true,
      encryption: BucketEncryption.S3_MANAGED,
      lifecycleRules: [
        {
          id: 'MoveOldFilesToIA',
          transitions: [{ storageClass: 'STANDARD_IA', transitionAfter: cdk.Duration.days(30) }],
        },
        {
          id: 'ExpireAfter1Year',
          expiration: cdk.Duration.days(365),
        },
      ],
      blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    new cdk.CfnOutput(this, 'BucketName', { value: bucket.bucketName });
  }
}

Marisa: So this bucket is versioned, encrypted, and automatically moves old files to Infrequent Access after 30 days? Pretty neat!

Reimu: Yep. This setup is secure and cost-efficient — perfect for logs or data archives.


Scene 3: Uploading Files to S3 from CLI

Reimu: After deploying:

cdk deploy

You can upload a test file:

aws s3 cp ./data.txt s3://<BucketName>/data.txt

and check versioning:

aws s3api list-object-versions --bucket <BucketName>

Marisa: I love that we can test everything right from the CLI.


Part 2 — ⚡ DynamoDB: Tables, Indexes, and Throughput

Scene 4: Creating a DynamoDB Table

Reimu: Next up — DynamoDB! Let’s define a table with a partition key, sort key, and Global Secondary Index (GSI).

// lib/stacks/database-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { AttributeType, Table, BillingMode, ProjectionType } from 'aws-cdk-lib/aws-dynamodb';

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

    const table = new Table(this, 'UserTable', {
      partitionKey: { name: 'userId', type: AttributeType.STRING },
      sortKey: { name: 'createdAt', type: AttributeType.STRING },
      billingMode: BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      tableName: 'UserTable',
    });

    // Add a Global Secondary Index
    table.addGlobalSecondaryIndex({
      indexName: 'emailIndex',
      partitionKey: { name: 'email', type: AttributeType.STRING },
      projectionType: ProjectionType.ALL,
    });

    new cdk.CfnOutput(this, 'TableName', { value: table.tableName });
  }
}

Marisa: So this table uses on-demand billing — no need to set read/write capacity manually?

Reimu: Exactly. It scales automatically based on usage. And the emailIndex lets you query users by email efficiently.


Scene 5: Accessing DynamoDB from Lambda

Reimu: Let’s connect a Lambda function that reads data from DynamoDB.

// lib/constructs/lambda-with-dynamo.ts
import { Construct } from 'constructs';
import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda';
import { Table } from 'aws-cdk-lib/aws-dynamodb';

export interface LambdaWithDynamoProps {
  readonly table: Table;
}

export class LambdaWithDynamo extends Construct {
  constructor(scope: Construct, id: string, props: LambdaWithDynamoProps) {
    super(scope, id);

    const lambda = new Function(this, 'ReadUserLambda', {
      runtime: Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: Code.fromInline(`
        const AWS = require('aws-sdk');
        const db = new AWS.DynamoDB.DocumentClient();
        exports.handler = async (event) => {
          const result = await db.scan({ TableName: process.env.TABLE_NAME }).promise();
          return { statusCode: 200, body: JSON.stringify(result.Items) };
        };
      `),
      environment: {
        TABLE_NAME: props.table.tableName,
      },
    });

    props.table.grantReadData(lambda);
  }
}

Marisa: So the Lambda gets permission automatically with grantReadData? No manual IAM policy needed?

Reimu: Exactly. That’s one of CDK’s superpowers — permissions are inferred and safely attached for you.


Part 3 — 🧮 RDS: Relational Databases in CDK

Scene 6: Creating an RDS Instance

Reimu: Now for something more traditional — an RDS MySQL database.

// lib/stacks/rds-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { DatabaseInstance, DatabaseInstanceEngine, StorageType, Credentials } from 'aws-cdk-lib/aws-rds';
import { Vpc } from 'aws-cdk-lib/aws-ec2';

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

    const vpc = new Vpc(this, 'RdsVpc', { maxAzs: 2 });

    const db = new DatabaseInstance(this, 'MyRdsInstance', {
      engine: DatabaseInstanceEngine.mysql({ version: { majorVersion: '8.0' } }),
      vpc,
      instanceType: new cdk.aws_ec2.InstanceType('t3.micro'),
      allocatedStorage: 20,
      storageType: StorageType.GP2,
      credentials: Credentials.fromGeneratedSecret('admin'),
      databaseName: 'appdb',
      multiAz: false,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      deletionProtection: false,
      publiclyAccessible: false,
    });

    new cdk.CfnOutput(this, 'RdsEndpoint', { value: db.instanceEndpoint.hostname });
  }
}

Marisa: This automatically creates the subnet groups, security groups, and secrets in Secrets Manager?

Reimu: Exactly. CDK handles all the glue for you. You can retrieve the generated credentials like this:

aws secretsmanager get-secret-value --secret-id MyRdsInstanceSecret

Scene 7: Using Aurora Serverless Instead (Optional)

Reimu: Want something that scales automatically? Replace RDS with Aurora Serverless v2.

import { DatabaseClusterEngine, ServerlessCluster } from 'aws-cdk-lib/aws-rds';

const cluster = new ServerlessCluster(this, 'AuroraCluster', {
  engine: DatabaseClusterEngine.AURORA_MYSQL,
  defaultDatabaseName: 'serverlessdb',
  vpc,
  scaling: { autoPause: cdk.Duration.minutes(10), minCapacity: 2, maxCapacity: 8 },
});

Marisa: Ooh, less management overhead — that’s perfect for development or variable workloads!


Part 4 — 🔗 Connecting Resources Together

Scene 8: Lambda Reading from DynamoDB and Writing to S3

Reimu: Let’s combine everything into one use case: A Lambda that reads from DynamoDB and writes the data to S3.

// lib/stacks/data-pipeline-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Table } from 'aws-cdk-lib/aws-dynamodb';
import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda';

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

    const bucket = new Bucket(this, 'PipelineOutputBucket', { versioned: true });
    const table = new Table(this, 'PipelineTable', {
      partitionKey: { name: 'id', type: cdk.aws_dynamodb.AttributeType.STRING },
      billingMode: cdk.aws_dynamodb.BillingMode.PAY_PER_REQUEST,
    });

    const lambda = new Function(this, 'PipelineLambda', {
      runtime: Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: Code.fromInline(`
        const AWS = require('aws-sdk');
        const s3 = new AWS.S3();
        const db = new AWS.DynamoDB.DocumentClient();

        exports.handler = async () => {
          const data = await db.scan({ TableName: process.env.TABLE_NAME }).promise();
          const body = JSON.stringify(data.Items);
          await s3.putObject({ Bucket: process.env.BUCKET_NAME, Key: 'dump.json', Body: body }).promise();
          return { statusCode: 200, body: 'Data exported to S3!' };
        };
      `),
      environment: {
        BUCKET_NAME: bucket.bucketName,
        TABLE_NAME: table.tableName,
      },
    });

    bucket.grantWrite(lambda);
    table.grantReadData(lambda);
  }
}

Marisa: Wow, that’s a full data pipeline in less than 100 lines!

Reimu: Yup — IaC magic. Deploy it, insert some DynamoDB items, then run the Lambda and check S3 for dump.json.


Scene 9: Recap

You’ve Learned in Chapter 5

Topic Key Skill
S3 Buckets Lifecycle rules, encryption, versioning
DynamoDB GSIs, on-demand billing, Lambda integration
RDS / Aurora SQL database creation with credentials & VPC
Cross-Resource Access Granting permissions via CDK methods (grantReadWrite, grantReadData)
Data Pipeline Lambda → DynamoDB → S3 integration

Marisa: So now I can design both NoSQL and SQL layers, plus data pipelines — all from CDK! It feels like backend and infrastructure are finally one world.

Reimu: Exactly! Next time, we’ll tackle Compute & Serverless — Lambda, API Gateway, and even EKS. You’ll see how to expose these data layers to the outside world.

Marisa: Let’s go! More TypeScript, more AWS spells! ⚡

⚙️ Chapter 6: Compute & Serverless — Lambda, API Gateway, and EKS

— with Yukkuri Reimu & Yukkuri Marisa


Scene 1: The Compute Layer in the Cloud

Reimu: Alright, Marisa, we’ve handled storage and data. Now it’s time for compute — the brains of the system!

Marisa: Finally! So today we’ll learn about Lambda, API Gateway, and even EKS?

Reimu: Exactly! We’ll start with pure serverless, then move to containers. Let’s begin with the simplest compute building block: AWS Lambda.


Part 1 — 🪄 AWS Lambda with CDK

Scene 2: Creating a Lambda Function

Reimu: Here’s a minimal Lambda defined in CDK TypeScript.

// lib/stacks/compute-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda';

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

    const helloLambda = new Function(this, 'HelloLambda', {
      runtime: Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: Code.fromInline(`
        exports.handler = async function(event) {
          console.log("Event:", JSON.stringify(event));
          return { statusCode: 200, body: "Hello from Lambda!" };
        };
      `),
    });

    new cdk.CfnOutput(this, 'LambdaName', { value: helloLambda.functionName });
  }
}

Marisa: That’s so short! So when I run cdk deploy, CDK automatically creates the Lambda function?

Reimu: Exactly. It uploads the inline code as a ZIP to S3 behind the scenes.

You can invoke it directly from the CLI:

aws lambda invoke --function-name HelloLambda out.json
cat out.json

Output:

Hello from Lambda!

Scene 3: Using External Source Files Instead of Inline Code

Reimu: For bigger functions, you can use external source files instead of inline code.

lambda/
└── app.js
// lambda/app.js
exports.handler = async (event) => {
  return { statusCode: 200, body: JSON.stringify({ message: "Hello from external file!" }) };
};

Then update CDK:

const lambda = new Function(this, 'ExternalLambda', {
  runtime: Runtime.NODEJS_18_X,
  handler: 'app.handler',
  code: Code.fromAsset('lambda'),
});

Marisa: Got it! This makes it easy to maintain larger logic.


Part 2 — 🌐 API Gateway Integration

Scene 4: Exposing Lambda via API Gateway

Reimu: Let’s expose our Lambda as a REST API endpoint.

import { LambdaRestApi } from 'aws-cdk-lib/aws-apigateway';

const api = new LambdaRestApi(this, 'HelloApi', {
  handler: helloLambda,
  proxy: false,
});

const hello = api.root.addResource('hello');
hello.addMethod('GET');

Marisa: So this gives me an HTTP endpoint like: https://xxxx.execute-api.ap-northeast-1.amazonaws.com/prod/hello ?

Reimu: Exactly. Now run:

cdk deploy
curl https://xxxx.execute-api.ap-northeast-1.amazonaws.com/prod/hello

Output:

Hello from Lambda!

Marisa: That’s ridiculously easy. No need to manually connect Lambda + API Gateway!


Scene 5: HTTP API (Simpler & Faster Alternative)

Reimu: If you don’t need full REST features, CDK v2 also supports HTTP API — cheaper and faster.

import { HttpApi, HttpMethod } from '@aws-cdk/aws-apigatewayv2-alpha';
import { HttpLambdaIntegration } from '@aws-cdk/aws-apigatewayv2-integrations-alpha';

const httpApi = new HttpApi(this, 'SimpleApi');
const integration = new HttpLambdaIntegration('LambdaIntegration', helloLambda);

httpApi.addRoutes({
  path: '/hello',
  methods: [HttpMethod.GET],
  integration,
});

Marisa: So HTTP API is lightweight, good for simple JSON APIs?

Reimu: Exactly — it’s perfect for microservices or serverless backends.


Part 3 — 🐳 Container Workloads with ECS Fargate or EKS

Scene 6: Running Containers with ECS Fargate

Reimu: Now, let’s move from pure serverless to containers. Fargate lets you run containers without managing EC2 instances.

// lib/stacks/fargate-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Vpc, Cluster } from 'aws-cdk-lib/aws-ecs';
import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns';

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

    const vpc = new Vpc(this, 'FargateVpc', { maxAzs: 2 });
    const cluster = new Cluster(this, 'FargateCluster', { vpc });

    new ApplicationLoadBalancedFargateService(this, 'WebService', {
      cluster,
      cpu: 256,
      memoryLimitMiB: 512,
      desiredCount: 1,
      taskImageOptions: {
        image: cdk.aws_ecs.ContainerImage.fromRegistry('nginx'),
        containerPort: 80,
      },
      publicLoadBalancer: true,
    });
  }
}

Marisa: Whoa — one stack, and I get a full web service with load balancer and container?

Reimu: Yup. CDK patterns make ECS Fargate feel as easy as deploying Lambda.

Try this:

cdk deploy

Then check the output URL — it’ll show NGINX running in Fargate.


Scene 7: Deploying a Container to EKS (Kubernetes)

Reimu: Now for the big one: EKS (Elastic Kubernetes Service). This lets you manage Kubernetes clusters with IaC.

// lib/stacks/eks-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as eks from 'aws-cdk-lib/aws-eks';
import { Vpc } from 'aws-cdk-lib/aws-ec2';

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

    const vpc = new Vpc(this, 'EksVpc', { maxAzs: 2 });

    const cluster = new eks.Cluster(this, 'MyEksCluster', {
      version: eks.KubernetesVersion.V1_29,
      vpc,
      defaultCapacity: 2,
    });

    cluster.addManifest('hello-k8s', {
      apiVersion: 'v1',
      kind: 'Pod',
      metadata: { name: 'hello' },
      spec: {
        containers: [{ name: 'hello', image: 'nginx', ports: [{ containerPort: 80 }] }],
      },
    });
  }
}

Marisa: Wait — CDK can create and configure Kubernetes clusters too?!

Reimu: Exactly. It’s all just Constructs under the hood! After deploying, you can connect with:

aws eks update-kubeconfig --name MyEksCluster
kubectl get pods

Part 4 — ☁️ Architecture Overview

Reimu: Let’s visualize what we’ve built so far:

graph TD
  A[API Gateway] --> B[Lambda]
  B --> C[S3 / DynamoDB / RDS]
  D[Fargate Service] --> E[ALB]
  F[EKS Cluster] --> G[Kubernetes Pods]

Marisa: So now we’ve covered both serverless compute and container-based workloads! CDK really is a one-stop shop for all AWS compute.


Part 5 — 🧰 Bonus: Connecting Lambda to Data Layer

Reimu: Remember our DynamoDB from Chapter 5? Here’s how we connect it to the Lambda from this chapter.

import { Table } from 'aws-cdk-lib/aws-dynamodb';

const lambda = new Function(this, 'ReadUserLambda', {
  runtime: Runtime.NODEJS_18_X,
  handler: 'index.handler',
  code: Code.fromInline(`
    const AWS = require('aws-sdk');
    const db = new AWS.DynamoDB.DocumentClient();
    exports.handler = async () => {
      const result = await db.scan({ TableName: process.env.TABLE }).promise();
      return { statusCode: 200, body: JSON.stringify(result.Items) };
    };
  `),
  environment: { TABLE: userTable.tableName },
});

userTable.grantReadData(lambda);

Marisa: That’s clean — Lambda reads from DynamoDB, and API Gateway exposes it publicly. Full backend in ~40 lines!


Scene 8: Clean-Up

cdk destroy

Marisa: That destroys all resources — Lambdas, APIs, clusters, everything?

Reimu: Exactly. IaC makes cleanup just as easy as deployment.


Scene 9: Recap

You Learned in Chapter 6

Topic Highlights
Lambda Functions Inline or asset-based code, automatic IAM
API Gateway REST and HTTP API integrations
ECS Fargate Serverless containers with ALB
EKS (Kubernetes) Managed clusters as CDK resources
Integration Patterns Connect compute with data layers
Mermaid Diagrams Visualizing architectures in IaC docs

Marisa: Wow, CDK really makes compute orchestration simple — from Lambda to Kubernetes!

Reimu: Yup. In the next chapter, we’ll explore Networking & Security — VPCs, subnets, Security Groups, and IAM policies. That’s where the real infrastructure design begins!

Marisa: Can’t wait! More diagrams, more CDK code, more cloud magic! ☁️⚡

🌐 Chapter 7: Networking & Security — VPCs, Subnets, Security Groups, and IAM

— with Yukkuri Reimu & Yukkuri Marisa


Scene 1: The Foundation of All Infrastructure

Reimu: Hey Marisa, before we add more compute or databases, we need to talk about something essential — networking.

Marisa: You mean the VPC world — subnets, NAT, and all those mysterious CIDRs?

Reimu: Exactly. Everything in AWS lives inside a VPC (Virtual Private Cloud). And CDK makes building one both powerful and simple.


Part 1 — 🏗️ Defining a VPC with Public & Private Subnets

Scene 2: Creating a Basic VPC

// lib/stacks/network-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Vpc } from 'aws-cdk-lib/aws-ec2';

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

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

    this.vpc = new Vpc(this, 'MainVpc', {
      maxAzs: 2,
      natGateways: 1,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'public-subnet',
          subnetType: cdk.aws_ec2.SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'private-subnet',
          subnetType: cdk.aws_ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
      ],
    });

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

Marisa: So this VPC has two subnets per Availability Zone — one public, one private — and a single NAT gateway?

Reimu: Exactly. maxAzs: 2 automatically spreads subnets across two AZs for redundancy.

You can visualize it like this:

VPC (10.0.0.0/16)
├─ Public Subnet A (10.0.0.0/24)
│   └─ NAT Gateway
├─ Private Subnet A (10.0.1.0/24)
├─ Public Subnet B (10.0.2.0/24)
├─ Private Subnet B (10.0.3.0/24)

Scene 3: Exporting the VPC for Other Stacks

Reimu: To reuse this VPC across multiple stacks (like Lambda or RDS), we export it:

// In app entry point (bin/app.ts)
const network = new NetworkStack(app, 'NetworkStack');
const compute = new ComputeStack(app, 'ComputeStack', { vpc: network.vpc });

And the compute stack’s constructor looks like:

interface ComputeStackProps extends cdk.StackProps {
  readonly vpc: cdk.aws_ec2.IVpc;
}

Marisa: Ah, so now other stacks can share the same networking layer! That’s clean modular design.


Part 2 — 🔐 Security Groups and Network ACLs

Scene 4: Security Groups — The Virtual Firewalls

Reimu: Security Groups (SGs) are like stateful firewalls for your EC2, Lambda, or RDS.

import { SecurityGroup, Peer, Port } from 'aws-cdk-lib/aws-ec2';

const webSg = new SecurityGroup(this, 'WebServerSG', {
  vpc: this.vpc,
  allowAllOutbound: true,
  description: 'Allow HTTP and SSH access',
});

webSg.addIngressRule(Peer.anyIpv4(), Port.tcp(80), 'Allow HTTP');
webSg.addIngressRule(Peer.ipv4('203.0.113.0/24'), Port.tcp(22), 'Allow SSH from office');

Marisa: So it’s “stateful”, meaning responses are automatically allowed back in?

Reimu: Exactly. If you allow inbound on port 80, the return traffic is automatically allowed — unlike NACLs.


Scene 5: Network ACLs (NACLs)

Reimu: NACLs are stateless and apply at the subnet level, not per resource. They’re useful for stricter compliance.

import { NetworkAcl, NetworkAclEntry, AclCidr, AclTraffic, Action } from 'aws-cdk-lib/aws-ec2';

const nacl = new NetworkAcl(this, 'PublicSubnetAcl', {
  vpc: this.vpc,
  subnetSelection: { subnetType: cdk.aws_ec2.SubnetType.PUBLIC },
});

new NetworkAclEntry(this, 'AllowHttpInbound', {
  networkAcl: nacl,
  ruleNumber: 100,
  cidr: AclCidr.anyIpv4(),
  traffic: AclTraffic.tcpPort(80),
  direction: cdk.aws_ec2.TrafficDirection.INGRESS,
  ruleAction: Action.ALLOW,
});

Marisa: So SGs are for applications, NACLs are for subnet-level guardrails?

Reimu: Perfect summary! Most projects rely on SGs, but regulated environments might need both.


Part 3 — 🧩 IAM Roles, Policies, and Least Privilege

Scene 6: Creating an IAM Role

Reimu: Now let’s talk IAM — who can access what. A Role defines permissions for AWS services like Lambda or EC2.

import { Role, ServicePrincipal, ManagedPolicy } from 'aws-cdk-lib/aws-iam';

const lambdaRole = new Role(this, 'LambdaRole', {
  assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
  description: 'Role for Lambda with S3 and CloudWatch access',
});

lambdaRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'));
lambdaRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AmazonS3ReadOnlyAccess'));

Marisa: So this Lambda can write logs to CloudWatch and read from S3, but nothing else?

Reimu: Exactly — that’s least privilege. Always grant only what’s needed.


Scene 7: Inline Policies for Custom Permissions

Reimu: If you need more fine-grained access, use inline JSON policies.

import { PolicyStatement } from 'aws-cdk-lib/aws-iam';

lambdaRole.addToPolicy(
  new PolicyStatement({
    actions: ['dynamodb:PutItem', 'dynamodb:GetItem'],
    resources: ['arn:aws:dynamodb:ap-northeast-1:123456789012:table/UserTable'],
  })
);

Marisa: So now the Lambda can access only that specific table? Cool — no wildcard madness!


Scene 8: Using IAM Roles with Lambda

Reimu: Now attach the role to a Lambda.

import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda';

const lambda = new Function(this, 'SecureLambda', {
  runtime: Runtime.NODEJS_18_X,
  handler: 'index.handler',
  code: Code.fromInline(`
    exports.handler = async () => {
      console.log("Accessing DynamoDB securely!");
    };
  `),
  role: lambdaRole,
});

Marisa: And because the role was defined earlier, the Lambda automatically inherits all the right permissions. No more trial-and-error in the AWS console!


Part 4 — 🧱 Example: Web Server in a Private Subnet

Reimu: Let’s tie it all together — a VPC with a web server that lives privately behind a NAT gateway.

import { Instance, InstanceType, MachineImage, SubnetType } from 'aws-cdk-lib/aws-ec2';

const instance = new Instance(this, 'PrivateWebServer', {
  vpc: this.vpc,
  instanceType: new InstanceType('t3.micro'),
  machineImage: MachineImage.latestAmazonLinux2(),
  vpcSubnets: { subnetType: SubnetType.PRIVATE_WITH_EGRESS },
  securityGroup: webSg,
  keyName: 'my-keypair',
});

Marisa: So this instance can reach the internet outbound through NAT, but it’s not directly accessible from outside — secure by design!

Reimu: Exactly. That’s the best practice pattern for private workloads.


Scene 9: Visualizing the Network

graph TD
  A[Internet] -->|HTTP| B[Public Subnet / ALB]
  B -->|NAT| C[Private Subnet / EC2, Lambda]
  C --> D[RDS / DynamoDB]
  C -.-> E[IAM Roles & Policies]

Marisa: That diagram makes everything clear — the traffic flows in, passes through controlled layers, and IAM protects the inside.


Part 5 — 🧰 Best Practices Summary

Networking Best Practices

Area Best Practice
VPC Design Use public/private subnets, enable NAT for private egress
High Availability Deploy across at least 2 AZs
Security Groups Allow only required ports, block all else
NACLs Optional layer for compliance or audit
IAM Roles Use service-specific roles with least privilege
Policies Avoid * in actions or resources

Scene 10: Cleanup

cdk destroy

Marisa: That cleans up the entire VPC, NAT, and IAM roles?

Reimu: Yup — everything defined in your CDK stack is reproducible and disposable.


Scene 11: Recap

You Learned in Chapter 7

Concept Key Takeaway
VPC The base network for all AWS resources
Subnets Public for internet-facing, private for internal
Security Groups Stateful firewalls for instances & Lambdas
Network ACLs Stateless rules for subnets
IAM Fine-grained access control for AWS services
Least Privilege Only grant what’s needed — nothing more

Marisa: Now I finally understand how all these pieces connect — networking, security, and identity working together.

Reimu: Exactly. Next time, we’ll dive into Event-Driven & Messaging Architectures — SQS, SNS, and EventBridge — the backbone of async systems.

Marisa: I’m ready! Bring on the queues and events! 📬

📬 Chapter 8: Event-Driven & Messaging Architectures — SQS, SNS, EventBridge

— with Yukkuri Reimu & Yukkuri Marisa


Scene 1: From Request-Response to Event-Driven

Reimu: Hey Marisa, today we’re diving into the heart of asynchronous systemsevent-driven architecture!

Marisa: Ooh, like when one Lambda sends a message and another reacts to it later?

Reimu: Exactly! Instead of directly calling each other, components talk through events using SNS, SQS, or EventBridge — making systems more scalable and decoupled.

Let’s build each step with CDK!


Part 1 — 📦 SQS: Simple Queue Service

Scene 2: Creating an SQS Queue

// lib/stacks/queue-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Queue } from 'aws-cdk-lib/aws-sqs';

export class QueueStack extends cdk.Stack {
  public readonly queue: Queue;

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

    this.queue = new Queue(this, 'AppQueue', {
      visibilityTimeout: cdk.Duration.seconds(30),
      retentionPeriod: cdk.Duration.days(4),
    });

    new cdk.CfnOutput(this, 'QueueUrl', { value: this.queue.queueUrl });
  }
}

Marisa: So this queue holds messages until a consumer (like a Lambda) picks them up, right?

Reimu: Exactly. SQS ensures durability and decoupling — your producers can keep running even if consumers are slow.


Scene 3: Connecting a Lambda Consumer to SQS

// lib/constructs/lambda-sqs-consumer.ts
import { Construct } from 'constructs';
import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda';
import { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources';
import { Queue } from 'aws-cdk-lib/aws-sqs';

export interface LambdaSqsConsumerProps {
  readonly queue: Queue;
}

export class LambdaSqsConsumer extends Construct {
  constructor(scope: Construct, id: string, props: LambdaSqsConsumerProps) {
    super(scope, id);

    const consumer = new Function(this, 'QueueConsumerLambda', {
      runtime: Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: Code.fromInline(`
        exports.handler = async (event) => {
          for (const record of event.Records) {
            console.log("Message received:", record.body);
          }
        };
      `),
    });

    consumer.addEventSource(new SqsEventSource(props.queue, { batchSize: 5 }));
  }
}

Marisa: So the Lambda automatically triggers when new messages appear in the queue?

Reimu: Exactly. No need for manual polling — CDK wires everything up.


Part 2 — 📣 SNS: Simple Notification Service

Scene 4: Publishing and Subscribing

Reimu: SNS is all about broadcasting events — one-to-many communication.

// lib/stacks/sns-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Topic } from 'aws-cdk-lib/aws-sns';
import { EmailSubscription } from 'aws-cdk-lib/aws-sns-subscriptions';
import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda';
import { LambdaSubscription } from 'aws-cdk-lib/aws-sns-subscriptions';

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

    const topic = new Topic(this, 'UserSignupTopic', {
      displayName: 'User Signup Notifications',
    });

    topic.addSubscription(new EmailSubscription('example@domain.com'));

    const lambda = new Function(this, 'EmailLogger', {
      runtime: Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: Code.fromInline(`
        exports.handler = async (event) => {
          console.log("SNS Event:", JSON.stringify(event));
        };
      `),
    });

    topic.addSubscription(new LambdaSubscription(lambda));

    new cdk.CfnOutput(this, 'TopicArn', { value: topic.topicArn });
  }
}

Marisa: So if an event is published to this topic, both the email and the Lambda subscriber will receive it?

Reimu: Exactly! SNS = fan-out pattern. One event → multiple subscribers.

You can test it with:

aws sns publish --topic-arn <TopicArn> --message "New user registered!"

Part 3 — 🕸️ EventBridge: The AWS Event Bus

Scene 5: Creating Event Rules and Targets

Reimu: EventBridge is even more powerful — it routes structured events between AWS services.

Let’s create a rule that triggers a Lambda when a “user.created” event occurs.

// lib/stacks/eventbridge-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { EventBus, Rule, EventField } from 'aws-cdk-lib/aws-events';
import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets';
import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda';

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

    const bus = new EventBus(this, 'CustomBus', {
      eventBusName: 'UserEventsBus',
    });

    const lambda = new Function(this, 'UserCreatedHandler', {
      runtime: Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: Code.fromInline(`
        exports.handler = async (event) => {
          console.log("User created event received:", JSON.stringify(event));
        };
      `),
    });

    new Rule(this, 'UserCreatedRule', {
      eventBus: bus,
      eventPattern: {
        source: ['app.user'],
        detailType: ['user.created'],
      },
      targets: [new LambdaFunction(lambda)],
    });
  }
}

Marisa: So I can publish events to this bus, and EventBridge will route them by matching the pattern?

Reimu: Exactly! Here’s a test event:

aws events put-events --entries '[
  {
    "Source": "app.user",
    "DetailType": "user.created",
    "Detail": "{\"userId\":\"123\",\"email\":\"hi@example.com\"}",
    "EventBusName": "UserEventsBus"
  }
]'

Marisa: Then the Lambda will automatically log that new user? That’s so clean!


Part 4 — 🔗 Connecting Resources Across Stacks or Accounts

Scene 6: Cross-Stack Integration

Reimu: Want to connect components from different stacks? CDK lets you pass references easily.

// In app.ts
const queueStack = new QueueStack(app, 'QueueStack');
const eventStack = new EventBridgeStack(app, 'EventStack', {
  queue: queueStack.queue,
});

Then inside the event stack:

interface EventBridgeStackProps extends cdk.StackProps {
  readonly queue: cdk.aws_sqs.IQueue;
}

queue.grantSendMessages(lambda);

Marisa: So even if my queue and event bus are in separate stacks, I can safely wire them together?

Reimu: Exactly. And for cross-account systems, CDK supports exporting ARNs and using fromQueueArn or fromTopicArn to import them.

const importedQueue = cdk.aws_sqs.Queue.fromQueueArn(
  this,
  'ImportedQueue',
  'arn:aws:sqs:us-east-1:111122223333:AppQueue'
);

Part 5 — 🧠 Architecture Visualization

graph TD
  A[Producer Lambda] -->|Publish| B[SNS Topic]
  B --> C1[Email Subscriber]
  B --> C2[SQS Queue Consumer Lambda]
  D[User Service] -->|PutEvent| E[EventBridge Bus]
  E --> F[UserCreated Lambda]

Marisa: So SNS handles fan-out, SQS buffers workloads, and EventBridge orchestrates event flows. Each has its own specialty!

Reimu: Exactly — SNS for broadcasting, SQS for decoupling, EventBridge for orchestration. Together, they form a powerful event-driven ecosystem.


Part 6 — 🧰 Best Practices

Event-Driven Design Guidelines

Concept Best Practice
Loose Coupling Use SNS/SQS/EventBridge instead of direct Lambda calls
Error Handling Enable dead-letter queues (DLQs) for SQS and Lambda
Schema Discipline Use consistent detailType and source for EventBridge events
Cross-Stack Access Pass constructs or use imported ARNs
Security Use least-privilege policies (queue.grantSendMessages())

Scene 7: Clean-Up

cdk destroy

Marisa: That tears down all queues, topics, and event rules?

Reimu: Yep — CDK keeps your event world tidy and reproducible.


Scene 8: Recap

You Learned in Chapter 8

Topic Key Skill
SQS Reliable queueing and async decoupling
SNS Publish-subscribe broadcast pattern
EventBridge Event routing and orchestration
Lambda Integrations Event-triggered functions
Cross-Stack Resources Sharing queues, topics, and buses safely

Marisa: So this is how big distributed systems stay loosely coupled and scalable!

Reimu: Exactly! Next time, we’ll move into Advanced CDK Patterns and Reusable Constructs — you’ll learn to build your own CDK libraries and internal frameworks.

Marisa: Perfect! Let’s go build some reusable magic next! 🧩✨

🧩 Chapter 9: Constructs and Reusability — Creating Your Own Constructs

— with Yukkuri Reimu & Yukkuri Marisa


Scene 1: Why Build Custom Constructs?

Marisa: Hey Reimu, every CDK project I make seems to start the same way — create an S3 bucket, attach a Lambda, maybe an API Gateway… It’s getting repetitive!

Reimu: Exactly! That’s where custom constructs come in. Think of them as your own building blocks — reusable, composable pieces of infrastructure.

Marisa: Like my own Lego pieces for AWS?

Reimu: Perfect analogy! Instead of rewriting S3 + Lambda + IAM setup each time, you build it once as a Construct, and reuse it across stacks, projects, or even publish it to npm.


Part 1 — 🧱 What Is a Construct?

Reimu: A Construct is the smallest unit of abstraction in CDK. Everything in CDK — a Bucket, a Function, a VPC — is a construct!

They form a tree structure:

App
 └─ Stack
     ├─ Construct (e.g. Bucket)
     └─ Construct (e.g. Lambda)

Let’s create our own reusable one!


Part 2 — ✨ Creating a Custom Construct

Scene 2: Folder Structure

lib/
├── constructs/
│   └── lambda-with-s3.ts
├── stacks/
│   └── app-stack.ts
└── ...

Scene 3: Building the Construct

// lib/constructs/lambda-with-s3.ts
import { Construct } from 'constructs';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda';
import { RemovalPolicy } from 'aws-cdk-lib';

export interface LambdaWithS3Props {
  readonly functionName?: string;
  readonly bucketName?: string;
}

export class LambdaWithS3 extends Construct {
  public readonly bucket: Bucket;
  public readonly lambda: Function;

  constructor(scope: Construct, id: string, props: LambdaWithS3Props = {}) {
    super(scope, id);

    // 1. Create an S3 bucket
    this.bucket = new Bucket(this, 'AppBucket', {
      bucketName: props.bucketName,
      versioned: true,
      removalPolicy: RemovalPolicy.DESTROY,
    });

    // 2. Create a Lambda that interacts with the bucket
    this.lambda = new Function(this, 'AppLambda', {
      runtime: Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: Code.fromInline(`
        const AWS = require('aws-sdk');
        const s3 = new AWS.S3();
        exports.handler = async () => {
          const res = await s3.listObjectsV2({ Bucket: process.env.BUCKET_NAME }).promise();
          console.log("Objects:", res.Contents);
          return { statusCode: 200, body: JSON.stringify(res.Contents) };
        };
      `),
      functionName: props.functionName,
      environment: { BUCKET_NAME: this.bucket.bucketName },
    });

    // 3. Grant permissions
    this.bucket.grantRead(this.lambda);
  }
}

Marisa: So this construct automatically creates both S3 and Lambda — and links them?

Reimu: Exactly! You can now reuse this single class in multiple stacks without repeating setup code.


Part 3 — 🧩 Using the Custom Construct

// lib/stacks/app-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { LambdaWithS3 } from '../constructs/lambda-with-s3';

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

    new LambdaWithS3(this, 'DataProcessor', {
      bucketName: 'app-data-bucket',
      functionName: 'AppDataLambda',
    });

    new LambdaWithS3(this, 'LogsHandler', {
      bucketName: 'app-logs-bucket',
      functionName: 'AppLogsLambda',
    });
  }
}

Marisa: Whoa, two Lambda + S3 pairs with just a few lines! That’s crazy efficient!

Reimu: Exactly. The magic of abstraction and composition — the same logic, no duplication.


Part 4 — 🧩 Understanding Construct Levels (L1 / L2 / L3)

Scene 4: The Three Layers of Constructs

Reimu: In CDK, constructs come in three abstraction levels:

Level Description Example
L1 Direct mapping to CloudFormation resources (low-level, verbose) CfnBucket, CfnFunction
L2 High-level AWS service abstraction with sensible defaults Bucket, Function, Vpc
L3 Composed constructs (multiple services combined into a pattern) LambdaWithS3, ApplicationLoadBalancedFargateService

Marisa: So we’ve just built an L3 Construct — combining multiple L2 resources!

Reimu: Exactly! Here’s an example of all levels side-by-side:

// L1: Raw CloudFormation
new cdk.aws_s3.CfnBucket(this, 'RawBucket', {
  versioningConfiguration: { status: 'Enabled' },
});

// L2: CDK Wrapper
new cdk.aws_s3.Bucket(this, 'SimpleBucket', {
  versioned: true,
});

// L3: Custom Construct (composition)
new LambdaWithS3(this, 'FullStackBucket');

Marisa: Ah, I see — L1 is low-level, L2 is practical, and L3 is reusable business logic!


Part 5 — 📦 Packaging Constructs for Reuse

Scene 5: Making It a Library

Reimu: You can package your construct as a reusable npm library. Let’s turn it into a shared package.

Step 1: Folder layout for a construct library

cdk-constructs/
├── package.json
├── tsconfig.json
└── src/
    └── lambda-with-s3.ts

Step 2: package.json

{
  "name": "@your-org/cdk-constructs",
  "version": "1.0.0",
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "scripts": {
    "build": "tsc"
  },
  "dependencies": {
    "aws-cdk-lib": "^2.150.0",
    "constructs": "^10.3.0"
  }
}

Step 3: tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "declaration": true,
    "outDir": "lib",
    "strict": true
  },
  "include": ["src/**/*.ts"]
}

Step 4: Export it in src/index.ts

export * from './lambda-with-s3';

Step 5: Publish to npm or a private registry

npm run build
npm publish

Marisa: So I can now install it in another project like:

npm install @your-org/cdk-constructs

and use it directly?

Reimu: Exactly! Then in any CDK app:

import { LambdaWithS3 } from '@your-org/cdk-constructs';

Part 6 — 🧰 Pro Tips for Reusable Constructs

Best Practices for Building Constructs

Tip Description
Use Props Interfaces Clearly define inputs; keep them optional when safe
Expose Outputs e.g., bucket.bucketName for cross-stack references
Follow CDK naming Use PascalCase for classes, camelCase for props
Avoid Hardcoding Use environment variables or context for region/account
Document Everything Use JSDoc for clarity (/** ... */)
Publish Safely Tag releases, follow semantic versioning

Scene 7: Visualizing the Concept

graph TD
  A[L1: CfnBucket] --> B[L2: Bucket]
  B --> C[L3: LambdaWithS3]
  C --> D[AppStack: Uses Reusable Constructs]

Marisa: So constructs are basically “CDK within CDK”! The more I modularize, the cleaner my infra code becomes.

Reimu: Exactly! CDK constructs let you apply software engineering principles — abstraction, DRY, versioning — to your infrastructure.


Scene 8 — Recap

You Learned in Chapter 9

Topic Key Takeaway
Why Custom Constructs Eliminate repetition, increase maintainability
Construct Levels (L1–L3) From raw CFN → service-level → reusable patterns
Reusable Packaging Share your infra as npm modules
Best Practices Type-safe props, clean naming, semantic versioning

Marisa: So basically, we can build our own CDK “frameworks”! Reusable pieces that encapsulate patterns for the whole team.

Reimu: Exactly. And in the next chapter, we’ll see how to test, validate, and automate deployments with CI/CD pipelines for CDK — the DevOps layer of Infrastructure as Code.

Marisa: Perfect! Testing and automation — bring it on! 🚀

⚙️ Chapter 10: Testing, Validation and CI/CD for CDK Apps

— with Yukkuri Reimu & Yukkuri Marisa


Scene 1: Why Test Infrastructure as Code?

Reimu: Marisa, when you write app code, you write tests, right?

Marisa: Of course! Unit tests, integration tests… but for CDK? Isn’t it just “deploy and pray”?

Reimu: (laughs) That’s the old way! With AWS CDK, you can test, validate, and automate infrastructure just like application code. This chapter covers three big ideas:

  1. 🧪 Unit testing with assertions and snapshots
  2. 🚀 Integration tests and test deployments
  3. 🔁 CI/CD pipelines for automatic deployment

Part 1 — 🧪 Unit Testing CDK Code

Scene 2: Setting up the Test Framework

Reimu: CDK projects already use Jest by default. Let’s install dependencies first:

npm install --save-dev jest @types/jest ts-jest aws-cdk-lib constructs

Then initialize Jest:

npx ts-jest config:init

Scene 3: Writing a Simple Stack

We’ll use a simple stack that creates an S3 bucket.

// lib/s3-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Bucket } from 'aws-cdk-lib/aws-s3';

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

    new Bucket(this, 'MyTestBucket', {
      versioned: true,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });
  }
}

Scene 4: Assertion Tests

Reimu: Let’s check if our stack actually creates the bucket with versioning enabled.

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

test('S3 bucket is versioned', () => {
  const app = new cdk.App();
  const stack = new S3Stack(app, 'TestStack');
  const template = Template.fromStack(stack);

  template.hasResourceProperties('AWS::S3::Bucket', {
    VersioningConfiguration: { Status: 'Enabled' },
  });
});

Marisa: So this test doesn’t deploy anything? It just checks the synthesized CloudFormation template?

Reimu: Exactly! You’re verifying the intent of your IaC before deploying. Fast and safe.


Scene 5: Snapshot Testing

Reimu: You can also snapshot the entire stack — useful for regression tests.

test('Snapshot test for entire template', () => {
  const app = new cdk.App();
  const stack = new S3Stack(app, 'SnapshotStack');
  const template = Template.fromStack(stack);

  expect(template.toJSON()).toMatchSnapshot();
});

Run the tests:

npm test

Jest will generate and compare snapshot files in __snapshots__/.

Marisa: So if someone changes the CDK code, the test fails unless they intentionally update the snapshot?

Reimu: Exactly. It’s like version control for your infrastructure intent!


Part 2 — 🔄 Integration Tests and Test Environments

Scene 6: Deploying to a Test Environment

Reimu: For deeper validation, create a dedicated test stage.

// bin/app.ts
import * as cdk from 'aws-cdk-lib';
import { S3Stack } from '../lib/s3-stack';

const app = new cdk.App();

new S3Stack(app, 'S3Stack-Test', {
  env: { account: '111122223333', region: 'us-east-1' },
});

new S3Stack(app, 'S3Stack-Prod', {
  env: { account: '444455556666', region: 'us-east-1' },
});

Marisa: So I can deploy to the test environment first, validate, then deploy to prod later?

Reimu: Exactly — separate stages mean safer rollouts.


Scene 7: Integration Test Example (Runtime Validation)

You can even test deployed stacks using AWS SDK in Jest.

// test/integration.test.ts
import { S3 } from 'aws-sdk';

test('Bucket exists after deployment', async () => {
  const s3 = new S3({ region: 'us-east-1' });
  const result = await s3.listBuckets().promise();
  const bucketNames = result.Buckets?.map(b => b.Name);
  expect(bucketNames).toContain('app-data-bucket');
});

Marisa: So this actually calls AWS? Wouldn’t that need credentials?

Reimu: Yep, that’s why these are integration tests, not unit tests. They’re slower, but confirm real-world behavior.


Part 3 — 🤖 CI/CD Pipelines with AWS CDK Pipelines

Scene 8: Automating the Workflow

Reimu: Finally, let’s make deployment automatic with CDK Pipelines.

Install dependencies:

npm install aws-cdk-lib constructs

Then create a pipeline stack:

// lib/pipeline-stack.ts
import * as cdk from 'aws-cdk-lib';
import { CodePipeline, CodePipelineSource, ShellStep } from 'aws-cdk-lib/pipelines';
import { Construct } from 'constructs';
import { S3Stack } from './s3-stack';

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

    const pipeline = new CodePipeline(this, 'AppPipeline', {
      pipelineName: 'CDKAppPipeline',
      synth: new ShellStep('Synth', {
        input: CodePipelineSource.gitHub('your-org/your-repo', 'main'),
        commands: [
          'npm ci',
          'npm run build',
          'npm run test',
          'npx cdk synth'
        ],
      }),
    });

    pipeline.addStage(new DeployStage(this, 'TestStage', {
      env: { account: '111122223333', region: 'us-east-1' },
    }));

    pipeline.addStage(new DeployStage(this, 'ProdStage', {
      env: { account: '444455556666', region: 'us-east-1' },
    }));
  }
}

class DeployStage extends cdk.Stage {
  constructor(scope: Construct, id: string, props?: cdk.StageProps) {
    super(scope, id, props);

    new S3Stack(this, 'DeployedS3Stack');
  }
}

Marisa: So this builds, tests, synthesizes, and deploys automatically whenever I push to GitHub?

Reimu: Exactly! It’s continuous integration + continuous delivery for your infrastructure. No more manual cdk deploy every time.


Scene 9: Pipeline Architecture Diagram

graph TD
  A[GitHub Repo] --> B[CodePipeline]
  B --> C[Synth Step (build/test/synth)]
  C --> D[Test Stage (Sandbox Account)]
  D --> E[Manual Approval?]
  E --> F[Prod Stage (Production Account)]

Marisa: That’s elegant! The test stage verifies the stack, and only then the prod stage goes live.

Reimu: Exactly — this pattern is used by real-world CDK teams at scale.


Part 4 — 🧰 Best Practices for Testing & Automation

Testing Tips

Type Purpose Tools
Unit Tests Verify template structure assertions.Template
Snapshot Tests Detect unintended changes toMatchSnapshot()
Integration Tests Validate deployed infra aws-sdk
Pipelines Automate synth → deploy aws-cdk-lib/pipelines

Pipeline Tips

  • Separate test and prod accounts
  • Automate npm run test before deploy
  • Add manual approval step before production
  • Use artifact buckets for large templates
  • Keep pipelines idempotent — re-runnable anytime

Scene 10: Cleanup

cdk destroy

Marisa: So even my pipeline is just another stack I can destroy and recreate?

Reimu: Exactly. Your entire CI/CD setup is as code — portable, repeatable, and versioned.


Scene 11 — Recap

You Learned in Chapter 10

Topic Key Takeaway
Unit Testing Verify constructs with assertions
Snapshot Testing Detect template drift
Integration Testing Validate real AWS behavior
CDK Pipelines Automate deploys with test/prod stages
Best Practices Least privilege, staged rollout, CI hooks

Marisa: I love this! CDK is truly software engineering for infrastructure — tests, pipelines, everything.

Reimu: Exactly! Next up, we’ll dive into multi-account strategies and cross-region deployments, so you can scale your CDK apps beyond a single environment.

Marisa: Let’s go! Infrastructure at enterprise scale — here we come! 🚀

🌍 Chapter 11: Multi-Account / Multi-Region Deployment Strategies

— with Yukkuri Reimu & Yukkuri Marisa


Scene 1: Why Go Multi-Account or Multi-Region?

Marisa: Hey Reimu, we’ve been deploying everything to one account and one region. But my boss just said, “Let’s separate prod and dev accounts… and maybe add us-east-1 for DR.” 😨

Reimu: (laughs) Welcome to enterprise-scale CDK! When you operate multiple accounts or regions, you gain security, isolation, and resilience — but you also need to manage environments, bootstrapping, and pipelines carefully.

Let’s go step by step!


Part 1 — 🧭 Using Environments in CDK (Account & Region)

Scene 2: Defining an Environment

Reimu: In CDK, an environment is simply { account, region }. You specify it when creating a stack.

// bin/app.ts
import * as cdk from 'aws-cdk-lib';
import { AppStack } from '../lib/app-stack';

const app = new cdk.App();

const devEnv = { account: '111122223333', region: 'ap-northeast-1' };
const prodEnv = { account: '444455556666', region: 'us-east-1' };

new AppStack(app, 'AppStack-Dev', { env: devEnv });
new AppStack(app, 'AppStack-Prod', { env: prodEnv });

app.synth();

Marisa: So now I have two independent stacks in different accounts or regions?

Reimu: Exactly! Each is synthesized separately and deployed to its respective environment.


Scene 3: Accessing Environment Context in Code

Reimu: Inside your stack, you can access the current account and region dynamically:

const account = cdk.Stack.of(this).account;
const region = cdk.Stack.of(this).region;

new cdk.CfnOutput(this, 'EnvInfo', {
  value: `Account: ${account}, Region: ${region}`,
});

Marisa: That’s great — I can use it to name resources like my-app-${region} dynamically!

Reimu: Exactly. Just remember: environment context is resolved at synth-time, not runtime.


Part 2 — 🧱 Shared Infrastructure vs Workload Infrastructure

Scene 4: Splitting Shared and Workload Stacks

Reimu: Now, let’s separate shared resources (like networking or IAM roles) from workload resources (like app stacks).

// bin/app.ts
import { NetworkStack } from '../lib/network-stack';
import { ApplicationStack } from '../lib/application-stack';

const sharedEnv = { account: '111122223333', region: 'ap-northeast-1' };
const appEnv = { account: '444455556666', region: 'us-east-1' };

const network = new NetworkStack(app, 'SharedNetwork', { env: sharedEnv });
const appStack = new ApplicationStack(app, 'AppService', {
  env: appEnv,
  vpc: network.vpc, // cross-account reference (we’ll handle this next)
});

Marisa: Wait, can I just pass the VPC like that across accounts?

Reimu: Not directly — cross-account references are tricky. You’ll need exports and imports using resource ARNs or IDs.


Scene 5: Exporting Shared Resources

// lib/network-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Vpc } from 'aws-cdk-lib/aws-ec2';

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

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

    this.vpc = new Vpc(this, 'SharedVpc', { maxAzs: 2 });

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

Then import it in another account:

// lib/application-stack.ts
import { Vpc } from 'aws-cdk-lib/aws-ec2';

const importedVpc = Vpc.fromLookup(this, 'ImportedVpc', {
  vpcId: cdk.Fn.importValue('SharedVpcId'),
});

Marisa: Oh, so I export in one stack, and import in another — just like CloudFormation cross-stack outputs!

Reimu: Exactly. That’s the official CDK way for cross-account resource sharing.


Scene 6: Cross-Region Design Considerations

Reimu: But for cross-region, CDK can’t directly reference resources. You’ll need to pass parameters (like ARNs or URLs) through environment variables, or use AWS Systems Manager (SSM) Parameter Store.

import { StringParameter } from 'aws-cdk-lib/aws-ssm';

// In Tokyo
new StringParameter(this, 'VpcIdParam', {
  parameterName: '/shared/vpc/id',
  stringValue: this.vpc.vpcId,
});

// In Virginia
const importedVpcId = StringParameter.valueFromLookup(this, '/shared/vpc/id');

Marisa: Nice — so SSM works as a cross-region data bridge.


Part 3 — 🚀 Bootstrapping for Multi-Account CDK

Scene 7: What Is Bootstrapping?

Reimu: Every account and region you deploy to must be bootstrapped before using CDK. Bootstrapping creates the S3 bucket and IAM roles CDK needs internally.

Run this for each target account/region:

aws --profile dev cdk bootstrap aws://111122223333/ap-northeast-1
aws --profile prod cdk bootstrap aws://444455556666/us-east-1

Marisa: So CDK needs its own infrastructure to manage deployments?

Reimu: Exactly. This step sets up the “CDK Toolkit” stack, which holds deployment artifacts and IAM trust policies.


Scene 8: Enabling Cross-Account Pipelines

If you use CDK Pipelines (from Chapter 10), you’ll need to grant the pipeline account permissions to deploy to others.

aws cloudformation describe-stacks --stack-name CDKToolkit

Then add cross-account trust manually (or via organization policies):

{
  "Effect": "Allow",
  "Principal": { "AWS": "arn:aws:iam::999988887777:role/cdk-pipeline-role" },
  "Action": "sts:AssumeRole"
}

Marisa: So the pipeline account can “assume roles” in target accounts to deploy stacks there?

Reimu: Exactly. That’s how you automate multi-account deployments securely.


Part 4 — 🧩 Example: Multi-Account Pipeline Deployment

// lib/pipeline-stack.ts
import * as cdk from 'aws-cdk-lib';
import { CodePipeline, CodePipelineSource, ShellStep } from 'aws-cdk-lib/pipelines';
import { Construct } from 'constructs';
import { ApplicationStage } from './application-stage';

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

    const pipeline = new CodePipeline(this, 'MultiAccountPipeline', {
      synth: new ShellStep('Synth', {
        input: CodePipelineSource.gitHub('your-org/your-repo', 'main'),
        commands: ['npm ci', 'npm run build', 'npm test', 'npx cdk synth'],
      }),
    });

    pipeline.addStage(new ApplicationStage(this, 'TestStage', {
      env: { account: '111122223333', region: 'ap-northeast-1' },
    }));

    pipeline.addStage(new ApplicationStage(this, 'ProdStage', {
      env: { account: '444455556666', region: 'us-east-1' },
    }));
  }
}

// lib/application-stage.ts
import { Stage, StageProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { ApplicationStack } from './application-stack';

export class ApplicationStage extends Stage {
  constructor(scope: Construct, id: string, props?: StageProps) {
    super(scope, id, props);
    new ApplicationStack(this, 'AppStack');
  }
}

Marisa: So the pipeline deploys the same app into dev (Tokyo) and prod (Virginia) automatically — just like Chapter 10, but multi-account!

Reimu: Exactly! Each stage runs under its own AWS environment context.


Part 5 — 🧠 Visualization

graph TD
  A[GitHub Repo] --> B[Pipeline Account]
  B --> C[Test Account: ap-northeast-1]
  B --> D[Prod Account: us-east-1]
  C -->|Deploy Stacks| E[S3, Lambda, DynamoDB]
  D -->|Deploy Stacks| F[S3, Lambda, API Gateway]

Marisa: So one pipeline orchestrates multiple environments — this is the real DevOps backbone!


Part 6 — 🧰 Best Practices for Multi-Account CDK

Multi-Account Strategy Guidelines

Area Best Practice
Environment Separation Isolate dev/test/prod in different accounts
Networking Use shared VPC or Transit Gateway for secure communication
Bootstrapping Run cdk bootstrap per account/region
Cross-Account Access Use IAM roles with limited trust
SSM Parameters Pass values across regions safely
CDK Pipelines Centralize deployment automation

Scene 9: Cleanup

cdk destroy

Marisa: That even destroys multi-region resources?

Reimu: Yep — CDK tracks everything via CloudFormation stacks per environment. One destroy per account, and your infra is clean.


Scene 10 — Recap

You Learned in Chapter 11

Topic Key Takeaway
Environments Define { account, region } for precise deployment targets
Shared vs Workload Infra Split reusable network/IAM layers from app stacks
Cross-Account References Use CfnOutput, Fn.importValue, or SSM parameters
Bootstrapping Required for every target account and region
CDK Pipelines Automate multi-account, multi-region deployments

Marisa: Wow, this makes CDK truly “enterprise-ready”! It’s like managing multiple worlds — each isolated, yet connected through automation.

Reimu: Exactly! Next chapter, we’ll explore cost optimization, governance, and security — how to keep all these environments efficient and compliant.

Marisa: Bring it on, Reimu! Time to make our infra both scalable and cost-effective! 💸

💰 Chapter 12: Cost Optimization, Security, and Governance Considerations

— with Yukkuri Reimu & Yukkuri Marisa


Scene 1: Keeping Cloud Under Control

Marisa: Reimu, I just got the AWS bill... and my eyes almost popped out. I think my demo stacks are still running. 😱

Reimu: (laughs) Welcome to the cloud economy! Don’t worry — CDK isn’t just about deploying; it’s also a tool for controlling cost, improving security, and enforcing governance.

Today we’ll cover:

  1. 💰 Tagging and cost tracking
  2. 🔐 Security and compliance practices
  3. 🧭 Governance guardrails and organizational rules

Part 1 — 💰 Cost Optimization and Tagging

Scene 2: The Power of Tags

Reimu: Every AWS resource can have tags: key-value pairs used for cost tracking, automation, and filtering.

import * as cdk from 'aws-cdk-lib';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

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

    const bucket = new Bucket(this, 'DataBucket', {
      versioned: true,
    });

    cdk.Tags.of(bucket).add('Project', 'CDK-Workshop');
    cdk.Tags.of(bucket).add('Environment', 'Dev');
    cdk.Tags.of(bucket).add('Owner', 'YukkuriMarisa');
  }
}

Marisa: So these tags will show up in the AWS Billing Dashboard?

Reimu: Exactly. You can use the Cost Explorer to filter by Project or Environment to track costs per team or workload.


Scene 3: Global Tagging for Entire Stack

Reimu: Instead of tagging each resource, you can apply global tags at the app or stack level.

// bin/app.ts
const app = new cdk.App();

cdk.Tags.of(app).add('Organization', 'YukkuriLabs');
cdk.Tags.of(app).add('CostCenter', 'R&D-Infra');

All resources inside the app inherit these tags automatically.

Marisa: Oh nice, one line to tag the entire infrastructure! That’s cleaner.


Scene 4: Lifecycle Policies for Cost Control

Reimu: Cost optimization also means automatic cleanup. For example, use S3 lifecycle rules to delete old data.

import { LifecycleRule } from 'aws-cdk-lib/aws-s3';

const bucket = new Bucket(this, 'LogBucket', {
  versioned: true,
  lifecycleRules: [
    {
      id: 'ExpireOldLogs',
      expiration: cdk.Duration.days(30),
    },
  ],
});

Marisa: So logs older than 30 days are deleted automatically — no manual cleanup?

Reimu: Exactly! Automation saves money and time.


Part 2 — 🔐 Security Best Practices

Scene 5: Encryption Everywhere

Reimu: Encryption is the first layer of defense. CDK makes enabling encryption dead simple.

import { BucketEncryption } from 'aws-cdk-lib/aws-s3';
import { Key } from 'aws-cdk-lib/aws-kms';

const kmsKey = new Key(this, 'AppKey', {
  enableKeyRotation: true,
  alias: 'alias/app-key',
});

new Bucket(this, 'SecureBucket', {
  encryption: BucketEncryption.KMS,
  encryptionKey: kmsKey,
});

Marisa: So the bucket uses my KMS key instead of AWS-managed keys?

Reimu: Exactly — and rotating that key yearly is best practice. You can also use it for DynamoDB, RDS, or SQS.


Scene 6: Logging and Auditing

Reimu: Always log what happens in your environment — with CloudTrail and CloudWatch Logs.

import { Trail } from 'aws-cdk-lib/aws-cloudtrail';
import { LogGroup } from 'aws-cdk-lib/aws-logs';

const logGroup = new LogGroup(this, 'TrailLogs');
new Trail(this, 'AuditTrail', {
  bucket: new Bucket(this, 'TrailBucket'),
  sendToCloudWatchLogs: true,
  cloudWatchLogGroup: logGroup,
});

Marisa: So every AWS API call gets recorded — like who deployed what and when?

Reimu: Exactly. That’s your audit trail for compliance and security analysis.


Scene 7: Least-Privilege IAM Policies

import { Role, ServicePrincipal, PolicyStatement } from 'aws-cdk-lib/aws-iam';

const lambdaRole = new Role(this, 'LambdaRole', {
  assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
});

lambdaRole.addToPolicy(new PolicyStatement({
  actions: ['s3:GetObject'],
  resources: ['arn:aws:s3:::secure-bucket/*'],
}));

Reimu: Only grant the minimum necessary permissions — that’s called least privilege.

Marisa: Makes sense. No more AdministratorAccess for every Lambda I create! 😅


Scene 8: Security Group Hardening

Reimu: And don’t forget network-level security.

import { SecurityGroup, Peer, Port } from 'aws-cdk-lib/aws-ec2';

const sg = new SecurityGroup(this, 'WebSG', { vpc });
sg.addIngressRule(Peer.ipv4('203.0.113.0/24'), Port.tcp(443), 'Allow HTTPS only');

Marisa: So no more “open to the world” 0.0.0.0/0 rules?

Reimu: Exactly — your future self (and AWS bill) will thank you.


Part 3 — 🧭 Governance and Guardrails

Scene 9: Governance in Practice

Reimu: Large organizations often enforce governance using AWS Organizations, Service Control Policies (SCPs), and AWS Config Rules — all of which can be managed through CDK.


Scene 10: Example — Service Control Policy (SCP)

Reimu: Let’s deny expensive instance types globally with a CDK-based policy.

import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { CfnPolicy } from 'aws-cdk-lib/aws-organizations';

new CfnPolicy(this, 'DenyExpensiveInstances', {
  name: 'DenyExpensiveInstances',
  type: 'SERVICE_CONTROL_POLICY',
  description: 'Deny creation of large EC2 instance types',
  content: JSON.stringify({
    Version: '2012-10-17',
    Statement: [
      {
        Effect: 'Deny',
        Action: 'ec2:RunInstances',
        Resource: '*',
        Condition: {
          'StringEqualsIfExists': {
            'ec2:InstanceType': ['m5.24xlarge', 'c6i.32xlarge']
          }
        }
      }
    ],
  }),
});

Marisa: So this SCP prevents anyone in the org from spinning up huge EC2s?

Reimu: Exactly. Governance as code — consistent, enforced, and auditable.


Scene 11: AWS Config Rules for Compliance

Reimu: Use AWS Config to continuously check for violations.

import { ManagedRule } from 'aws-cdk-lib/aws-config';

new ManagedRule(this, 'S3PublicReadProhibited', {
  identifier: 'S3_BUCKET_PUBLIC_READ_PROHIBITED',
});

Marisa: So if someone makes a public S3 bucket by mistake, AWS Config catches it?

Reimu: Yep, and you can even trigger Lambda remediations automatically.


Scene 12: CloudWatch Alarms for Budget and Anomalies

import { Alarm, ComparisonOperator, Metric } from 'aws-cdk-lib/aws-cloudwatch';

const billingMetric = new Metric({
  namespace: 'AWS/Billing',
  metricName: 'EstimatedCharges',
  dimensionsMap: { Currency: 'USD' },
});

new Alarm(this, 'CostAlarm', {
  metric: billingMetric,
  threshold: 100,
  evaluationPeriods: 1,
  comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
});

Marisa: So I can get alerted when my bill goes over $100? Perfect! 😭

Reimu: (laughs) Yes — use SNS or Slack webhooks for notifications. Proactive alerts save both time and money.


Part 4 — 🧠 Architecture Overview

graph TD
  A[CDK Stacks] --> B[Tagged Resources]
  A --> C[KMS Encryption]
  A --> D[CloudTrail + Config Rules]
  C --> E[Governance SCPs]
  B --> F[Cost Explorer]
  D --> G[Security Alerts (SNS / Slack)]

Marisa: So CDK lets us build a self-governing cloud — tagged, secure, and monitored. That’s amazing!

Reimu: Exactly — that’s the next step after “just IaC”. It’s Infrastructure Governance as Code.


Part 5 — 🧰 Best Practices Summary

Cost Optimization

Practice Example
Tag all resources cdk.Tags.of(app).add()
Automate cleanup S3 lifecycle rules, retention policies
Monitor cost CloudWatch billing alarms

Security

Practice Example
Encrypt everything BucketEncryption.KMS
Log all actions CloudTrail + CloudWatch Logs
Enforce least privilege PolicyStatement per service
Restrict access SecurityGroup inbound rules

Governance

Practice Example
Guardrails SCPs for cost control
Compliance checks AWS Config managed rules
Organization-wide standards Shared tagging + policies

Scene 13: Cleanup

cdk destroy

Marisa: It feels good knowing I can spin up and tear down safely. This chapter really connects CDK to real-world governance.

Reimu: Exactly. CDK isn’t just about automation — it’s about responsible automation. And now you have the tools to keep your cloud lean, secure, and compliant.


Scene 14 — Recap

You Learned in Chapter 12

Topic Key Takeaway
Tagging & Cost Control Use tags, lifecycles, and alarms for budget management
Security Apply encryption, auditing, and least privilege
Governance Use SCPs, AWS Config, and organization-level rules
Culture Treat governance as code, not a manual checklist

Marisa: This was a perfect reality check — IaC isn’t just code, it’s accountability. Can we now learn how to integrate monitoring and observability too?

Reimu: Exactly where we’re heading next! In Chapter 13, we’ll explore Monitoring, Observability, and Incident Response with CDK — CloudWatch, X-Ray, alarms, dashboards, and more.

Marisa: Nice! Bring on the metrics and graphs! 📊✨

🔄 Chapter 13: Migration & Upgrades — From CDK v1 to v2, and Keeping Up to Date

— with Yukkuri Reimu & Yukkuri Marisa


Scene 1: When the Version Number Changes, the World Shifts

Marisa: Hey Reimu, my old CDK v1 project suddenly started showing warnings. It says "aws-cdk-lib v2 is now available!" — should I panic?

Reimu: (laughs) Don’t worry, Marisa! CDK v2 is an evolution, not a revolution — but it does change how things are imported, built, and versioned.

Let’s walk through what’s new, how to upgrade, and how to stay up to date without losing sleep.


Part 1 — 🧩 What Changed in CDK v2

Scene 2: The Big Picture

Reimu: CDK v1 was like a messy toolbox: hundreds of packages — one for each AWS service.

@aws-cdk/aws-s3
@aws-cdk/aws-lambda
@aws-cdk/aws-ec2
...

Marisa: Yeah… my package.json was longer than my Lambda code. 😅

Reimu: CDK v2 fixed that! Now everything is consolidated into one package:

npm install aws-cdk-lib constructs

Then import from it:

import * as cdk from 'aws-cdk-lib';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Function } from 'aws-cdk-lib/aws-lambda';

Marisa: So no more 30 dependencies in package.json?

Reimu: Exactly. One package to rule them all.


Scene 3: Key Differences — v1 vs v2

Category CDK v1 CDK v2
Libraries Many (@aws-cdk/aws-*) Single (aws-cdk-lib)
Constructs version v3 v10 (new API stability)
Minimum Node.js 10.x 14.x+
CLI Package aws-cdk same, but supports v2 only
Experimental modules @aws-cdk/aws-xxx-alpha @aws-cdk/aws-xxx-alpha (separate namespace)

Marisa: So I can’t mix v1 and v2 modules in the same app, right?

Reimu: Exactly — you choose one or the other. But migration is surprisingly smooth.


Part 2 — 🔧 Strategy for Upgrading Existing CDK Projects

Scene 4: Step-by-Step Upgrade Plan

Reimu: Let’s go step by step — it’s like refactoring your infra gently.


🩹 Step 1: Upgrade the CLI

npm install -g aws-cdk@latest
cdk --version

Output example:

2.151.0 (build 123abc)

Marisa: That’s it? Just update the CLI?

Reimu: Yep — CDK CLI is backward compatible with v1 stacks, so you can safely use the latest CLI even before upgrading your code.


⚙️ Step 2: Replace Old Dependencies

Before:

"dependencies": {
  "@aws-cdk/core": "^1.134.0",
  "@aws-cdk/aws-s3": "^1.134.0",
  "@aws-cdk/aws-lambda": "^1.134.0"
}

After:

"dependencies": {
  "aws-cdk-lib": "^2.151.0",
  "constructs": "^10.3.0"
}

Reimu: Remove all @aws-cdk/* modules. CDK v2 wraps them all into aws-cdk-lib.


🔄 Step 3: Update Imports

Before:

import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';

After:

import * as cdk from 'aws-cdk-lib';
import { Bucket } from 'aws-cdk-lib/aws-s3';

Marisa: So I just switch imports and it compiles again?

Reimu: Almost! Some property names and interfaces got slight renames — we’ll handle that next.


🧹 Step 4: Fix Deprecated APIs

Reimu: Most v1 deprecations were removed in v2. Here are common fixes:

v1 Deprecated v2 Replacement
node.tryGetContext() Stack.of(this).node.tryGetContext()
BucketEncryption.S3_MANAGED same (no change)
@aws-cdk/assert use aws-cdk-lib/assertions
cdk.Construct use constructs.Construct

Example fix:

import { Construct } from 'constructs';

🧪 Step 5: Validate with Snapshot Testing

Run:

npx jest

or use CDK’s built-in synth test:

cdk synth

If it compiles and synthesizes, you’re 95% done.

Marisa: That’s actually less painful than expected!


🚀 Step 6: Redeploy and Verify

cdk bootstrap
cdk deploy

Output:

✅ AppStack deployed successfully (v2)

Reimu: Always re-bootstrap once after migrating — CDK v2 uses updated trust policies and asset formats.


Part 3 — 🧰 Common Migration Examples

Scene 5: Example — S3 + Lambda Migration

Before (v1)

import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';
import * as lambda from '@aws-cdk/aws-lambda';

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

    const bucket = new s3.Bucket(this, 'DataBucket');
    new lambda.Function(this, 'Handler', {
      runtime: lambda.Runtime.NODEJS_12_X,
      handler: 'index.handler',
      code: lambda.Code.fromInline('exports.handler = () => console.log("hi");'),
      environment: { BUCKET: bucket.bucketName },
    });
  }
}

After (v2)

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda';

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

    const bucket = new Bucket(this, 'DataBucket');
    new Function(this, 'Handler', {
      runtime: Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: Code.fromInline('exports.handler = () => console.log("hi");'),
      environment: { BUCKET: bucket.bucketName },
    });
  }
}

Marisa: So the main changes are imports and updated Node.js runtime?

Reimu: Exactly. The logic stays the same — just a cleaner structure.


Scene 6: Testing Migration with Assertions

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

test('S3 bucket exists', () => {
  const app = new cdk.App();
  const stack = new ModernStack(app, 'TestStack');
  const template = Template.fromStack(stack);

  template.hasResource('AWS::S3::Bucket', {});
});

Reimu: Using aws-cdk-lib/assertions works directly in v2 — no need for the deprecated @aws-cdk/assert.


Part 4 — 📦 Staying Current with CDK

Scene 7: Keep Dependencies Up to Date

Reimu: CDK evolves fast — AWS adds features monthly. Use these commands regularly:

npm outdated
npm update aws-cdk-lib constructs

Or automate with Dependabot in GitHub:

# .github/dependabot.yml
updates:
  - package-ecosystem: npm
    directory: "/"
    schedule:
      interval: weekly

Marisa: So my infra will stay modern automatically?

Reimu: Exactly — automated upgrades prevent version drift.


Scene 8: Follow Deprecations and Changelogs

Reimu: Keep an eye on:

Example:

npx cdk doctor

Output:

✅ CDK CLI 2.150.0 is up to date
✅ aws-cdk-lib 2.150.0 compatible with constructs 10.3.0

Marisa: So CDK even tells me when my dependencies mismatch. That’s helpful!


Part 5 — 🧠 Best Practices for Upgrades

Upgrade Checklist

Category Action
CLI Always use latest aws-cdk
Dependencies Use aws-cdk-lib + constructs only
Imports Simplify: import { Bucket } from 'aws-cdk-lib/aws-s3'
Deprecated APIs Replace removed symbols, use assertions v2
Bootstrap Re-run for new accounts or versions
Automate Dependabot or Renovate for upgrades
Stay Informed Check changelog and GitHub issues monthly

Part 6 — 🧩 Architecture Visualization

graph TD
  A[CDK v1 Project] -->|Refactor Imports| B[Unified aws-cdk-lib]
  B -->|Update CLI| C[Bootstrap v2 Environment]
  C -->|Test & Deploy| D[Modern CDK v2 Stack]
  D -->|Stay Updated| E[Dependabot + Changelog]

Marisa: So migration is basically refactoring and cleanup — not a total rewrite.

Reimu: Exactly! And once you’re on v2, you get faster builds, fewer packages, and better stability.


Scene 9: Example — Version Pinning for Long-Term Stability

{
  "dependencies": {
    "aws-cdk-lib": "~2.150.0",
    "constructs": "~10.3.0"
  }
}

Reimu: Use ~ to pin to patch versions — safe for long-running enterprise projects.

Marisa: Got it! That keeps things predictable even when CDK updates frequently.


Part 7 — Recap

You Learned in Chapter 13

Topic Key Takeaway
CDK v2 Changes Unified library, simpler imports, Constructs v10
Upgrade Process Step-by-step migration with minimal breakage
Testing Migration Use Jest + aws-cdk-lib/assertions
Staying Current Use Dependabot, changelogs, and CDK Doctor
Best Practices Version pinning, periodic upgrade testing

Marisa: Phew! I thought migrating to v2 would be chaos, but it’s more like cleaning up a messy closet — feels great afterward.

Reimu: Exactly! CDK v2 brings clarity and maintainability — just what IaC needs.

Next up: Chapter 14 — Future Trends & CDK in the Evolving Cloud Landscape, where we’ll explore Pulumi, Terraform CDK (CDKtf), and what’s next for IaC.

Marisa: Nice! Let’s peek into the future of cloud development! ☁️🚀

🏁 Chapter 14: Capstone — Build and Deploy a Real-World Application

— with Yukkuri Reimu & Yukkuri Marisa


Scene 1: The Final Boss — Building a Real App

Marisa: Reimu! We’ve built S3s, Lambdas, APIs, even pipelines... Now it’s time for something real, right?

Reimu: Exactly. This chapter is our CDK graduation exam — we’ll build a serverless web app with:

  • 🌐 Frontend (React SPA hosted on S3 + CloudFront)
  • ⚙️ Backend (API Gateway + Lambda + DynamoDB)
  • 🧠 Database (NoSQL, auto-scaled)
  • 📈 Monitoring (CloudWatch + Alarms)

…and we’ll deploy the whole thing entirely with CDK v2.


Part 1 — 🏗️ Architecture Design

Scene 2: The Big Picture

graph TD
  subgraph Frontend
    A[React App (S3 + CloudFront)]
  end
  subgraph Backend
    B[API Gateway] --> C[Lambda Function]
    C --> D[DynamoDB Table]
  end
  subgraph Monitoring
    E[CloudWatch Logs] --> F[Alarm via SNS]
  end
  A -->|HTTP| B
  C --> E

Reimu: This is a classic “serverless 3-tier” setup — fully managed, scalable, and low-cost.

Marisa: Perfect for a portfolio or startup MVP!


Part 2 — 🧩 CDK Project Setup

Scene 3: Directory Structure

cdk-capstone/
├── bin/
│   └── cdk-capstone.ts
├── lib/
│   ├── frontend-stack.ts
│   ├── backend-stack.ts
│   ├── database-stack.ts
│   ├── monitoring-stack.ts
│   └── pipeline-stack.ts
├── lambda/
│   └── handler.ts
├── web/
│   └── build/ (React compiled files)
└── package.json

Scene 4: CDK App Entry Point

// bin/cdk-capstone.ts
import * as cdk from 'aws-cdk-lib';
import { FrontendStack } from '../lib/frontend-stack';
import { BackendStack } from '../lib/backend-stack';
import { DatabaseStack } from '../lib/database-stack';
import { MonitoringStack } from '../lib/monitoring-stack';

const app = new cdk.App();

const db = new DatabaseStack(app, 'DatabaseStack');
const backend = new BackendStack(app, 'BackendStack', { table: db.table });
const frontend = new FrontendStack(app, 'FrontendStack', { apiUrl: backend.apiUrl });
new MonitoringStack(app, 'MonitoringStack', { lambda: backend.lambda });

Reimu: We’ll use stack dependencies to connect pieces — database → backend → frontend → monitoring.

Marisa: So CDK handles the order automatically via construct references?

Reimu: Exactly! No manual wiring needed.


Part 3 — 💾 Database Layer (DynamoDB)

Scene 5: Database Stack

// lib/database-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb';

export class DatabaseStack extends cdk.Stack {
  public readonly table: Table;

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

    this.table = new Table(this, 'UserTable', {
      partitionKey: { name: 'userId', type: AttributeType.STRING },
      billingMode: BillingMode.PAY_PER_REQUEST,
      tableName: 'users',
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    new cdk.CfnOutput(this, 'TableName', { value: this.table.tableName });
  }
}

Marisa: Pay-per-request mode means no need to set capacity units manually, right?

Reimu: Exactly — cost-efficient for unpredictable workloads.


Part 4 — ⚙️ Backend Layer (Lambda + API Gateway)

Scene 6: Lambda Function Code

// lambda/handler.ts
import { DynamoDB } from 'aws-sdk';
const db = new DynamoDB.DocumentClient();

exports.handler = async (event: any) => {
  console.log("Event:", JSON.stringify(event));
  const method = event.httpMethod;

  if (method === 'GET') {
    const result = await db.scan({ TableName: process.env.TABLE_NAME! }).promise();
    return { statusCode: 200, body: JSON.stringify(result.Items) };
  }

  if (method === 'POST') {
    const item = JSON.parse(event.body);
    await db.put({ TableName: process.env.TABLE_NAME!, Item: item }).promise();
    return { statusCode: 201, body: JSON.stringify(item) };
  }

  return { statusCode: 400, body: 'Unsupported method' };
};

Scene 7: Backend Stack (API + Lambda)

// lib/backend-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Table } from 'aws-cdk-lib/aws-dynamodb';
import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda';
import { LambdaRestApi } from 'aws-cdk-lib/aws-apigateway';

interface BackendStackProps extends cdk.StackProps {
  readonly table: Table;
}

export class BackendStack extends cdk.Stack {
  public readonly apiUrl: string;
  public readonly lambda: Function;

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

    this.lambda = new Function(this, 'ApiHandler', {
      runtime: Runtime.NODEJS_18_X,
      handler: 'handler.handler',
      code: Code.fromAsset('lambda'),
      environment: { TABLE_NAME: props.table.tableName },
    });

    props.table.grantReadWriteData(this.lambda);

    const api = new LambdaRestApi(this, 'UserApi', {
      handler: this.lambda,
      proxy: false,
    });

    const users = api.root.addResource('users');
    users.addMethod('GET');
    users.addMethod('POST');

    this.apiUrl = api.url;
    new cdk.CfnOutput(this, 'ApiUrl', { value: this.apiUrl });
  }
}

Marisa: So /users handles both GET and POST — a simple CRUD API powered by Lambda and DynamoDB?

Reimu: Exactly. Stateless and infinitely scalable!


Part 5 — 🌐 Frontend Layer (S3 + CloudFront)

Scene 8: Frontend Stack

// lib/frontend-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Bucket, BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';
import { Bucket as S3Bucket } from 'aws-cdk-lib/aws-s3';
import { Distribution, OriginAccessIdentity } from 'aws-cdk-lib/aws-cloudfront';
import { S3Origin } from 'aws-cdk-lib/aws-cloudfront-origins';

interface FrontendStackProps extends cdk.StackProps {
  readonly apiUrl: string;
}

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

    const siteBucket = new S3Bucket(this, 'SiteBucket', {
      websiteIndexDocument: 'index.html',
      publicReadAccess: false,
    });

    const oai = new OriginAccessIdentity(this, 'OAI');
    siteBucket.grantRead(oai);

    const distribution = new Distribution(this, 'SiteDistribution', {
      defaultRootObject: 'index.html',
      defaultBehavior: { origin: new S3Origin(siteBucket, { originAccessIdentity: oai }) },
    });

    new BucketDeployment(this, 'DeployWebsite', {
      sources: [Source.asset('./web/build')],
      destinationBucket: siteBucket,
      distribution,
      distributionPaths: ['/*'],
    });

    new cdk.CfnOutput(this, 'CloudFrontURL', { value: distribution.distributionDomainName });
  }
}

Marisa: So React build files are uploaded to S3 and served globally via CloudFront?

Reimu: Exactly — and we can embed the API endpoint in the frontend environment variables during build time.


Scene 9: React App Example

// web/src/api.js
const API_URL = process.env.REACT_APP_API_URL;

export async function getUsers() {
  const res = await fetch(`${API_URL}/users`);
  return res.json();
}

export async function addUser(user) {
  await fetch(`${API_URL}/users`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(user),
  });
}

Reimu: Build with:

REACT_APP_API_URL=https://xxxx.execute-api.ap-northeast-1.amazonaws.com/prod npm run build

Part 6 — 📈 Monitoring Stack

Scene 10: Monitoring and Alarms

// lib/monitoring-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Function } from 'aws-cdk-lib/aws-lambda';
import { Metric, Alarm, ComparisonOperator } from 'aws-cdk-lib/aws-cloudwatch';
import { Topic } from 'aws-cdk-lib/aws-sns';
import { EmailSubscription } from 'aws-cdk-lib/aws-sns-subscriptions';

interface MonitoringStackProps extends cdk.StackProps {
  readonly lambda: Function;
}

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

    const errorMetric = props.lambda.metricErrors();
    const alarmTopic = new Topic(this, 'AlarmTopic', { displayName: 'Lambda Error Alerts' });
    alarmTopic.addSubscription(new EmailSubscription('ops@example.com'));

    new Alarm(this, 'LambdaErrorAlarm', {
      metric: errorMetric,
      threshold: 1,
      evaluationPeriods: 1,
      comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
      alarmDescription: 'Lambda encountered at least one error',
      alarmActions: [alarmTopic.topicArn],
    });
  }
}

Marisa: So I’ll get an email if the Lambda starts failing?

Reimu: Exactly! And you can extend it with Slack or PagerDuty notifications later.


Part 7 — 🚀 Deploy and Validate

Scene 11: Deploy Everything

npm run build     # build frontend
cdk bootstrap
cdk deploy --all

Expected Output:

✅ DatabaseStack deployed
✅ BackendStack deployed
✅ FrontendStack deployed
✅ MonitoringStack deployed

CloudFrontURL = d3abcd1234.cloudfront.net
ApiUrl = https://abc123.execute-api.ap-northeast-1.amazonaws.com/prod/

Open the CloudFront URL → add users → check DynamoDB → monitor CloudWatch logs.

Marisa: It’s alive!! 🎉

Reimu: Congratulations! You’ve just deployed a fully serverless production-grade app — using only CDK code.


Part 8 — 🧰 Best Practices Recap

Architecture Design

Layer Tech CDK Constructs
Frontend S3 + CloudFront Bucket, Distribution, BucketDeployment
Backend API Gateway + Lambda LambdaRestApi, Function
Database DynamoDB Table
Monitoring CloudWatch + SNS Alarm, Topic

Deployment Tips

  • Always use --all to maintain stack dependencies
  • Store outputs (API URLs, domain names) for reuse
  • Use context (cdk.json) for environment customization
  • Apply tags for cost tracking

Scene 12: Cleanup

cdk destroy --all

Marisa: ...And just like that, my whole app vanishes cleanly. It’s like magic!

Reimu: Exactly — IaC = Reversible Magic. No more “forgotten dev environments” burning your budget.


Scene 13 — Recap

You Learned in Chapter 14

Topic Key Takeaway
End-to-End Deployment From S3 to DynamoDB via Lambda and API Gateway
Integration Passing outputs across stacks for full-stack coordination
Monitoring CloudWatch + SNS alerts
Serverless Best Practices Scalable, cost-efficient, IaC-driven
Confidence You can now design and deploy real-world systems with CDK

Marisa: This chapter felt like shipping a real product — front to back! I can finally say, “I deploy infrastructure like a pro.” 😎

Reimu: Exactly, Marisa. You’re now a CDK master — capable of building, securing, monitoring, and scaling apps in the cloud.

Next time, we’ll wrap up with the Appendices — CLI references, Python tips, and troubleshooting guides for real-world usage.

Marisa: Yay! The final bonus round! 🎓☁️

🚀 Chapter 15: Beyond the Basics — Emerging Patterns and Tools

— with Yukkuri Reimu & Yukkuri Marisa


Scene 1: The Cloud Is Bigger Than AWS

Marisa: Reimu, we’ve mastered CDK on AWS — but my company is also using Azure and GCP. Is CDK still useful outside AWS?

Reimu: Great question! CDK has evolved beyond AWS — into multi-cloud and even non-cloud infrastructure. In this chapter, we’ll explore:

  1. 🌩️ CDK in hybrid / multi-cloud contexts
  2. 🧩 Constructs marketplace & third-party modules
  3. 🛡️ Drift detection, compliance as code, and non-AWS use cases

This is the “next frontier” of Infrastructure as Code!


Part 1 — 🌩️ CDK in Hybrid & Multi-Cloud Contexts

Scene 2: Meet CDKtf and CDK8s

Reimu: CDK’s design inspired several siblings:

Tool Target Language Support Description
AWS CDK AWS CloudFormation TS, Python, etc. The classic CDK
CDKtf Terraform TS, Python, C# CDK for Terraform
CDK8s Kubernetes TS, Python, Go CDK for Kubernetes manifests

Marisa: Wait — so I can use the same CDK style to generate Terraform or K8s YAML?

Reimu: Exactly! Same constructs, same TypeScript syntax — different output engines.


Scene 3: Example — CDK for Terraform (CDKtf)

Reimu: Let’s create a Google Cloud Storage bucket via CDK code — but actually deploy it with Terraform.

// main.ts
import { App, TerraformStack } from 'cdktf';
import { GoogleProvider, StorageBucket } from '@cdktf/provider-google';

const app = new App();
const stack = new TerraformStack(app, 'MultiCloudStack');

new GoogleProvider(stack, 'google', { project: 'my-gcp-project' });

new StorageBucket(stack, 'AppBucket', {
  name: 'cdk-multicloud-bucket',
  location: 'US',
  uniformBucketLevelAccess: true,
});

app.synth();

Deploy with:

cdktf deploy

Marisa: That’s awesome — the same CDK idioms, but Terraform underneath!

Reimu: Exactly. CDKtf bridges AWS-style abstraction and multi-cloud flexibility.


Scene 4: Example — CDK8s for Kubernetes

// app.ts
import { Chart, App } from 'cdk8s';
import { KubeDeployment, KubeService } from './imports/k8s';

const app = new App();
const chart = new Chart(app, 'WebApp');

new KubeDeployment(chart, 'Deployment', {
  spec: {
    replicas: 2,
    selector: { matchLabels: { app: 'web' } },
    template: {
      metadata: { labels: { app: 'web' } },
      spec: {
        containers: [{ name: 'web', image: 'nginx', ports: [{ containerPort: 80 }] }],
      },
    },
  },
});

new KubeService(chart, 'Service', {
  spec: {
    type: 'LoadBalancer',
    selector: { app: 'web' },
    ports: [{ port: 80 }],
  },
});

app.synth();

Marisa: So CDK8s outputs Kubernetes YAML — no Helm needed?

Reimu: Exactly! It generates clean manifests you can kubectl apply directly.


Scene 5: Hybrid Cloud Example (AWS + On-Prem)

Reimu: CDK can even integrate on-prem resources via APIs or SDKs. For instance, deploying AWS Lambda functions that manage local VM configs:

new lambda.Function(this, 'SyncOnPremVMs', {
  runtime: lambda.Runtime.NODEJS_18_X,
  handler: 'index.handler',
  code: lambda.Code.fromInline(`
    const https = require('https');
    exports.handler = async () => {
      await https.request({ host: 'onprem-api.local', path: '/sync' }).end();
    };
  `),
});

Marisa: So hybrid automation is possible too — neat!


Part 2 — 🧩 Constructs Marketplace & Third-Party Modules

Scene 6: The Construct Hub

Reimu: AWS now hosts Construct Hub — a registry for community and enterprise constructs.

npx cdk import constructs

Example: A third-party S3 static site construct:

import { StaticSite } from '@cloudcomponents/cdk-static-website';

new StaticSite(this, 'MyWebsite', {
  websiteFolder: './web/build',
  domainName: 'example.com',
  certificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/abcd',
});

Marisa: So I can just reuse someone’s production-ready construct instead of rewriting everything?

Reimu: Exactly — think of it as the npm of infrastructure.


Scene 7: Publishing Your Own Construct Library

Reimu: You can even create your own construct and publish it for others!

import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';

export interface SimpleBucketProps {
  versioned?: boolean;
}

export class SimpleBucket extends Construct {
  constructor(scope: Construct, id: string, props: SimpleBucketProps = {}) {
    super(scope, id);
    new cdk.aws_s3.Bucket(this, 'Bucket', {
      versioned: props.versioned ?? true,
    });
  }
}

Then publish with:

npm publish

Marisa: So I can build my company’s internal infra modules as reusable CDK constructs?

Reimu: Exactly — Infrastructure as a Library. It’s how teams achieve large-scale consistency.


Part 3 — 🛡️ Infrastructure Drift, Compliance, and Non-AWS Use Cases

Scene 8: Detecting Infrastructure Drift

Reimu: Even IaC can drift — when someone changes resources manually. CDK can detect this via cdk diff or CloudFormation drift detection.

cdk diff

Example output:

Stack MyApp
Resources
[-] AWS::S3::Bucket myBucket has been deleted manually

Marisa: Ah, so I can spot manual changes before redeploying — nice!


Scene 9: Compliance as Code (Using AWS Config + CDK)

import { ManagedRule } from 'aws-cdk-lib/aws-config';

new ManagedRule(this, 'NoPublicBuckets', {
  identifier: 'S3_BUCKET_PUBLIC_READ_PROHIBITED',
});

new ManagedRule(this, 'EncryptedVolumes', {
  identifier: 'EC2_VOLUME_INUSE_CHECK',
});

Reimu: These rules automatically enforce compliance — turning governance into code, not paperwork.

Marisa: So “Compliance as Code” is real — and CDK helps implement it!


Scene 10: Integrating with Policy Engines (like OPA / Checkov)

Reimu: You can add policy-as-code tools like Checkov or Open Policy Agent (OPA).

checkov -d cdk.out/

Marisa: So it scans synthesized templates for security violations?

Reimu: Exactly. Combine CDK + Checkov for continuous compliance.


Scene 11: Using CDK for Non-AWS Resources

Reimu: Since CDK is just TypeScript, you can integrate any SDK-based resource — for example, provisioning GitHub repositories or Slack webhooks.

import axios from 'axios';

new cdk.CustomResource(this, 'CreateRepo', {
  serviceToken: customHandler.functionArn,
  properties: {
    repoName: 'my-cdk-repo',
  },
});

// Lambda handler
exports.handler = async (event) => {
  await axios.post('https://api.github.com/orgs/myorg/repos', {
    name: event.ResourceProperties.repoName,
  }, {
    headers: { Authorization: `token ${process.env.GITHUB_TOKEN}` }
  });
};

Marisa: So CDK can automate GitHub and SaaS provisioning too!? That’s literally “Infrastructure Beyond the Cloud”!

Reimu: Exactly — many teams now use CDK as a general-purpose automation platform.


Part 4 — 🧠 Patterns for the Future

Scene 12: Modern IaC Patterns

Pattern Description Example
App of Apps Compose multiple stacks as modules Monorepo with shared constructs
Environment-as-Code Use CDK Pipelines + Multi-Account Stacks Centralized deployment
Compliance-as-Code Define Config/Guardrails in CDK ManagedRule, SCP
Automation-as-Code Use CustomResource + Lambda Slack, GitHub APIs

Scene 13: The Future of CDK

Reimu: CDK is shaping a new IaC philosophy — one that’s developer-first, not just YAML-first. Expect future trends like:

  • AI-assisted IaC generation (already starting!)
  • Visual CDK editors
  • CDK for edge computing (IoT, CloudFront Functions)
  • Cross-cloud orchestration

Marisa: So CDK’s story isn’t ending — it’s expanding!


Part 5 — 🧩 Best Practices Summary

Emerging CDK Ecosystem

Area Key Tools Purpose
Multi-Cloud CDKtf, CDK8s Terraform & Kubernetes IaC
Constructs Construct Hub Share reusable modules
Compliance AWS Config, Checkov Continuous security
Automation Custom Resources SaaS & external integrations

Principles Going Forward

  • Think Declarative + Programmatic
  • Treat IaC as software, not configuration
  • Prefer construct reuse over copy-paste
  • Integrate linting, testing, and drift detection

Scene 14: Recap

You Learned in Chapter 15

Topic Key Takeaway
Multi-Cloud CDK CDKtf / CDK8s enable hybrid & cross-platform IaC
Constructs Ecosystem Reuse & publish modules via Construct Hub
Drift & Compliance Detect changes, enforce policies as code
Non-AWS Integrations Extend CDK to SaaS and DevOps tools
Future Vision IaC as a programmable, multi-domain platform

Scene 15: Graduation

Marisa: Wow... CDK isn’t just AWS IaC — it’s like a universal language for infrastructure!

Reimu: Exactly. From YAML to TypeScript, from one cloud to many — you’ve mastered Infrastructure as Code, the CDK way. 🌍✨

Marisa: So this is the end of the book... but the start of my IaC career!

Reimu: Perfectly said, Marisa. Now go build the next generation of cloud systems — with CDK as your magic wand. 🪄☁️

📚 Chapter 16: Appendices

— with Yukkuri Reimu & Yukkuri Marisa


Scene 1: The Grand Finale

Marisa: Reimu… we’ve finished 15 chapters of CDK already!? This feels like the final boss credits.

Reimu: (laughs) Yes, Marisa! This final chapter is your developer’s toolbox — CLI commands, code snippets, resource limits, and community guides.

Let’s make it your CDK pocket reference. 🧰✨


🅰️ Appendix A — CDK CLI Reference Cheat Sheet

Scene 2: Essential Commands

Reimu: Let’s start with the most important — your CDK CLI toolkit.

# Initialize a new TypeScript app
cdk init app --language typescript

# List all stacks
cdk list

# Synthesize CloudFormation template
cdk synth

# Compare local vs deployed stack
cdk diff

# Deploy a stack (or all)
cdk deploy [--all]

# Destroy stacks
cdk destroy [--all]

Marisa: So I can synthesize and preview before deploying?

Reimu: Exactly! cdk diff is your safety net — always check before deploying to production.


Scene 3: Environment Management

# Bootstrap an AWS environment (once per account/region)
cdk bootstrap aws://123456789012/ap-northeast-1

# Specify context or environment variables
cdk deploy -c stage=prod

Reimu: And remember, every environment (account + region) must be bootstrapped once.


Scene 4: Project Maintenance Commands

# Upgrade CDK dependencies
npm update aws-cdk-lib constructs

# Doctor check
cdk doctor

# Generate docs for your constructs
cdk docs

Marisa: So cdk doctor actually checks environment health?

Reimu: Yes! It verifies version compatibility and toolkit setup.


Scene 5: Handy Shortcuts

# Deploy with approval bypass
cdk deploy --require-approval never

# Output stack info as JSON
cdk list --json

# Run CDK commands in parallel
cdk deploy --concurrency 4

Reimu: These are life-savers for CI/CD or large environments.


🅱️ Appendix B — Useful TypeScript Snippets for CDK

Scene 6: Common Patterns

Reimu: Here are reusable TypeScript idioms that make CDK development cleaner.

✅ Environment-Aware Stack Props

const app = new cdk.App();
const env = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION };

new MyStack(app, 'MyStack', { env });

✅ Cross-Stack Reference

new cdk.CfnOutput(stackA, 'BucketName', {
  value: bucket.bucketName,
  exportName: 'SharedBucket',
});

const importedBucket = s3.Bucket.fromBucketName(stackB, 'Imported', cdk.Fn.importValue('SharedBucket'));

✅ Custom Constructs

import { Construct } from 'constructs';
import { Bucket } from 'aws-cdk-lib/aws-s3';

export class LoggingBucket extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);
    new Bucket(this, 'Bucket', {
      versioned: true,
      serverAccessLogsPrefix: 'logs/',
    });
  }
}

✅ Tagging Helper

cdk.Tags.of(app).add('Project', 'MyApp');
cdk.Tags.of(app).add('Owner', 'YukkuriMarisa');

✅ Conditional Resource Creation

if (process.env.NODE_ENV === 'prod') {
  new s3.Bucket(this, 'ProdBucket');
} else {
  new s3.Bucket(this, 'DevBucket', { removalPolicy: cdk.RemovalPolicy.DESTROY });
}

Scene 7: Testing Snippets

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

test('S3 Bucket Created', () => {
  const app = new cdk.App();
  const stack = new MyStack(app, 'TestStack');
  const template = Template.fromStack(stack);
  template.hasResource('AWS::S3::Bucket', {});
});

Marisa: So this ensures my stack always contains an S3 bucket — even after refactors?

Reimu: Exactly. Snapshot testing is your IaC safety belt. 💪


🅲️ Appendix C — AWS CDK Resource Limits & Service Quotas

Scene 8: CloudFormation & CDK Limits

Reimu: Even the cloud has limits — here are key constraints to remember.

Category Limit Notes
Stack name length 128 chars Keep IDs short
Template size 1 MB (inline) / 51 MB (S3) Large stacks split with nested stacks
Resources per stack 500 Use multiple stacks for big systems
Parameters per stack 60 Minimize dynamic parameters
Outputs per stack 200 Use cross-stack references
CDK context keys 1000 Manage with cdk.context.json

Scene 9: Service-Specific Quotas (Common Gotchas)

Service Default Limit Best Practice
S3 100 buckets per account Use prefixes instead
Lambda 1000 concurrent executions Add throttling or reserved concurrency
API Gateway 600 routes per API Modularize APIs
DynamoDB 256 tables per account Use generic tables
CloudWatch Logs 5 TPS per CreateLogGroup Pre-provision during setup

Marisa: So when I hit “resource limit exceeded”, that’s CloudFormation talking?

Reimu: Exactly — and splitting into logical stacks usually fixes it.


Scene 10: Advanced: Nested Stack Pattern

import { NestedStack } from 'aws-cdk-lib';

export class MonitoringNestedStack extends NestedStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    new cloudwatch.Alarm(this, 'Alarm', {
      metric: new cloudwatch.Metric({
        namespace: 'AWS/Lambda',
        metricName: 'Errors',
      }),
      threshold: 1,
      evaluationPeriods: 1,
    });
  }
}

Reimu: Nested stacks help keep large templates modular and under size limits.


🅳️ Appendix D — Where to Find Recipes, Community, and Further Reading

Scene 11: Official Documentation & Repos

Reimu: Your go-to places for learning and troubleshooting CDK:

Resource Link
AWS CDK Developer Guide https://docs.aws.amazon.com/cdk/latest/guide/
Construct Hub (Community Constructs) https://constructs.dev
AWS CDK GitHub https://github.com/aws/aws-cdk
CDK Patterns (Examples) https://cdkpatterns.com
Awesome CDK (Curated List) https://github.com/kolomied/awesome-cdk

Scene 12: Community & Discussion

Reimu: Get involved with the global CDK community:

📢 Slack:  cdk.dev
💬 Reddit: r/aws_cdk
🐦 Twitter/X: #AWSCDK
📺 YouTube: "AWS CDK Patterns"
🧑‍💻 Discord: aws-cdk-community

Marisa: There’s even a “CDK Patterns” site — that’s like design patterns for infrastructure?

Reimu: Exactly. Think Gang of Four, but for the cloud! ☁️


Scene 13: Further Reading

Reimu: To go deeper, check out these books and talks:

  • AWS CDK in Practice — Matthew Bonig
  • Infrastructure from Code — Cloud Engineering Summit
  • AWS CDK Best Practices — Official AWS Blog
  • CDK Day Conference Talks — YouTube archives

Scene 14: Sample Repositories & Templates

# Clone AWS samples
git clone https://github.com/aws-samples/aws-cdk-examples.git

# Example folders
aws-cdk-examples/typescript/api-lambda-dynamodb
aws-cdk-examples/python/s3-static-site
aws-cdk-examples/java/ecs-fargate-service

Marisa: Wow, so I can just clone and experiment with real examples?

Reimu: Exactly — hands-on is the fastest way to master CDK. ⚡


🎓 Epilogue — The Journey Completed

Marisa: So that’s it… from cdk init to multi-account deployments to compliance automation. Feels like we’ve built an entire cloud empire.

Reimu: You did, Marisa. You now understand the full lifecycle — from concept to governance — and how CDK ties it all together through code.

Marisa: So, what’s next?

Reimu: Next, you use this knowledge to build real systems — faster, safer, and smarter. And maybe... write your own CDK construct library someday. 🌟

Marisa: Deal! Time to deploy the future — one stack at a time! 🚀


✅ Chapter 16 Summary

Section Key Takeaway
Appendix A Master CDK CLI for fast iteration
Appendix B Keep reusable TypeScript snippets handy
Appendix C Understand resource and stack limits
Appendix D Learn from community and open source patterns

Reimu: And with that… congratulations! You’ve completed the AWS CDK v2 Hands-on Book.

Marisa: Yay!! 🥳 From zero to full-stack infrastructure magician — all in TypeScript!

Reimu: Exactly. Now go forth and build the cloud with code. ☁️✨