Bạn đã tạo S3 bucket bằng Console. Rồi Lambda function. Rồi DynamoDB table. Rồi API Gateway. Rồi IAM role. Rồi bạn cần replicate toàn bộ infrastructure cho staging environment. Bạn làm gì? Click lại từng bước trên Console? Viết bash script 500 dòng gọi AWS CLI? Hay export CloudFormation template thủ công?
CDK (Cloud Development Kit) giải quyết vấn đề này triệt để: viết infrastructure bằng code thực sự (TypeScript, Python, Go, Java, C#), CDK synthesize ra CloudFormation template, deploy nhất quán qua CloudFormation. Không YAML, không JSON thủ công, không click console.
flowchart LR
Code["CDK Code<br/>TypeScript/Python/Go"]
Synth["cdk synth<br/>→ CloudFormation JSON<br/>+ asset upload"]
Diff["cdk diff<br/>→ changeset review"]
Deploy["cdk deploy<br/>→ CloudFormation Stack"]
Resources["AWS Resources<br/>S3, Lambda, DynamoDB..."]
Code --> Synth
Synth --> Diff
Diff --> Deploy
Deploy --> Resources
```text
Bài này không phải "CDK tutorial" — mà là architecture của CDK, pattern thực chiến, và những thứ bạn sẽ gặp khi dùng CDK trong production.
---
## CDK hoạt động như thế nào?
CDK có 3 tầng kiến trúc, mỗi tầng là một level abstraction cao hơn:
```text
Level 3: Patterns (ecsPatterns.ApplicationLoadBalancedFargateService)
└── Level 2: AWS Constructs (s3.Bucket, lambda.Function)
└── Level 1: CloudFormation Resources (CfnBucket, CfnFunction)
└── CloudFormation Template (JSON/YAML)
└── AWS API Calls
```text
Khi bạn gọi `cdk synth`, CDK duyệt qua toàn bộ construct tree, tạo ra CloudFormation template JSON, upload assets (Docker image, Lambda code) lên S3 bootstrap bucket, và output template ra `cdk.out/`.
Khi bạn gọi `cdk deploy`, CDK gửi template lên CloudFormation, CloudFormation tạo changeset và apply — gọi AWS API để tạo/modify/delete resource.
### App → Stack → Construct: tổ chức code
```typescript
#!/usr/bin/env node
import * as cdk from "aws-cdk-lib";
// Level 1: App — entry point, chứa nhiều Stack
const app = new cdk.App();
// Level 2: Stack — unit of deployment (1 stack = 1 CloudFormation stack)
// Mỗi stack deploy vào 1 account + region
new MyStorageStack(app, "Storage", {
env: { account: "123456789012", region: "ap-southeast-1" },
});
new MyComputeStack(app, "Compute", {
env: { account: "123456789012", region: "ap-southeast-1" },
// Nhận reference từ Storage stack thông qua props
bucketName: "myapp-data-123456789012",
});
app.synth(); // Generate CloudFormation templates
```text
Tại sao nên tách thành nhiều stack? 3 lý do chính:
1. **Blast radius**: Nếu stack Compute fail, stack Storage vẫn intact. S3 bucket không bị xóa chỉ vì Lambda config sai
2. **Deploy frequency**: Storage thay đổi ít (vài tháng 1 lần), Compute thay đổi nhiều (mỗi sprint). Tách ra để deploy không ảnh hưởng lẫn nhau
3. **Account/Region isolation**: Stack A có thể deploy vào Production account (ap-southeast-1), Stack B vào Staging account (us-east-1) — cùng codebase
---
## Construct levels: chọn đúng level
### L1 Construct: 1:1 với CloudFormation resource
```typescript
// L1: CfnBucket — map trực tiếp vào AWS::S3::Bucket CloudFormation resource
// Mọi property bạn set là property của CloudFormation template
const bucket = new s3.CfnBucket(this, "RawBucket", {
bucketName: "myapp-data-123456789012",
versioningConfiguration: {
status: "Enabled",
},
publicAccessBlockConfiguration: {
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true,
},
bucketEncryption: {
serverSideEncryptionConfiguration: [{
serverSideEncryptionByDefault: {
sseAlgorithm: "AES256",
},
}],
},
});
```text
L1 dùng khi: CDK chưa có L2 construct cho service đó, hoặc bạn cần property rất đặc biệt mà L2 không expose.
### L2 Construct: AWS-opinionated, sensible defaults
```typescript
// L2: s3.Bucket — CDK tự set sensible defaults
const bucket = new s3.Bucket(this, "DataBucket", {
// CDK tự động:
// - Tạo bucket name (physical name) với hash suffix → tránh conflict
// - Enable encryption mặc định
// - Block public access mặc định
// - Tạo bucket policy nếu cần
// Bạn chỉ cần override những gì khác default:
versioned: true,
encryption: s3.BucketEncryption.KMS,
encryptionKey: myKmsKey,
removalPolicy: cdk.RemovalPolicy.RETAIN, // Không xóa khi destroy stack
lifecycleRules: [{
transitions: [{
storageClass: s3.StorageClass.INTELLIGENT_TIERING,
after: cdk.Duration.days(0),
}],
}],
});
```text
L2 dùng cho **90% công việc hàng ngày**. CDK tự động tạo IAM role, policy, và các resource phụ trợ.
### L3 Construct: Pattern phổ biến
```typescript
// L3: ApplicationLoadBalancedFargateService — pattern đầy đủ
// Tự động tạo: VPC (nếu chưa có), ECS Cluster, Task Definition,
// ECS Service, ALB, Target Group, Listener, Security Group, IAM roles...
// Tất cả trong 1 construct
const service = new ecsPatterns.ApplicationLoadBalancedFargateService(this, "Api", {
cluster,
cpu: 512,
memoryLimitMiB: 1024,
desiredCount: 3,
taskImageOptions: {
image: ecs.ContainerImage.fromEcrRepository(ecrRepo, "v1.0.0"),
containerPort: 3000,
environment: {
NODE_ENV: "production",
},
},
publicLoadBalancer: true,
});
```text
L3 dùng khi bạn muốn **nhanh** — một pattern phổ biến được đóng gói sẵn.
---
## Bootstrap: setup một lần cho mỗi account/region
```bash
# Bootstrap — tạo S3 bucket + IAM roles cần cho CDK deploy
cdk bootstrap aws://123456789012/ap-southeast-1
# CDK tạo:
# - S3 bucket: cdk-hnb659fds-assets-123456789012-ap-southeast-1 (lưu assets)
# - IAM roles: cdk-xxx-lookup-role, cdk-xxx-deploy-role, cdk-xxx-file-publishing-role
# - ECR repository: cdk-hnb659fds-container-assets-123456789012-ap-southeast-1
# - SSM parameters để CDK lookup thông tin
```text
Bootstrap cần chạy 1 lần cho mỗi account + region bạn muốn deploy CDK. Nếu bạn có 3 account (prod, staging, dev) và 2 region mỗi account → chạy 6 lần bootstrap.
```bash
# Bootstrap multiple accounts
cdk bootstrap aws://111111111111/ap-southeast-1
cdk bootstrap aws://111111111111/us-east-1
cdk bootstrap aws://222222222222/ap-southeast-1
cdk bootstrap aws://222222222222/us-east-1
cdk bootstrap aws://333333333333/ap-southeast-1
cdk bootstrap aws://333333333333/us-east-1
```text
---
## Vòng đời deploy: synth → diff → deploy → destroy
```bash
# 1. Synthesize: xem CloudFormation template sẽ được tạo ra
cdk synth MyStack
# Output ra cdk.out/MyStack.template.json
# Cũng upload assets (Docker image, Lambda zip) lên bootstrap bucket
# 2. Diff: xem những gì sẽ thay đổi TRƯỚC KHI deploy
cdk diff MyStack
# Output:
# [+] AWS::S3::Bucket DataBucket — NEW (sẽ được tạo)
# [~] AWS::Lambda::Function ApiHandler — UPDATE (sẽ bị modify)
# [~] MemorySize: 256 → 512
# [-] AWS::DynamoDB::Table OldTable — DESTROY (sẽ bị xóa!)
# 3. Deploy: apply changes lên AWS
cdk deploy MyStack
# Tự động tạo CloudFormation changeset và execute
# 4. Destroy: xóa toàn bộ stack (dọn dẹp khi không dùng nữa)
cdk destroy MyStack
# ⚠️ Resources có RemovalPolicy.RETAIN sẽ không bị xóa
```text
`cdk diff` là lệnh quan trọng nhất — nó cho bạn biết chính xác cái gì sẽ thay đổi trước khi bạn deploy. Luôn chạy `cdk diff` trước `cdk deploy` để tránh bất ngờ (như lỡ xóa production database).
---
## Context: tách code khỏi config
```json
// cdk.json — context values cho từng environment
{
"app": "npx ts-node --prefer-ts-exts bin/myapp.ts",
"context": {
"staging": {
"domainName": "staging.myapp.com",
"instanceCount": 1,
"logRetention": 7
},
"production": {
"domainName": "myapp.com",
"instanceCount": 3,
"logRetention": 90
}
}
}
```text
```typescript
// Dùng context trong stack
const env = app.node.tryGetContext("environment") || "staging";
const config = app.node.tryGetContext(env);
new MyStack(app, `${env}-stack`, {
env: { account: config.accountId, region: "ap-southeast-1" },
config, // Truyền config vào stack
});
// Trong stack
class MyStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: StackProps & { config: any }) {
super(scope, id, props);
const instanceCount = props.config.instanceCount;
const logRetention = props.config.logRetention;
new lambda.Function(this, "Api", {
memorySize: instanceCount > 1 ? 512 : 256,
logRetention: logRetention,
// ...
});
}
}
```text
Pattern này cho phép bạn deploy cùng một codebase ra nhiều environment — staging dùng 1 instance nhỏ, production dùng 3 instance lớn.
---
## Cross-stack reference: stack A export, stack B import
```typescript
// StorageStack: export bucket ARN
export class StorageStack extends cdk.Stack {
public readonly dataBucket: s3.IBucket;
public readonly tableArn: string;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
this.dataBucket = new s3.Bucket(this, "Data", {
encryption: s3.BucketEncryption.KMS,
});
const table = new dynamodb.Table(this, "Table", {
partitionKey: { name: "PK", type: dynamodb.AttributeType.STRING },
});
this.tableArn = table.tableArn;
// Export ra CloudFormation output (cho cross-stack reference)
new cdk.CfnOutput(this, "BucketName", {
value: this.dataBucket.bucketName,
exportName: "data-bucket-name",
});
}
}
// ComputeStack: import từ StorageStack
const app = new cdk.App();
const storageStack = new StorageStack(app, "Storage", { env: envProd });
new ComputeStack(app, "Compute", {
env: envProd,
dataBucket: storageStack.dataBucket, // Cross-stack reference
tableArn: storageStack.tableArn,
});
// Trong ComputeStack
class ComputeStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: { dataBucket: s3.IBucket; tableArn: string }) {
super(scope, id, props);
// Lambda function có quyền đọc bucket từ StorageStack
const handler = new lambda.Function(this, "Handler", { ... });
props.dataBucket.grantRead(handler); // Tự động tạo IAM policy
// Import table từ ARN
const table = dynamodb.Table.fromTableArn(this, "ImportedTable", props.tableArn);
table.grantReadWriteData(handler);
}
}
```text
---
## Aspects: cross-cutting rule cho toàn bộ stack
Aspect cho phép bạn áp dụng rule lên mọi construct trong stack — dùng để enforce tag, security policy, naming convention:
```typescript
// Aspect: tự động tag mọi taggable resource
class TagAspect implements cdk.IAspect {
constructor(private tags: Record<string, string>) {}
visit(node: IConstruct) {
if (cdk.TagManager.isTaggable(node)) {
Object.entries(this.tags).forEach(([key, value]) => {
node.tags.setTag(key, value);
});
}
}
}
// Aspect: enforce bucket versioning
class VersioningAspect implements cdk.IAspect {
visit(node: IConstruct) {
if (node instanceof s3.CfnBucket) {
node.versioningConfiguration = {
status: "Enabled",
};
}
}
}
// Apply globally
const app = new cdk.App();
cdk.Aspects.of(app).add(new TagAspect({
team: "backend",
env: "production",
managedBy: "cdk",
costCenter: "engineering",
}));
cdk.Aspects.of(app).add(new VersioningAspect());
```text
```bash
# cdk-nag: policy-as-code — tự động kiểm tra best practices
npm install cdk-nag
```text
```typescript
import { AwsSolutionsChecks } from "cdk-nag";
const app = new cdk.App();
cdk.Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));
// Tự động cảnh báo:
// - S3 bucket không enable encryption
// - Lambda dùng Admin policy thay vì least privilege
// - CloudFront không dùng HTTPS
// - RDS không enable backup
```text
---
## Testing: snapshot + fine-grained assertions
```typescript
import { Template } from "aws-cdk-lib/assertions";
import { Capture } from "aws-cdk-lib/assertions";
describe("MyStack", () => {
let template: Template;
beforeAll(() => {
const app = new cdk.App();
const stack = new MyStack(app, "TestStack", {
env: { account: "123456789012", region: "ap-southeast-1" },
});
template = Template.fromStack(stack);
});
test("S3 bucket has encryption enabled", () => {
template.hasResourceProperties("AWS::S3::Bucket", {
BucketEncryption: {
ServerSideEncryptionConfiguration: [{
ServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" },
}],
},
});
});
test("Lambda has correct configuration", () => {
template.hasResourceProperties("AWS::Lambda::Function", {
Runtime: "nodejs22.x",
MemorySize: 512,
Timeout: 30,
TracingConfig: { Mode: "Active" }, // X-Ray tracing
});
});
test("DynamoDB is PAY_PER_REQUEST", () => {
template.hasResourceProperties("AWS::DynamoDB::Table", {
BillingMode: "PAY_PER_REQUEST",
});
});
test("IAM role has no wildcard permissions", () => {
const roles = template.findResources("AWS::IAM::Role");
// Đảm bảo không có role nào có AdministratorAccess
// (nên dùng cdk-nag thay vì test thủ công)
});
});
```text
---
## Tổng kết
CDK là cách hiện đại nhất để quản lý infrastructure trên AWS. Những điểm cốt lõi:
- **App → Stack → Construct**: tổ chức code theo unit of deployment (Stack), reusable component (Construct)
- **L1/L2/L3**: dùng L2 cho 90% công việc, L3 cho pattern nhanh, L1 cho edge case
- **Bootstrap 1 lần/account/region**: tạo S3 bucket + IAM roles cho CDK deploy
- **`cdk diff` trước mỗi `cdk deploy`**: biết chính xác cái gì thay đổi, tránh xóa nhầm production
- **Context tách code khỏi config**: một codebase, deploy ra staging/production với config khác nhau
- **Cross-stack reference**: stack A export, stack B import — modular infrastructure
- **Aspects**: enforce rule toàn cục: tag, security, naming convention
- **cdk-nag**: tự động kiểm tra best practices, chặn lỗi trước khi deploy
- **RemovalPolicy.RETAIN** cho production data (S3, RDS): không xóa khi destroy stack
Bài sau: [Phần 23: CDK nâng cao — pipeline, testing, aspect, best practice](/posts/aws/23-cdk-nang-cao-pipeline-testing-aspect/)
## Câu hỏi hay gặp
**Q: CDK vs Terraform: chọn cái nào?**
A: CDK nếu team bạn viết TypeScript/Python/Go và chủ yếu dùng AWS. Terraform nếu multi-cloud hoặc cần state file management advanced. CDK synthesize ra CloudFormation → hưởng mọi feature của CloudFormation (rollback, drift detection, changeset). Terraform tự quản lý state → linh hoạt hơn nhưng phải tự xử lý state locking, backend.
**Q: CDK có hỗ trợ import resource có sẵn không?**
A: Có — dùng `cdk import` (từ CDK v2) để import existing resource vào stack. Hoặc dùng `Xxx.fromXxxArn()` static method để reference resource mà không quản lý nó trong stack.
**Q: Làm sao để rollback CDK deploy?**
A: CloudFormation tự động rollback nếu deploy fail. Nếu deploy thành công nhưng sai logic, bạn revert code và deploy lại (roll-forward, không roll-back). CloudFormation không hỗ trợ rollback về version cũ sau khi deploy thành công — đây là lý do `cdk diff` quan trọng.