AWS CDK ハンズオン

TypeScriptでAWSインフラをコードとして定義・デプロイする

AWS CDK v2 TypeScript VPC + EC2×2 v1.0

AWS CDK とは

AWS CDK (Cloud Development Kit) は、TypeScript・Python・Java・C# などの プログラミング言語を使ってAWSインフラを定義するフレームワークです。 コードからCloudFormationテンプレートを自動生成し、AWSにデプロイします。

CDKの特徴

特徴説明
プログラミング言語で記述TypeScript/Python等のループ・条件分岐・関数が使える
Constructsによる抽象化ベストプラクティスが組み込まれた高レベルコンポーネント
型チェック・補完IDEのオートコンプリートでAWSリソースのプロパティを確認できる
CloudFormationへの変換最終的にはCFnテンプレートが生成され、CloudFormationでデプロイ
テスト可能ユニットテスト・スナップショットテストが標準でサポート

CDKの基本概念

用語説明
AppCDKアプリのルート。複数のStackを含む
StackCloudFormationのスタックに対応。リソースの管理単位
ConstructAWSリソースを表すビルディングブロック (L1/L2/L3)
SynthCDKコードからCFnテンプレートを生成する処理
BootstrapCDKデプロイに必要なS3バケット等をAWSアカウントに事前作成

CDKのデプロイフロー

TypeScript コード
lib/handson-cdk-stack.ts
↓ cdk synth
CloudFormation テンプレート (JSON/YAML)
cdk.out/ に自動生成
↓ cdk deploy
CloudFormation スタック → AWSリソース作成

Constructs の 3 つのレベル

CDKでは Construct という単位でリソースを定義します。3段階の抽象化レベルがあります。

L1
Cfn Constructs
CloudFormationリソースと1対1対応。プロパティ名もCFnと同じ。
CfnVPC
CfnInstance
CfnSecurityGroup
L2
AWS Constructs
L1をラップしたベストプラクティス込みの使いやすいAPI。最もよく使う。
ec2.Vpc
ec2.Instance
iam.Role
L3
Patterns
複数リソースをまとめたパターン。WebサーバーやAPI Gateway+Lambda等。
ecs_patterns
.ApplicationLoadBalancedFargateService

L1 vs L2 の比較例 (EC2セキュリティグループ)

L1 (CFn直接)

// 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 (推奨)

// 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

今回作成するコード (抜粋)

bin/handson-cdk.ts (Appエントリーポイント)

#!/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' },
});

lib/handson-cdk-stack.ts (Stackの核心部分)

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 / CDK 比較

項目 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等でユニットテスト可
学習コスト 低〜中 中〜高 (プログラミング知識必要)
CDKを選ぶ理由
  • プログラマーにとって馴染みのある言語でインフラを書ける
  • IDEの型補完でAWSリソースのプロパティを確認しながら書ける
  • ループや関数でDRYなコードが書ける(例: 10台のEC2を1ループで定義)
  • ユニットテストでリソースの設定を自動検証できる

今回構築するアーキテクチャ

VPC (10.0.0.0/16)
Public Subnet (1a)
10.0.0.0/24
EC2 WebServer 1
Public Subnet (1c)
10.0.1.0/24
EC2 WebServer 2
Internet Gateway (CDKが自動作成)
Security Group: TCP:80 from 0.0.0.0/0
IAM Role: AmazonSSMManagedInstanceCore (Session Manager接続用)

作成されるリソース一覧

リソース名前/設定補足
VPC10.0.0.0/16CDKが自動的にIGW・ルートテーブルも作成
Public Subnet × 210.0.0.0/24, 10.0.1.0/24ap-northeast-1a / 1c
Internet Gateway自動作成VPCにアタッチ済み
Route Table自動作成0.0.0.0/0 → IGW
Security Grouphandson-sg-webInbound: TCP80, Outbound: ALL
IAM Rolehandson-ec2-ssm-roleSSM Session Manager用
EC2 Instance × 2t2.micro, Amazon Linux 2023Nginx自動インストール
CDKのL2 VPC Constructが自動で作るもの

new ec2.Vpc(this, 'Vpc', { ... }) と書くだけで、CDKは以下を自動生成します:

  • VPC本体
  • Internet Gateway + VPCへのアタッチ
  • 各サブネット
  • ルートテーブル + サブネットへの関連付け
  • デフォルトルート (0.0.0.0/0 → IGW)

CFnやTerraformでは1つずつ定義が必要だったものが、L2 Constructでは数行になります。

環境セットアップ

必要なもの

Step 1: AWS CDK CLI のインストール

CDK CLI をグローバルインストール
npm install -g aws-cdk

インストール確認:

cdk --version

※ CloudShellを使う場合は毎セッション実行が必要です

Step 2: プロジェクトの作成

CDKプロジェクトを初期化
mkdir handson-cdk && cd handson-cdk cdk init app --language typescript

cdk init でプロジェクト雛形が生成されます。
bin/lib/ が自動作成されます。

Step 3: CDK Bootstrap (初回のみ)

AWSアカウントにCDKデプロイ基盤を作成
cdk bootstrap aws://<AWSアカウントID>/ap-northeast-1

CDKデプロイに必要なS3バケット (cdk-xxxxxxx-assets-*) と IAMロールがアカウントに作成されます。同一アカウント・リージョンでは初回のみ必要です。

Step 4: スタックファイルの編集

lib/handson-cdk-stack.ts を次のコードで置き換える
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コマンドの流れ

cdk init コード編集 cdk synth cdk diff cdk deploy cdk destroy
← コード編集 → cdk diff → cdk deploy を繰り返して変更を適用 →
コマンド説明
cdk synthCDKコードをCFnテンプレートに変換して表示。実際のデプロイはしない
cdk diff現在デプロイ中の状態と新しいコードの差分を表示
cdk deployスタックをAWSにデプロイ(内部でCFnを実行)
cdk destroyスタック全体を削除
cdk lsプロジェクト内のスタック一覧を表示

