AWS CDKは、プログラミング言語でインフラを定義できるフレームワークです。本記事では、IaC比較記事で触れなかった実践的な使い方を解説します。
Constructの深掘り
Constructレベル
flowchart TB
subgraph Levels["Constructレベル"]
L1["L1: CFn Resources<br/>CfnXxx"]
L2["L2: AWS Constructs<br/>高レベル抽象化"]
L3["L3: Patterns<br/>複合パターン"]
end
L1 --> |"1:1マッピング"| CFn["CloudFormation"]
L2 --> |"スマートデフォルト"| L1
L3 --> |"複数リソース"| L2
style L2 fill:#22c55e,color:#fff
style L3 fill:#3b82f6,color:#fff
L1 Construct(Cfn)
import * as ec2 from 'aws-cdk-lib/aws-ec2';
// L1: CloudFormationリソースと1:1対応
const cfnVpc = new ec2.CfnVPC(this, 'CfnVpc', {
cidrBlock: '10.0.0.0/16',
enableDnsHostnames: true,
enableDnsSupport: true,
tags: [{ key: 'Name', value: 'my-vpc' }],
});
// すべてのプロパティを手動で設定
const cfnSubnet = new ec2.CfnSubnet(this, 'CfnSubnet', {
vpcId: cfnVpc.ref,
cidrBlock: '10.0.1.0/24',
availabilityZone: 'ap-northeast-1a',
});
L2 Construct(推奨)
import * as ec2 from 'aws-cdk-lib/aws-ec2';
// L2: スマートデフォルト付き
const vpc = new ec2.Vpc(this, 'Vpc', {
maxAzs: 2,
natGateways: 1,
subnetConfiguration: [
{
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
cidrMask: 24,
},
{
name: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
cidrMask: 24,
},
],
});
// 自動的に以下が作成される:
// - VPC
// - パブリック/プライベートサブネット(各AZ)
// - インターネットゲートウェイ
// - NATゲートウェイ
// - ルートテーブル
L3 Construct(パターン)
import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns';
// L3: 複数リソースを含むパターン
const service = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'Service', {
vpc,
taskImageOptions: {
image: ecs.ContainerImage.fromRegistry('nginx'),
containerPort: 80,
},
desiredCount: 2,
publicLoadBalancer: true,
});
// 自動的に以下が作成される:
// - ECSクラスター
// - Fargateサービス
// - タスク定義
// - ALB
// - ターゲットグループ
// - セキュリティグループ
カスタムConstruct
再利用可能なConstruct
import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
export interface DatabaseConstructProps {
vpc: ec2.IVpc;
instanceType?: ec2.InstanceType;
multiAz?: boolean;
backupRetention?: cdk.Duration;
}
export class DatabaseConstruct extends Construct {
public readonly instance: rds.DatabaseInstance;
public readonly secret: secretsmanager.ISecret;
constructor(scope: Construct, id: string, props: DatabaseConstructProps) {
super(scope, id);
const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', {
vpc: props.vpc,
description: 'Database security group',
});
this.instance = new rds.DatabaseInstance(this, 'Instance', {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_15,
}),
vpc: props.vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
instanceType: props.instanceType ?? ec2.InstanceType.of(
ec2.InstanceClass.T3,
ec2.InstanceSize.MEDIUM
),
multiAz: props.multiAz ?? false,
backupRetention: props.backupRetention ?? cdk.Duration.days(7),
securityGroups: [securityGroup],
deletionProtection: true,
credentials: rds.Credentials.fromGeneratedSecret('dbadmin'),
});
this.secret = this.instance.secret!;
}
public allowConnectionFrom(peer: ec2.IPeer, port: ec2.Port = ec2.Port.tcp(5432)) {
this.instance.connections.allowFrom(peer, port);
}
}
使用例
const database = new DatabaseConstruct(this, 'Database', {
vpc,
multiAz: true,
backupRetention: cdk.Duration.days(14),
});
database.allowConnectionFrom(appSecurityGroup);
// 出力
new cdk.CfnOutput(this, 'DatabaseEndpoint', {
value: database.instance.dbInstanceEndpointAddress,
});
CDKテスト
テストの種類
flowchart LR
subgraph Testing["CDKテスト"]
Snapshot["スナップショットテスト"]
FineGrained["詳細アサーション"]
Validation["バリデーション"]
end
Snapshot --> |"回帰テスト"| Changes["変更検出"]
FineGrained --> |"リソース確認"| Resources["リソース検証"]
Validation --> |"入力検証"| Input["プロパティ検証"]
style Testing fill:#3b82f6,color:#fff
スナップショットテスト
import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { MyStack } from '../lib/my-stack';
describe('MyStack', () => {
test('matches snapshot', () => {
const app = new cdk.App();
const stack = new MyStack(app, 'TestStack');
const template = Template.fromStack(stack);
expect(template.toJSON()).toMatchSnapshot();
});
});
詳細アサーション
import { Template, Match, Capture } from 'aws-cdk-lib/assertions';
describe('MyStack', () => {
let template: Template;
beforeEach(() => {
const app = new cdk.App();
const stack = new MyStack(app, 'TestStack');
template = Template.fromStack(stack);
});
test('creates VPC with correct CIDR', () => {
template.hasResourceProperties('AWS::EC2::VPC', {
CidrBlock: '10.0.0.0/16',
EnableDnsHostnames: true,
});
});
test('creates exactly 2 private subnets', () => {
template.resourceCountIs('AWS::EC2::Subnet', 4); // 2 public + 2 private
});
test('Lambda function has correct environment', () => {
template.hasResourceProperties('AWS::Lambda::Function', {
Environment: {
Variables: {
TABLE_NAME: Match.anyValue(),
LOG_LEVEL: 'INFO',
},
},
Runtime: 'nodejs18.x',
Timeout: 30,
});
});
test('RDS instance has encryption enabled', () => {
template.hasResourceProperties('AWS::RDS::DBInstance', {
StorageEncrypted: true,
DeletionProtection: true,
});
});
// キャプチャを使用した検証
test('security group allows correct ingress', () => {
const portCapture = new Capture();
template.hasResourceProperties('AWS::EC2::SecurityGroupIngress', {
IpProtocol: 'tcp',
FromPort: portCapture,
ToPort: portCapture,
});
expect(portCapture.asNumber()).toBe(443);
});
});
バリデーションテスト
describe('DatabaseConstruct validation', () => {
test('throws error for invalid retention period', () => {
const app = new cdk.App();
const stack = new cdk.Stack(app, 'TestStack');
const vpc = new ec2.Vpc(stack, 'Vpc');
expect(() => {
new DatabaseConstruct(stack, 'Database', {
vpc,
backupRetention: cdk.Duration.days(0), // Invalid
});
}).toThrow(/Backup retention must be at least 1 day/);
});
});
Aspects
Aspectsの仕組み
flowchart TB
subgraph Aspects["Aspects"]
Define["Aspect定義"]
Apply["適用"]
Visit["全ノード訪問"]
end
Define --> Apply
Apply --> Visit
Visit --> |"条件に応じて"| Modify["変更/検証"]
style Aspects fill:#f59e0b,color:#000
タグ付けAspect
import { IAspect, Tags } from 'aws-cdk-lib';
import { IConstruct } from 'constructs';
class TaggingAspect implements IAspect {
constructor(private readonly tags: Record<string, string>) {}
visit(node: IConstruct): void {
if (Tags.of(node)) {
Object.entries(this.tags).forEach(([key, value]) => {
Tags.of(node).add(key, value);
});
}
}
}
// 使用例
const app = new cdk.App();
const stack = new MyStack(app, 'MyStack');
Aspects.of(stack).add(new TaggingAspect({
Environment: 'production',
Project: 'my-app',
ManagedBy: 'CDK',
}));
セキュリティ検証Aspect
import { IAspect, Annotations } from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as rds from 'aws-cdk-lib/aws-rds';
class SecurityValidationAspect implements IAspect {
visit(node: IConstruct): void {
// S3バケットの暗号化チェック
if (node instanceof s3.CfnBucket) {
if (!node.bucketEncryption) {
Annotations.of(node).addError('S3 bucket must have encryption enabled');
}
}
// RDSインスタンスの暗号化チェック
if (node instanceof rds.CfnDBInstance) {
if (node.storageEncrypted !== true) {
Annotations.of(node).addError('RDS instance must have storage encryption enabled');
}
}
// セキュリティグループの0.0.0.0/0チェック
if (node instanceof ec2.CfnSecurityGroup) {
const ingress = node.securityGroupIngress as any[];
if (ingress?.some(rule => rule.cidrIp === '0.0.0.0/0' && rule.ipProtocol !== 'icmp')) {
Annotations.of(node).addWarning('Security group allows inbound traffic from 0.0.0.0/0');
}
}
}
}
Aspects.of(app).add(new SecurityValidationAspect());
コスト最適化Aspect
class CostOptimizationAspect implements IAspect {
visit(node: IConstruct): void {
// Lambda関数のメモリ警告
if (node instanceof lambda.CfnFunction) {
const memory = node.memorySize ?? 128;
if (memory > 1024) {
Annotations.of(node).addInfo(
`Lambda function has ${memory}MB memory. Consider if this is necessary.`
);
}
}
// NAT Gatewayの数チェック
if (node instanceof ec2.CfnNatGateway) {
Annotations.of(node).addInfo(
'NAT Gateway incurs hourly charges. Consider NAT Instance for dev environments.'
);
}
}
}
CDK Pipelines
セルフミューテーションパイプライン
flowchart LR
subgraph Pipeline["CDK Pipeline"]
Source["ソース"]
Synth["Synth"]
SelfMutate["セルフミューテーション"]
Deploy["Deployment"]
end
Source --> Synth
Synth --> SelfMutate
SelfMutate --> |"パイプライン更新"| Deploy
style Pipeline fill:#3b82f6,color:#fff
パイプライン定義
import * as cdk from 'aws-cdk-lib';
import { CodePipeline, CodePipelineSource, ShellStep } from 'aws-cdk-lib/pipelines';
export class PipelineStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const pipeline = new CodePipeline(this, 'Pipeline', {
pipelineName: 'MyAppPipeline',
synth: new ShellStep('Synth', {
input: CodePipelineSource.gitHub('owner/repo', 'main', {
authentication: cdk.SecretValue.secretsManager('github-token'),
}),
commands: [
'npm ci',
'npm run build',
'npm run test',
'npx cdk synth',
],
primaryOutputDirectory: 'cdk.out',
}),
selfMutation: true,
dockerEnabledForSynth: true,
});
// ステージング環境
const staging = new MyAppStage(this, 'Staging', {
env: { account: '111111111111', region: 'ap-northeast-1' },
});
pipeline.addStage(staging, {
pre: [
new ShellStep('Validate', {
commands: ['npm run lint', 'npm run test:integration'],
}),
],
post: [
new ShellStep('IntegrationTest', {
commands: ['npm run test:e2e'],
envFromCfnOutputs: {
API_URL: staging.apiUrlOutput,
},
}),
],
});
// 本番環境
const production = new MyAppStage(this, 'Production', {
env: { account: '222222222222', region: 'ap-northeast-1' },
});
pipeline.addStage(production, {
pre: [
new pipelines.ManualApprovalStep('PromoteToProd', {
comment: 'Approve deployment to production',
}),
],
});
}
}
ステージ定義
import { Stage, StageProps, CfnOutput } from 'aws-cdk-lib';
export class MyAppStage extends Stage {
public readonly apiUrlOutput: CfnOutput;
constructor(scope: Construct, id: string, props?: StageProps) {
super(scope, id, props);
const apiStack = new ApiStack(this, 'Api');
const dbStack = new DatabaseStack(this, 'Database');
const appStack = new AppStack(this, 'App', {
api: apiStack,
database: dbStack,
});
this.apiUrlOutput = appStack.apiUrl;
}
}
コンテキストと環境
コンテキスト
// cdk.json
{
"context": {
"environments": {
"dev": {
"account": "111111111111",
"region": "ap-northeast-1",
"instanceType": "t3.small"
},
"prod": {
"account": "222222222222",
"region": "ap-northeast-1",
"instanceType": "m5.large"
}
}
}
}
// スタックでの使用
const envConfig = this.node.tryGetContext('environments')[props.stage];
const instanceType = ec2.InstanceType.of(
ec2.InstanceClass.of(envConfig.instanceType.split('.')[0]),
ec2.InstanceSize.of(envConfig.instanceType.split('.')[1])
);
環境ごとの設定
interface EnvironmentConfig {
env: cdk.Environment;
instanceType: ec2.InstanceType;
minCapacity: number;
maxCapacity: number;
}
const environments: Record<string, EnvironmentConfig> = {
dev: {
env: { account: '111111111111', region: 'ap-northeast-1' },
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.SMALL),
minCapacity: 1,
maxCapacity: 2,
},
prod: {
env: { account: '222222222222', region: 'ap-northeast-1' },
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE),
minCapacity: 2,
maxCapacity: 10,
},
};
const app = new cdk.App();
const stage = app.node.tryGetContext('stage') || 'dev';
const config = environments[stage];
new MyStack(app, `MyStack-${stage}`, config);
ベストプラクティス
flowchart TB
subgraph BestPractices["Best Practices"]
L2["L2 Constructを優先"]
Test["テストを書く"]
Aspect["Aspectsで横断的関心事"]
Pipeline["CDK Pipelinesで自動化"]
end
style BestPractices fill:#22c55e,color:#fff
| カテゴリ | 項目 |
|---|---|
| 設計 | L2 Constructを優先使用 |
| テスト | スナップショット+詳細アサーション |
| セキュリティ | Aspectsで検証 |
| 運用 | CDK Pipelinesでセルフミューテーション |
まとめ
| 機能 | 用途 |
|---|---|
| カスタムConstruct | 再利用可能なコンポーネント |
| テスト | 品質保証 |
| Aspects | 横断的な設定・検証 |
| CDK Pipelines | CI/CD自動化 |
CDKの高度な機能を活用することで、保守性の高いインフラコードを実現できます。