書籍「AWSではじめるインフラ構築入門 安全で堅牢な本番環境のつくり方」の４章部分をCDK化した

# はじめに

社内でWeb開発をしているものの、インフラについてはインフラチームに頼りっきり。

このままではいかんと思い[AWSではじめるインフラ構築入門 安全で堅牢な本番環境のつくり方](https://www.amazon.co.jp/AWS%E3%81%A7%E3%81%AF%E3%81%98%E3%82%81%E3%82%8B%E3%82%A4%E3%83%B3%E3%83%95%E3%83%A9%E6%A7%8B%E7%AF%89%E5%85%A5%E9%96%80-%E5%AE%89%E5%85%A8%E3%81%A7%E5%A0%85%E7%89%A2%E3%81%AA%E6%9C%AC%E7%95%AA%E7%92%B0%E5%A2%83%E3%81%AE%E3%81%A4%E3%81%8F%E3%82%8A%E6%96%B9-%E4%B8%AD%E5%9E%A3-%E5%81%A5%E5%BF%97/dp/4798163430/ref=sr_1_1?adgrpid=131402262685&gclid=CjwKCAjw4c-ZBhAEEiwAZ105Rfx0qUnlW1Q6tS4-OcELhdZ39fQi6I--rEweAPSAlYC3I6ePq3jp8hoC_fYQAvD_BwE&hvadid=611261911665&hvdev=c&hvlocphy=1009717&hvnetw=g&hvqmt=e&hvrand=11156037430862966959&hvtargid=kwd-1208225358644&hydadcr=4078_13255669&jp-ad-ap=0&keywords=aws%E3%81%A7%E3%81%AF%E3%81%98%E3%82%81%E3%82%8B%E3%82%A4%E3%83%B3%E3%83%95%E3%83%A9%E6%A7%8B%E7%AF%89%E5%85%A5%E9%96%80&qid=1664432456&qu=eyJxc2MiOiIxLjM1IiwicXNhIjoiMS4wMyIsInFzcCI6IjEuMDgifQ%3D%3D&s=books&sr=1-1) でAWSについて勉強することにした。

…が書籍の４章「仮想ネットワークを作ろう」でVPCをうまく構築できず、放置気味に。

特に以下の点で苦労した。

- AWSコンソールのレイアウトが書籍と違う部分がある
- そもそもインフラ知識が無いので何してるかわからない
- 設定ミスしやすい
- 「よく分からなくなった。一度全部消してやり直そう」が大変
- 作ったリソース間の関係性を把握しにくい

# 解決策

[AWS CDK](https://docs.aws.amazon.com/cdk/api/v2/) でVPCを作成することにした。

CDKに触れたことはなかったが今後を考えると覚えておいた方が良いだろうと。運よくTypeScriptには慣れている。

# 前提条件

- AWS CDK　v2.43.1 を使用
- 言語はTypeScript
- [Getting started with the AWS CDK](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/getting_started.html) でCDKの環境が構築済みであること（Bootstrappingまで行う）
CDKの環境構築のためにはPython、AWSアカウント、AWS CLIが必要になりますがここでは割愛します。

また、書籍の手順とコードを対応させるためAWS CDKのL1のみを使います。(初学者はL1で始めたほうがわかりやすいと思う)
例えばサブネットを作る際にL1([ec2.CfnSubnet](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.CfnSubnet.html))ではなくL2([ec2.PrivateSubnet](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.PrivateSubnet.html), [ec2.PublicSubnet](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.PublicSubnet.html))で作ると一緒にルートテーブルも作成されてしまう。ちょっと混乱した。

参考: [aws-cdkのL1・L2・L3とは aws-cdk#3](https://qiita.com/KbSota/items/be3b4063deea52815d50)

# ４章「仮想ネットワークを作ろう」のCDK

[Your first AWS CDK app](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/hello_world.html)の「Create the app」まで進めます。

`lib/hello-cdk-stack.ts`を以下のコードに置き換えます。

```typescript:lib/hello-cdk-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';

export class HelloCdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ---------- 4.2 VPC ----------
    const vpc = new ec2.CfnVPC(this, 'CfnVPC1', {
      cidrBlock: '10.0.0.0/16', // 後で変更「可」
      tags: [{ key: 'Name', value: 'sample-vpc' }],
    });

    // ---------- 4.3 サブネットとアベイラビリティーゾーン ----------
    // 外部サブネット
    const publicSubnet1 = new ec2.CfnSubnet(this, 'CfnSubnet1', {
      availabilityZone: 'ap-northeast-1a',
      vpcId: vpc.ref,
      cidrBlock: '10.0.0.0/20', // 後で変更不可
      tags: [{ key: 'Name', value: 'sample-subnet-public01' }],
    });
    const publicSubnet2 = new ec2.CfnSubnet(this, 'CfnSubnet2', {
      availabilityZone: 'ap-northeast-1c',
      vpcId: vpc.ref,
      cidrBlock: '10.0.16.0/20', // 後で変更不可
      tags: [{ key: 'Name', value: 'sample-subnet-public02' }],
    });

    // 内部サブネット
    const privateSubnet1 = new ec2.CfnSubnet(this, 'CfnSubnet3', {
      availabilityZone: 'ap-northeast-1a',
      vpcId: vpc.ref,
      cidrBlock: '10.0.64.0/20', // 後で変更不可
      tags: [{ key: 'Name', value: 'sample-subnet-private01' }],
    });
    const privateSubnet2 = new ec2.CfnSubnet(this, 'CfnSubnet4', {
      availabilityZone: 'ap-northeast-1c',
      vpcId: vpc.ref,
      cidrBlock: '10.0.80.0/20', // 後で変更不可
      tags: [{ key: 'Name', value: 'sample-subnet-private02' }],
    });

    // ---------- 4.4 インターネットゲートウェイ ----------
    const igw = new ec2.CfnInternetGateway(this, 'CfnInternetGateway1', {
      tags: [{ key: 'Name', value: 'sample-igw' }],
    });
    // 作成したゲートウェイとvpcを紐付ける
    new ec2.CfnVPCGatewayAttachment(this, 'CfnVPCGatewayAttachment1', {
      vpcId: vpc.ref,
      internetGatewayId: igw.ref,
    })

    // ---------- 4.5 NATゲートウェイ ----------
    // Elastic IP (NATゲートウェイに割り当てるため、ここで作成)
    const eip1 = new ec2.CfnEIP(this, 'CfnEIP1', {
      tags: [{ key: 'Name', value: 'sample-eip-01' }],
    });
    const eip2 = new ec2.CfnEIP(this, 'CfnEIP2', {
      tags: [{ key: 'Name', value: 'sample-eip-02' }],
    });

    const ngw1 = new ec2.CfnNatGateway(this, 'CfnNatGateway1', {
      allocationId: eip1.attrAllocationId,
      subnetId: publicSubnet1.ref,
      tags: [{ key: 'Name', value: 'sample-ngw-01' }],
    })
    const ngw2 = new ec2.CfnNatGateway(this, 'CfnNatGateway2', {
      allocationId: eip2.attrAllocationId,
      subnetId: publicSubnet2.ref,
      tags: [{ key: 'Name', value: 'sample-ngw-02' }],
    })

    // ---------- 4.6 ルートテーブル ----------
    // VPC内の他のリソースが送信先(10.0.0.0/16)の時にlocalをターゲットとして使う設定は既に作られている
    // sample-rt-public
    const publicRouteTable = new ec2.CfnRouteTable(this, 'CfnRouteTable1', {
      vpcId: vpc.ref,
      tags: [{ key: 'Name', value: 'sample-rt-public' }],
    });
    new ec2.CfnRoute(this, 'CfnRoute1', {
      routeTableId: publicRouteTable.ref,
      destinationCidrBlock: '0.0.0.0/0',
      gatewayId: igw.ref,
    });
    // sample-rt-private01
    const privateRouteTable1 = new ec2.CfnRouteTable(this, 'CfnRouteTable2', {
      vpcId: vpc.ref,
      tags: [{ key: 'Name', value: 'sample-rt-private01' }],
    });
    new ec2.CfnRoute(this, 'CfnRoute2', {
      routeTableId: privateRouteTable1.ref,
      destinationCidrBlock: '0.0.0.0/0',
      natGatewayId: ngw1.ref,
    });
    // sample-rt-private02
    const privateRouteTable2 = new ec2.CfnRouteTable(this, 'CfnRouteTable3', {
      vpcId: vpc.ref,
      tags: [{ key: 'Name', value: 'sample-rt-private02' }],
    });
    new ec2.CfnRoute(this, 'CfnRoute3', {
      routeTableId: privateRouteTable2.ref,
      destinationCidrBlock: '0.0.0.0/0',
      natGatewayId: ngw2.ref,
    });

    // ルートテーブルの関連付け
    // sample-rt-public
    new ec2.CfnSubnetRouteTableAssociation(this, 'CfnSubnetRouteTableAssociation1', {
      routeTableId: publicRouteTable.ref,
      subnetId: publicSubnet1.ref,
    });
    new ec2.CfnSubnetRouteTableAssociation(this, 'CfnSubnetRouteTableAssociation2', {
      routeTableId: publicRouteTable.ref,
      subnetId: publicSubnet2.ref,
    });
    // sample-rt-private01
    new ec2.CfnSubnetRouteTableAssociation(this, 'CfnSubnetRouteTableAssociation3', {
      routeTableId: privateRouteTable1.ref,
      subnetId: privateSubnet1.ref,
    });
    // sample-rt-private02
    new ec2.CfnSubnetRouteTableAssociation(this, 'CfnSubnetRouteTableAssociation4', {
      routeTableId: privateRouteTable2.ref,
      subnetId: privateSubnet2.ref,
    });

    // ---------- 4.7 セキュリティグループ ----------
    // 踏み台サーバー用
    const sgBastion = new ec2.CfnSecurityGroup(this, 'CfnSecurityGroup1', {
      groupDescription: 'for bastion server',
      vpcId: vpc.ref,
      securityGroupIngress: [{
        ipProtocol: 'tcp',
        fromPort: 22, // sshのポート
        toPort: 22, // sshのポート
        cidrIp: '0.0.0.0/0',
        description: 'for bastion server',
      }],
      tags: [{ key: 'Name', value: 'sample-sg-bastion' }],
    })
    // ロードバランサ用
    const sgElb = new ec2.CfnSecurityGroup(this, 'CfnSecurityGroup2', {
      groupDescription: 'for load balancer',
      vpcId: vpc.ref,
      securityGroupIngress: [{
        ipProtocol: 'tcp',
        fromPort: 80, // HTTPのポート
        toPort: 80, // HTTPのポート
        cidrIp: '0.0.0.0/0',
        description: 'for load balancer',
      }, {
        ipProtocol: 'tcp',
        fromPort: 443, // HTTPSのポート
        toPort: 443, // HTTPSのポート
        cidrIp: '0.0.0.0/0',
        description: 'for load balancer',
      }],
      tags: [{ key: 'Name', value: 'sample-sg-elb' }],
    })
  }
}
```

## デプロイ

`cdk deploy`で書籍の４章と同じインフラが構築されます。

## 削除

`cdk destroy`で全部消えてくれます。

:::note warn
デプロイしたまま放置すると課金されるので削除したほうがいいです。（特にNATゲートウェイ）
再度デプロイすれば同じインフラ構築されますので。
:::

# ５章「踏み台サーバーを用意しよう」までCDKで

本来であれば

1. SSH用のキーペアを作成し、秘密鍵をダウンロード
1. 作成したキーペアを踏み台サーバー用EC2インスタンスに設定し、インスタンス作成

という手順になりますが、CDKだと秘密鍵はダウンロードできないし、「キーペアをEC2インスタンスに設定」の部分もできません。

なので[cdk-ec2-key-pair](https://github.com/udondan/cdk-ec2-key-pair)を利用します。[cdk-ec2-key-pair](https://github.com/udondan/cdk-ec2-key-pair)は作成したキーペアを AWS Secrets Manager に格納し、それをEC2に設定してくれます。

秘密鍵のダウンロードは以下のコマンドで行えます。

```bash
aws secretsmanager get-secret-value \
  --secret-id ec2-ssh-key/nakagaki/private \
  --query SecretString \
  --output text > nakagaki.pem
  # ファイル名「nakagaki.pem」で保存
```


:::note info
AWSの [Amazon EC2 でウェブアプリケーションをデプロイする](https://aws.amazon.com/jp/getting-started/guides/deploy-webapp-ec2/module-one/) でも [cdk-ec2-key-pair](https://github.com/udondan/cdk-ec2-key-pair) が紹介されていました。
:::

先ほど作成した`lib/hello-cdk-stack.ts`に以下のコードを追加します。

```typescript:lib/hello-cdk-stack.ts
// 追加
import * as iam from 'aws-cdk-lib/aws-iam';
import { KeyPair } from 'cdk-ec2-key-pair';

...

    // ---------- 5.2 SSH接続に必要なキーペアを用意する ----------
    // https://github.com/udondan/cdk-ec2-key-pair#usage
    const key = new KeyPair(this, 'KeyPair1', {
      name: 'nakagaki',
      storePublicKey: true, // by default the public key will not be stored in Secrets Manager
    });
    key.grantReadOnPublicKey;

    // ---------- 5.3 踏み台サーバーを用意する ----------
    // IAM role to allow access to other AWS services
    const role = new iam.Role(this, 'ec2Role', {
      assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
    });

    // IAM policy attachment to allow access to 
    role.addManagedPolicy(
      iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')
    );

    const ami = new ec2.AmazonLinuxImage({
      generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
      cpuType: ec2.AmazonLinuxCpuType.X86_64,
    });

    // Create the EC2 instance using the Security Group, AMI, and KeyPair defined.
    const ec2Instance = new ec2.CfnInstance(this, 'CfnInstance1', {
      imageId: ami.getImage(this).imageId,
      instanceType: 't2.micro',
      availabilityZone: 'ap-northeast-1a',
      networkInterfaces: [{
        deviceIndex: '0',
        groupSet: [sgBastion.ref, vpc.attrDefaultSecurityGroup],
        subnetId: publicSubnet1.ref,
        associatePublicIpAddress: true,
      }],
      keyName: key.keyPairName,
      tags: [{ key: 'Name', value: 'sample-ec2-bastion' }],
    });
```

デプロイ後、「5.4 接続確認」の手順が行えます。

# おわりに

CDK v2の情報が意外と少なくて苦労しました。

６章以降もCDKで作っていこうと思います。