TypeScriptでAWSインフラをコードとして定義・デプロイする
AWS CDK (Cloud Development Kit) は、TypeScript・Python・Java・C# などの プログラミング言語を使ってAWSインフラを定義するフレームワークです。 コードからCloudFormationテンプレートを自動生成し、AWSにデプロイします。
| 特徴 | 説明 |
|---|---|
| プログラミング言語で記述 | TypeScript/Python等のループ・条件分岐・関数が使える |
| Constructsによる抽象化 | ベストプラクティスが組み込まれた高レベルコンポーネント |
| 型チェック・補完 | IDEのオートコンプリートでAWSリソースのプロパティを確認できる |
| CloudFormationへの変換 | 最終的にはCFnテンプレートが生成され、CloudFormationでデプロイ |
| テスト可能 | ユニットテスト・スナップショットテストが標準でサポート |
| 用語 | 説明 |
|---|---|
| App | CDKアプリのルート。複数のStackを含む |
| Stack | CloudFormationのスタックに対応。リソースの管理単位 |
| Construct | AWSリソースを表すビルディングブロック (L1/L2/L3) |
| Synth | CDKコードからCFnテンプレートを生成する処理 |
| Bootstrap | CDKデプロイに必要なS3バケット等をAWSアカウントに事前作成 |
CDKでは Construct という単位でリソースを定義します。3段階の抽象化レベルがあります。
// L1: CloudFormationのプロパティをそのまま記述
new ec2.CfnSecurityGroup(this, 'Sg', {
groupName: 'my-sg',
vpcId: vpc.ref,
groupDescription: 'Web SG',
securityGroupIngress: [{
ipProtocol: 'tcp',
fromPort: 80,
toPort: 80,
cidrIp: '0.0.0.0/0',
}],
});
// L2: 直感的なAPIで同じことができる
const sg = new ec2.SecurityGroup(this, 'Sg', {
vpc,
securityGroupName: 'my-sg',
description: 'Web SG',
});
sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80));
handson-cdk/
├── bin/
│ └── handson-cdk.ts ← App エントリーポイント
├── lib/
│ └── handson-cdk-stack.ts ← Stack 定義 (リソースをここに書く)
├── cdk.json ← CDK設定ファイル
├── package.json
├── tsconfig.json
└── cdk.out/ ← synth後に生成 (CFnテンプレート)
└── HandsonCdkStack.template.json
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { HandsonCdkStack } from '../lib/handson-cdk-stack';
const app = new cdk.App();
new HandsonCdkStack(app, 'HandsonCdkStack', {
env: { region: 'ap-northeast-1' },
});
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
export class HandsonCdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// VPC (Public subnetのみ、NAT Gatewayなし)
const vpc = new ec2.Vpc(this, 'Vpc', {
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
maxAzs: 2,
natGateways: 0,
subnetConfiguration: [{
cidrMask: 24,
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
}],
});
// Security Group (HTTP:80 許可)
const webSg = new ec2.SecurityGroup(this, 'WebSg', { vpc });
webSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80));
// IAM Role (Session Manager)
const ec2Role = new iam.Role(this, 'Ec2SsmRole', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
],
});
// EC2 インスタンス
const web1 = new ec2.Instance(this, 'WebServer1', {
vpc,
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
machineImage: ec2.MachineImage.latestAmazonLinux2023(),
securityGroup: webSg,
role: ec2Role,
userData: (() => {
const ud = ec2.UserData.forLinux();
ud.addCommands('dnf install -y nginx', 'systemctl enable nginx', 'systemctl start nginx');
return ud;
})(),
});
// Outputs
new cdk.CfnOutput(this, 'WebServer1Url', { value: `http://${web1.instancePublicIp}` });
}
}
| 項目 | CloudFormation | Terraform | AWS CDK |
|---|---|---|---|
| 記述言語 | YAML / JSON | HCL | TypeScript / Python 等 |
| 対応クラウド | AWSのみ | マルチクラウド | AWSのみ (cdktfでマルチ可) |
| 抽象化レベル | 低 (リソース直接定義) | 中 (宣言的) | 高 (L2/L3 Constructs) |
| ループ・条件分岐 | 制限あり (Conditions等) | for_each / count | プログラミング言語そのまま |
| State管理 | 不要 (CFnが管理) | terraform.tfstateファイル | 不要 (CFnスタックが管理) |
| デプロイ方式 | CFnスタック | tfコマンド | CDK → CFnスタック |
| テスト | 難しい | terraform validate | Jest等でユニットテスト可 |
| 学習コスト | 低〜中 | 中 | 中〜高 (プログラミング知識必要) |
| リソース | 名前/設定 | 補足 |
|---|---|---|
| VPC | 10.0.0.0/16 | CDKが自動的にIGW・ルートテーブルも作成 |
| Public Subnet × 2 | 10.0.0.0/24, 10.0.1.0/24 | ap-northeast-1a / 1c |
| Internet Gateway | 自動作成 | VPCにアタッチ済み |
| Route Table | 自動作成 | 0.0.0.0/0 → IGW |
| Security Group | handson-sg-web | Inbound: TCP80, Outbound: ALL |
| IAM Role | handson-ec2-ssm-role | SSM Session Manager用 |
| EC2 Instance × 2 | t2.micro, Amazon Linux 2023 | Nginx自動インストール |
new ec2.Vpc(this, 'Vpc', { ... }) と書くだけで、CDKは以下を自動生成します:
CFnやTerraformでは1つずつ定義が必要だったものが、L2 Constructでは数行になります。
node --version で確認)aws configure)インストール確認:
cdk --version※ CloudShellを使う場合は毎セッション実行が必要です
cdk init でプロジェクト雛形が生成されます。
bin/ と lib/ が自動作成されます。
CDKデプロイに必要なS3バケット (cdk-xxxxxxx-assets-*) と
IAMロールがアカウントに作成されます。同一アカウント・リージョンでは初回のみ必要です。
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
export class HandsonCdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const environmentName = new cdk.CfnParameter(this, 'EnvironmentName', {
type: 'String',
default: 'handson',
});
// VPC
const vpc = new ec2.Vpc(this, 'Vpc', {
vpcName: `${environmentName.valueAsString}-vpc`,
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
maxAzs: 2,
natGateways: 0,
subnetConfiguration: [{
cidrMask: 24,
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
}],
});
// Security Group
const webSg = new ec2.SecurityGroup(this, 'WebSg', {
vpc,
securityGroupName: `${environmentName.valueAsString}-sg-web`,
description: 'Web server security group',
allowAllOutbound: true,
});
webSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'HTTP from internet');
// IAM Role
const ec2Role = new iam.Role(this, 'Ec2SsmRole', {
roleName: `${environmentName.valueAsString}-ec2-ssm-role`,
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
],
});
const ami = ec2.MachineImage.latestAmazonLinux2023({
cpuType: ec2.AmazonLinuxCpuType.X86_64,
});
// EC2 WebServer 1 (1a)
const web1 = new ec2.Instance(this, 'WebServer1', {
instanceName: `${environmentName.valueAsString}-web-1a`,
vpc,
vpcSubnets: { availabilityZones: [`${cdk.Stack.of(this).region}a`], subnetType: ec2.SubnetType.PUBLIC },
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
machineImage: ami,
securityGroup: webSg,
role: ec2Role,
userData: (() => {
const ud = ec2.UserData.forLinux();
ud.addCommands(
'dnf update -y', 'dnf install -y nginx',
'systemctl enable nginx', 'systemctl start nginx',
`echo "handson - WebServer 1 (1a) - v1
" > /usr/share/nginx/html/index.html`
);
return ud;
})(),
});
cdk.Tags.of(web1).add('Version', 'v1');
// EC2 WebServer 2 (1c)
const web2 = new ec2.Instance(this, 'WebServer2', {
instanceName: `${environmentName.valueAsString}-web-1c`,
vpc,
vpcSubnets: { availabilityZones: [`${cdk.Stack.of(this).region}c`], subnetType: ec2.SubnetType.PUBLIC },
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
machineImage: ami,
securityGroup: webSg,
role: ec2Role,
userData: (() => {
const ud = ec2.UserData.forLinux();
ud.addCommands(
'dnf update -y', 'dnf install -y nginx',
'systemctl enable nginx', 'systemctl start nginx',
`echo "handson - WebServer 2 (1c) - v1
" > /usr/share/nginx/html/index.html`
);
return ud;
})(),
});
cdk.Tags.of(web2).add('Version', 'v1');
// Outputs
new cdk.CfnOutput(this, 'VpcId', { description: 'VPC ID', value: vpc.vpcId });
new cdk.CfnOutput(this, 'WebServer1Ip', { value: web1.instancePublicIp });
new cdk.CfnOutput(this, 'WebServer2Ip', { value: web2.instancePublicIp });
new cdk.CfnOutput(this, 'WebServer1Url', { value: `http://${web1.instancePublicIp}` });
new cdk.CfnOutput(this, 'WebServer2Url', { value: `http://${web2.instancePublicIp}` });
}
}
| コマンド | 説明 |
|---|---|
cdk synth | CDKコードをCFnテンプレートに変換して表示。実際のデプロイはしない |
cdk diff | 現在デプロイ中の状態と新しいコードの差分を表示 |
cdk deploy | スタックをAWSにデプロイ(内部でCFnを実行) |
cdk destroy | スタック全体を削除 |
cdk ls | プロジェクト内のスタック一覧を表示 |
CDKプロジェクトを初期化してWebサーバー環境を構築します。
以下のディレクトリ構成が生成されます:
handson-cdk/ ├── bin/handson-cdk.ts ← Appエントリーポイント ├── lib/handson-cdk-stack.ts ← Stackの定義 ├── cdk.json ├── package.json └── tsconfig.json
✅ Environment aws://123456789012/ap-northeast-1 bootstrapped.
AWSコンソール → CloudFormation で CDKToolkit スタックが作成されています。
エディタ(CloudShell内なら vi lib/handson-cdk-stack.ts)で編集してください。
bin/handson-cdk.ts はデフォルトのままで問題ありません。
長いYAMLが出力されます。cdk.out/HandsonCdkStack.template.json にも保存されます。
CDKコードに文法エラーがないか、どんなCFnリソースが生成されるかをデプロイ前に確認できます。
初回は全リソースが + 追加 として表示されます。
Stack HandsonCdkStack
Resources
[+] AWS::EC2::VPC Vpc VpcXXXXXX
[+] AWS::EC2::Subnet Vpc/PublicSubnet1/Subnet VpcPublicSubnet1SubnetXXXXXX
[+] AWS::EC2::InternetGateway Vpc/IGW VpcIGWXXXXXX
[+] AWS::EC2::SecurityGroup WebSg WebSgXXXXXX
[+] AWS::IAM::Role Ec2SsmRole Ec2SsmRoleXXXXXX
[+] AWS::EC2::Instance WebServer1 WebServer1XXXXXX
[+] AWS::EC2::Instance WebServer2 WebServer2XXXXXX
...
セキュリティ関連の変更確認が表示された場合は y で承認します。
Do you wish to deploy these changes (y/n)? y HandsonCdkStack: deploying... [1/1] HandsonCdkStack: creating CloudFormation changeset... ✅ HandsonCdkStack ✨ Deployment time: 120.3s Outputs: HandsonCdkStack.WebServer1Url = http://xxx.xxx.xxx.xxx HandsonCdkStack.WebServer2Url = http://xxx.xxx.xxx.xxx Stack ARN: arn:aws:cloudformation:ap-northeast-1:xxxxxxxxxxxx:stack/HandsonCdkStack/xxxxxxxx
WebServer1Url と WebServer2Url にブラウザでアクセスコードを変更してHTTPS(443)ポートを Security Group に追加し、Version タグを v2 に更新します。
Version タグを v1 → v2 に変更変更箇所1: Security Groupにルール追加
// 追加: HTTPS
webSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443), 'HTTPS from internet');
変更箇所2: Versionタグを v2 に変更 (web1, web2 両方)
// 変更前 cdk.Tags.of(web1).add('Version', 'v1'); cdk.Tags.of(web2).add('Version', 'v1'); // 変更後 cdk.Tags.of(web1).add('Version', 'v2'); cdk.Tags.of(web2).add('Version', 'v2');
Stack HandsonCdkStack
Security Group Changes
┌───┬────────────────────┬─────┬────────────┬──────────────────────┐
│ │ Group │ Dir │ Protocol │ Peer │
├───┼────────────────────┼─────┼────────────┼──────────────────────┤
│ + │ ${WebSg.GroupId} │ In │ TCP 443 │ Everyone (IPv4) │
└───┴────────────────────┴─────┴────────────┴──────────────────────┘
IAM Statement Changes (none)
Resources
[~] AWS::EC2::SecurityGroup WebSg WebSgXXXXXX
└─ [+] SecurityGroupIngress/1
├─ IpProtocol: tcp
├─ FromPort: 443
├─ ToPort: 443
└─ CidrIp: 0.0.0.0/0
[~] AWS::EC2::Instance WebServer1 WebServer1XXXXXX
└─ [~] Tags
└─ [~] .../Version: "v1" → "v2"
[~] AWS::EC2::Instance WebServer2 WebServer2XXXXXX
└─ [~] Tags
└─ [~] .../Version: "v1" → "v2"
Terraformと同様、実際の変更前に何が変わるかを確認できます。
完了後、CloudFormationコンソールの「リソース」タブで Security Group を確認し、443ポートが追加されていることを確認してください。
CDKが書いたTypeScriptが、どのようなCFn JSONに変換されているかを確認できます。
EC2インスタンスは稼働中に課金されます。ハンズオン終了後は必ず削除してください。
確認プロンプトが表示されます:
Are you sure you want to delete: HandsonCdkStack (y/n)? y HandsonCdkStack: destroying... [1/1] ✅ HandsonCdkStack: destroyed
CDKToolkit は同一アカウント・リージョンで今後もCDKを使う場合は残しておいてOKです。
削除する場合はCloudFormationコンソールから CDKToolkit スタックを手動削除してください。
(S3バケットが空でないと削除できない場合があります。バケット内のオブジェクトを先に空にしてください。)
ec2.Vpc 1つでIGW・ルートテーブルも自動作成import { Template } from 'aws-cdk-lib/assertions';
import { HandsonCdkStack } from '../lib/handson-cdk-stack';
import * as cdk from 'aws-cdk-lib';
test('Security Group has HTTP ingress', () => {
const app = new cdk.App();
const stack = new HandsonCdkStack(app, 'TestStack');
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::EC2::SecurityGroup', {
SecurityGroupIngress: [{ IpProtocol: 'tcp', FromPort: 80, ToPort: 80 }],
});
});
npm test で実行できます。インフラの設定ミスをCI/CDで自動検出できます。
const app = new cdk.App();
new HandsonCdkStack(app, 'DevStack', {
env: { account: '111111111111', region: 'ap-northeast-1' },
environmentName: 'dev',
});
new HandsonCdkStack(app, 'ProdStack', {
env: { account: '222222222222', region: 'ap-northeast-1' },
environmentName: 'prod',
});
cdk deploy DevStack / cdk deploy ProdStack で個別デプロイできます。