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 systems — event-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:
- 🧪 Unit testing with assertions and snapshots
- 🚀 Integration tests and test deployments
- 🔁 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 testbefore 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:
- 💰 Tagging and cost tracking
- 🔐 Security and compliance practices
- 🧭 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:
- 📘 AWS CDK Changelog
- 🧩 Construct Hub
- 🐦 AWS Dev Tools Blog for new patterns
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
--allto 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:
- 🌩️ CDK in hybrid / multi-cloud contexts
- 🧩 Constructs marketplace & third-party modules
- 🛡️ 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. ☁️✨