ハンズオン①: 初回デプロイ

CDKプロジェクトを初期化してWebサーバー環境を構築します。

Step 1: プロジェクト初期化

CloudShell or ローカルで実行
# CDK CLIインストール(CloudShellの場合は毎回必要) npm install -g aws-cdk # プロジェクト作成 mkdir handson-cdk && cd handson-cdk cdk init app --language typescript

以下のディレクトリ構成が生成されます:

handson-cdk/
├── bin/handson-cdk.ts      ← Appエントリーポイント
├── lib/handson-cdk-stack.ts ← Stackの定義
├── cdk.json
├── package.json
└── tsconfig.json

Step 2: Bootstrap (初回のみ)

CDK デプロイ基盤の作成
# アカウントIDとリージョンを確認 aws sts get-caller-identity --query Account --output text aws configure get region # Bootstrap実行(<アカウントID>を実際の値に変更) cdk bootstrap aws://<アカウントID>/ap-northeast-1
成功時の出力
✅ Environment aws://123456789012/ap-northeast-1 bootstrapped.

AWSコンソール → CloudFormation で CDKToolkit スタックが作成されています。

Step 3: Stackコードの編集

lib/handson-cdk-stack.ts を「環境セットアップ Step 4」のコードで置き換える

エディタ(CloudShell内なら vi lib/handson-cdk-stack.ts)で編集してください。

bin/handson-cdk.ts はデフォルトのままで問題ありません。

Step 4: Synthesize (コードのテンプレート変換)

CDKコードをCFnテンプレートに変換して確認
cdk synth

長いYAMLが出力されます。cdk.out/HandsonCdkStack.template.json にも保存されます。

synthで確認できること

CDKコードに文法エラーがないか、どんなCFnリソースが生成されるかをデプロイ前に確認できます。

Step 5: Diff (差分確認)

現在の状態(未デプロイ)との差分を確認
cdk diff

初回は全リソースが + 追加 として表示されます。

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
...

Step 6: Deploy

スタックをAWSにデプロイ
cdk deploy

セキュリティ関連の変更確認が表示された場合は 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
確認ポイント
  • 出力された WebServer1UrlWebServer2Url にブラウザでアクセス
  • 「handson - WebServer 1 (1a) - v1」が表示されれば成功
  • AWSコンソール → CloudFormation → HandsonCdkStack でリソース一覧を確認

Step 7: デプロイ結果の確認

CloudFormationコンソールで確認
  1. AWSコンソール → CloudFormation → 「HandsonCdkStack」をクリック
  2. 「リソース」タブ → 作成されたリソース一覧を確認
  3. 「出力」タブ → WebServer1Url, WebServer2Url を確認
  4. 「イベント」タブ → 作成順序を確認

ハンズオン②: インフラの変更

コードを変更してHTTPS(443)ポートを Security Group に追加し、Version タグを v2 に更新します。

変更内容

Step 1: コードを編集

lib/handson-cdk-stack.ts を編集

変更箇所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');

Step 2: 差分を確認

cdk diff で変更内容を確認
cdk diff
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と同様、実際の変更前に何が変わるかを確認できます。

Step 3: デプロイ

変更を適用
cdk deploy

完了後、CloudFormationコンソールの「リソース」タブで Security Group を確認し、443ポートが追加されていることを確認してください。

補足: cdk synth で生成されたCFnテンプレートを確認

生成されたCloudFormationテンプレートを直接見てみる
cat cdk.out/HandsonCdkStack.template.json | head -100

CDKが書いたTypeScriptが、どのようなCFn JSONに変換されているかを確認できます。

ハンズオン③: スタックの削除

課金停止のためにリソースを必ず削除してください

EC2インスタンスは稼働中に課金されます。ハンズオン終了後は必ず削除してください。

Step 1: destroy コマンドで全削除

スタック全体を削除
cdk destroy

確認プロンプトが表示されます:

Are you sure you want to delete: HandsonCdkStack (y/n)? y
HandsonCdkStack: destroying... [1/1]
✅  HandsonCdkStack: destroyed

Step 2: 削除の確認

CloudFormationコンソールで確認
  1. AWSコンソール → CloudFormation
  2. 「HandsonCdkStack」が表示されないこと(または DELETE_COMPLETE ステータス)を確認
  3. EC2コンソール → インスタンス → terminated 状態になっていることを確認
  4. VPCコンソール → 作成したVPCが削除されていることを確認

Step 3: CDKToolkit スタック (Bootstrap) の削除 (任意)

不要な場合はBootstrap基盤も削除
注意

CDKToolkit は同一アカウント・リージョンで今後もCDKを使う場合は残しておいてOKです。

削除する場合はCloudFormationコンソールから CDKToolkit スタックを手動削除してください。

(S3バケットが空でないと削除できない場合があります。バケット内のオブジェクトを先に空にしてください。)

CDK の特長まとめ

今回のハンズオンで体験したこと
  • コードでインフラ定義: TypeScriptの型補完を使いながらAWSリソースを記述できた
  • L2 Constructの便利さ: ec2.Vpc 1つでIGW・ルートテーブルも自動作成
  • cdk diff: 変更前に差分を確認してから適用できる安全なワークフロー
  • cdk destroy: 1コマンドで全リソースをクリーンに削除

発展: CDK のユニットテスト

インフラのテストを書ける (CDKの大きな強み)
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で自動検出できます。

発展: 複数環境へのデプロイ

bin/handson-cdk.ts でDev/Prod環境を分ける
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 で個別デプロイできます。