Bài 22 dạy CDK cơ bản — App, Stack, Construct, deploy. Bài này đi vào những thứ biến CDK từ “tool tạo resource” thành “nền tảng infrastructure production-grade”: CDK Pipelines (CI/CD cho chính infrastructure), custom construct (đóng gói pattern team), testing, cdk-nag, và best practices.
flowchart LR
Push["git push main"] --> Pipeline["CDK Pipeline<br/>CodePipeline"]
Pipeline --> Synth["Synthesize<br/>npm ci + cdk synth"]
Synth --> Staging["Deploy Staging"]
Staging --> Test["Integration Test<br/>API smoke test"]
Test --> Approve["Manual Approval"]
Approve --> Prod["Deploy Production"]
```text
---
## CDK Pipelines: infrastructure tự deploy chính nó
CDK Pipelines là CDK construct tạo ra **CodePipeline pipeline** để deploy CDK app của bạn. Điểm đặc biệt: pipeline này **tự update chính nó** khi bạn thay đổi pipeline definition.
```typescript
import { CodePipeline, CodePipelineSource, ShellStep, ManualApprovalStep } from "aws-cdk-lib/pipelines";
import * as codepipeline_actions from "aws-cdk-lib/aws-codepipeline-actions";
// Pipeline tự deploy infrastructure
const pipeline = new CodePipeline(this, "Pipeline", {
pipelineName: "myapp-infra-pipeline",
synth: new ShellStep("Synth", {
// Source: GitHub repo
input: CodePipelineSource.gitHub("myorg/myapp-infra", "main", {
authentication: cdk.SecretValue.secretsManager("github-token", {
jsonField: "token",
}),
}),
// Commands chạy trong CodeBuild
commands: [
"npm ci",
"npx cdk synth",
// cdk synth tự động detect CDK Pipelines và tạo pipeline artifacts
],
}),
// Cross-account deploy cần bootstrap trust
crossAccountKeys: true,
});
```text
### Add stages: staging → integration test → production
```typescript
// Deploy staging
const myStage = new MyAppStage(this, "Staging", {
env: { account: "222222222222", region: "ap-southeast-1" },
});
const stagingStage = pipeline.addStage(myStage);
// Smoke test sau khi staging deploy xong
stagingStage.addPost(
new ShellStep("IntegrationTest", {
commands: [
"npm run test:integration",
],
envFromCfnOutputs: {
API_URL: myStage.apiUrl, // Lấy URL từ stack output (apiUrl là CfnOutput)
},
})
);
// Manual approval trước production
pipeline.addStage(new MyAppStage(this, "Production", {
env: { account: "111111111111", region: "ap-southeast-1" },
}), {
pre: [new ManualApprovalStep("PromoteToProduction")],
// Chỉ deploy production sau khi có người approve
});
```text
### Flow đầy đủ:
```text
git push main
→ CodePipeline trigger
→ CodeBuild: npm ci + cdk synth
→ Deploy staging (CloudFormation)
→ Integration test (gọi API staging, verify response)
→ Manual approval (email/Slack noti → click Approve)
→ Deploy production (CloudFormation)
```text
CDK Pipelines tự update chính nó. Khi bạn thêm stage mới hoặc thay đổi approval flow, pipeline tự detect và update. Bạn không cần manual update pipeline definition. Đây là điểm khác biệt lớn nhất so với tự viết CodePipeline bằng CloudFormation.
---
## Custom Construct: đóng gói pattern team
Khi team bạn lặp lại cùng một pattern nhiều lần (API service = Lambda + API Gateway + DynamoDB), đóng gói thành Custom Construct:
```typescript
interface ApiServiceProps {
serviceName: string;
tableConfig: {
partitionKey: dynamodb.Attribute;
sortKey?: dynamodb.Attribute;
};
handlerPath: string;
environment?: Record<string, string>;
}
class ApiService extends Construct {
public readonly apiUrl: string;
public readonly table: dynamodb.ITable;
public readonly handler: lambda.IFunction;
constructor(scope: Construct, id: string, props: ApiServiceProps) {
super(scope, id);
// DynamoDB table
this.table = new dynamodb.Table(this, "Table", {
tableName: `${props.serviceName}-${cdk.Stack.of(this).stackName}`,
partitionKey: props.tableConfig.partitionKey,
sortKey: props.tableConfig.sortKey,
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.RETAIN, // Production data
pointInTimeRecovery: true,
});
// Lambda handler
this.handler = new lambda.Function(this, "Handler", {
runtime: lambda.Runtime.NODEJS_22_X,
handler: props.handlerPath,
code: lambda.Code.fromAsset(`dist/${props.serviceName}`),
memorySize: 512,
timeout: cdk.Duration.seconds(30),
environment: {
TABLE_NAME: this.table.tableName,
...props.environment,
},
tracing: lambda.Tracing.ACTIVE,
logRetention: logs.RetentionDays.ONE_MONTH,
});
this.table.grantReadWriteData(this.handler);
// API Gateway HTTP API
const api = new apigatewayv2.HttpApi(this, "Api", {
corsPreflight: {
allowOrigins: ["https://myapp.com"],
allowMethods: [apigatewayv2.CorsHttpMethod.ANY],
allowHeaders: ["Content-Type", "Authorization"],
},
});
api.addRoutes({
path: "/{proxy+}",
methods: [apigatewayv2.HttpMethod.ANY],
integration: new apigatewayv2integrations.HttpLambdaIntegration(
"LambdaIntegration",
this.handler
),
});
this.apiUrl = api.url!;
}
}
// Dùng custom construct trong stack
const userService = new ApiService(this, "UserService", {
serviceName: "users",
tableConfig: {
partitionKey: { name: "PK", type: dynamodb.AttributeType.STRING },
sortKey: { name: "SK", type: dynamodb.AttributeType.STRING },
},
handlerPath: "users.handler",
});
const taskService = new ApiService(this, "TaskService", {
serviceName: "tasks",
tableConfig: {
partitionKey: { name: "PK", type: dynamodb.AttributeType.STRING },
sortKey: { name: "SK", type: dynamodb.AttributeType.STRING },
},
handlerPath: "tasks.handler",
});
```text
Custom construct tiết kiệm 50-100 dòng code cho mỗi service và đảm bảo consistency: mọi service đều có tracing, log retention, PITR, removal policy giống nhau.
---
## Testing: đảm bảo infrastructure không "drift"
### Snapshot testing: phát hiện thay đổi không mong muốn
```typescript
import { Template } from "aws-cdk-lib/assertions";
describe("MyStack snapshot", () => {
test("template matches snapshot", () => {
const app = new cdk.App();
const stack = new MyStack(app, "TestStack");
const template = Template.fromStack(stack);
// Snapshot test: nếu ai đó thay đổi infrastructure (thêm/xóa resource),
// test sẽ fail → buộc developer review và update snapshot
expect(template.toJSON()).toMatchSnapshot();
});
});
```text
### Fine-grained assertions: kiểm tra resource properties
```typescript
test("Lambda has X-Ray tracing enabled", () => {
template.hasResourceProperties("AWS::Lambda::Function", {
TracingConfig: { Mode: "Active" },
});
});
test("DynamoDB has PITR enabled", () => {
template.hasResourceProperties("AWS::DynamoDB::Table", {
PointInTimeRecoverySpecification: {
PointInTimeRecoveryEnabled: true,
},
});
});
test("IAM role has no AdministratorAccess", () => {
// Đếm số policy statement, không có statement nào có Effect=Allow, Action=*
const roles = template.findResources("AWS::IAM::Role");
// ...custom logic...
});
```text
### Validate input: chặn config sai từ sớm
```typescript
class ApiService extends Construct {
constructor(scope: Construct, id: string, props: ApiServiceProps) {
super(scope, id);
// Validate input: memory phải từ 128-10240
if (props.memorySize && (props.memorySize < 128 || props.memorySize > 10240)) {
throw new Error(`Memory size must be between 128 and 10240, got ${props.memorySize}`);
}
// Validate: serviceName không chứa ký tự đặc biệt
if (!/^[a-z0-9-]+$/.test(props.serviceName)) {
throw new Error(`serviceName must be lowercase alphanumeric with hyphens, got "${props.serviceName}"`);
}
}
}
```text
---
## Best practices tổng hợp
### 1. RemovalPolicy: bảo vệ production data
```typescript
// S3 bucket production → RETAIN (không xóa khi destroy)
new s3.Bucket(this, "Data", {
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
// S3 bucket staging → DESTROY (cleanup được khi xóa stack)
new s3.Bucket(this, "Cache", {
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// Pattern: dùng context để quyết định
const isProd = this.node.tryGetContext("env") === "production";
new s3.Bucket(this, "Data", {
removalPolicy: isProd ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY,
});
```text
### 2. Escape hatches: khi L2 không đủ
```typescript
// CDK L2 không expose 1 property → dùng escape hatch
const bucket = new s3.Bucket(this, "SpecialBucket", { ... });
const cfnBucket = bucket.node.defaultChild as s3.CfnBucket;
cfnBucket.addPropertyOverride("VersioningConfiguration.Status", "Enabled");
cfnBucket.addPropertyOverride("OwnershipControls.Rules", [{
ObjectOwnership: "BucketOwnerEnforced",
}]);
```text
### 3. Security best practices checklist
```typescript
// ✅ Encryption at rest
const bucket = new s3.Bucket(this, "Data", {
encryption: s3.BucketEncryption.KMS,
encryptionKey: myKey,
enforceSSL: true, // ✅ Chặn HTTP
});
// ✅ Block public access
// Mặc định với L2 Bucket — kiểm tra nếu override
// ✅ Least privilege IAM
bucket.grantRead(handler); // ✅ Tự động tạo policy hẹp
// Thay vì: handler.addToRolePolicy(new iam.PolicyStatement({ actions: ["s3:*"], resources: ["*"] }))
// ✅ Lambda tracing
new lambda.Function(this, "Handler", {
tracing: lambda.Tracing.ACTIVE,
});
// ✅ Log retention
new lambda.Function(this, "Handler", {
logRetention: logs.RetentionDays.ONE_MONTH, // Không infinite
});
```text
### 4. cdk-nag: policy-as-code
```typescript
import { AwsSolutionsChecks, HIPAASecurityChecks, NIST80053R5Checks } from "cdk-nag";
const app = new cdk.App();
// Apply multiple compliance packs
cdk.Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));
// cdk.Aspects.of(app).add(new HIPAASecurityChecks({ verbose: true }));
// cdk-nag sẽ tự động báo lỗi/warning nếu:
// - S3 bucket không có encryption
// - S3 bucket có public read access
// - Lambda không có tracing
// - IAM policy dùng wildcard
// - CloudFront không enforce HTTPS
// - RDS không có backup
// - Và 100+ rules khác...
```text
---
## Tổng kết
- **CDK Pipelines**: CI/CD cho infrastructure — git push → test → staging → approval → production
- **Custom Construct**: đóng gói pattern team, tiết kiệm code, đảm bảo consistency
- **Snapshot testing**: phát hiện infrastructure drift, buộc review thay đổi
- **cdk-nag**: tự động kiểm tra security/compliance best practices (100+ rules)
- **RemovalPolicy.RETAIN** cho production data: không xóa S3/RDS khi destroy stack
- **Escape hatches**: L2 không expose → dùng `addPropertyOverride`
- **Validate input**: chặn config sai trong constructor, trước khi synth
Bài sau: [Phần 24: GitHub Actions + AWS OIDC — deploy không cần access key](/posts/aws/24-github-actions-aws-oidc-deploy/)