AWS CDK で Infrastructure as Code する: VPC編

こんにちは。AWS勉強中の @masatomix です。

[前回？](https://qiita.com/masatomix/items/7087e7813637ff5e9c36)までAWS CDKをいろいろいじくったところを記事にしていましたが、ひきつづき AWS CDKです。これからAWS CDK / CloudFormationでいろいろ AWSのリソースを作成するサンプルを作っていきたいのですが、なにはともあれ独立したVPCがほしいので、まずはVPCを新規作成するサンプルです。


## 前提

- [AWS CDK で Infrastructure as Code する](https://qiita.com/masatomix/items/8fc3d52ee89489967c09) の記事の環境がそろっていること


## やってみる

作りたい環境はこんな感じ。

![2FbK1J3](https://imgur.com/5XiDqNL.png)

Availability Zone 3つにまたがるVPCを構築し、各AZにPublic/Private のSubnetを1つずつ。Private SubnetがInternet に出て行くための NAT Gatewayは1つのAZに配置[^1]。

もちろん、NAT Gatewayに割り振るElastic IPやInternet Gateway、各Subnetがネットに出て行くためのRouteTableなども作成しています。

[^1]: 本番構成では各AZに置くべきやつです。


### CDKのソースコード

コードです

```typescript:lib/VPCStack.ts
import { App, CfnParameter, Stack, StackProps } from 'aws-cdk-lib'
import {
  CfnEIP,
  CfnInternetGateway,
  CfnNatGateway,
  CfnRoute,
  CfnRouteTable,
  CfnSubnet,
  CfnSubnetRouteTableAssociation,
  CfnVPC,
  CfnVPCGatewayAttachment,
} from 'aws-cdk-lib/aws-ec2'
import { availabilityZones, getProfile } from './Utils'
// import * as sqs from 'aws-cdk-lib/aws-sqs';

export class VPCStack extends Stack {
  public readonly vpc: CfnVPC
  public readonly publicSubnets: CfnSubnet[]
  public readonly privateSubnets: CfnSubnet[]

  constructor(scope: App, id: string, props?: StackProps) {
    super(scope, id, props)

    // The code that defines your stack goes here
    const p = getProfile(this)

    const vpcCIDRs = {
      vpc: '192.168.0.0/16',
      subnets: [
        { public: '192.168.0.0/24', private: '192.168.1.0/24' },
        { public: '192.168.2.0/24', private: '192.168.3.0/24' },
        { public: '192.168.4.0/24', private: '192.168.5.0/24' },
      ],
    }
    const subnetCount = vpcCIDRs.subnets.length

    // VPC
    const vpc = new CfnVPC(this, `MyVPC`, {
      cidrBlock: vpcCIDRs.vpc,
      tags: [{ key: 'Name', value: `${p.name}-vpc` }],
    })
    this.vpc = vpc

    // Public Subnet
    const publicSubnets = vpcCIDRs.subnets.map(
      (subnet, index) =>
        new CfnSubnet(this, `MyPublicSubnet${index}`, {
          vpcId: vpc.ref,
          cidrBlock: subnet.public,
          availabilityZone: availabilityZones[index],
          mapPublicIpOnLaunch: true,
          tags: [{ key: 'Name', value: `${p.name}-public-subnet-${index}` }],
        }),
    )
    this.publicSubnets = publicSubnets

    // Private Subnet
    const privateSubnets = vpcCIDRs.subnets.map(
      (subnet, index) =>
        new CfnSubnet(this, `MyPrivateSubnet${index}`, {
          vpcId: vpc.ref,
          cidrBlock: subnet.private,
          availabilityZone: availabilityZones[index],
          mapPublicIpOnLaunch: false,
          tags: [{ key: 'Name', value: `${p.name}-private-subnet-${index}` }],
        }),
    )
    this.privateSubnets = privateSubnets

    // Internet Gateway
    const igw = new CfnInternetGateway(this, 'MyInternetGateWay', { tags: [{ key: 'Name', value: `${p.name}-igw` }] })
    const attachInternetGateway = new CfnVPCGatewayAttachment(this, 'AttachGateway', {
      vpcId: vpc.ref,
      internetGatewayId: igw.ref,
    })

    // Public RouteTables
    const publicRouteTables = vpcCIDRs.subnets.map(
      (subnet, index) =>
        new CfnRouteTable(this, `PublicRouteTable${index}`, {
          vpcId: vpc.ref,
          tags: [{ key: 'Name', value: `${p.name}-public-route-${index}` }],
        }),
    )

    // Private RouteTables
    const privateRouteTables = vpcCIDRs.subnets.map(
      (subnet, index) =>
        new CfnRouteTable(this, `PrivateRouteTable${index}`, {
          vpcId: vpc.ref,
          tags: [{ key: 'Name', value: `${p.name}-private-route-${index}` }],
        }),
    )

    for (let index = 0; index < subnetCount; index++) {
      // 各Public用 RouteTableに Public Subnetを紐付け
      new CfnSubnetRouteTableAssociation(this, `PublicSubnetRouteTableAssociation${index}`, {
        routeTableId: publicRouteTables[index].ref,
        subnetId: publicSubnets[index].ref,
      })

      // そのRouteTableはInternet Gatewayを紐付ける
      new CfnRoute(this, `PublicRouteToIGW${index}`, {
        routeTableId: publicRouteTables[index].ref,
        destinationCidrBlock: '0.0.0.0/0',
        gatewayId: igw.ref,
      })

      // 各Private用 RouteTableに Private Subnetを紐付け
      new CfnSubnetRouteTableAssociation(this, `PrivateSubnetRouteTableAssociation${index}`, {
        routeTableId: privateRouteTables[index].ref,
        subnetId: privateSubnets[index].ref,
      })
    }

    // // 各Subnetに、NAT GWを置くパタン
    // const eips = [...new Array(subnetCount)].map((_: undefined, index: number) => {
    //   return new CfnEIP(this, `EIPforNatGw${index}`, { domain: 'vpc' })
    // })
    // const natgws = [...new Array(subnetCount)].map((_: undefined, index: number) => {
    //   return new CfnNatGateway(this, `MyNAT${index}`, {
    //     allocationId: eips[index].attrAllocationId,
    //     subnetId: publicSubnets[index].ref
    //   })
    // })
    // for (let i = 0; i < subnetCount; i++) {
    //   new CfnRoute(this, `RouteToNAT${i}`, {
    //     routeTableId: privateRouteTables[i].ref,
    //     destinationCidrBlock: '0.0.0.0/0',
    //     natGatewayId: natgws[i].ref
    //   })
    // }
    // // 各Subnetに、NAT GWを置くパタン

    // 1Subnetに、NAT GWを置くパタン
    // publicSubnets[0] に、NAT GWを配置して
    const eip = new CfnEIP(this, 'EIPforNatGw1', { domain: 'vpc' })
    const natgw = new CfnNatGateway(this, `MyNAT1`, {
      allocationId: eip.attrAllocationId,
      subnetId: publicSubnets[0].ref,
      tags: [{ key: 'Name', value: `${p.name}-natgw` }],
    })
    natgw.addDependency(attachInternetGateway)

    // そのNAT GWを各Private SubnetのRouteTableにセットする
    for (let i = 0; i < subnetCount; i++) {
      new CfnRoute(this, `RouteToNAT${i}`, {
        routeTableId: privateRouteTables[i].ref,
        destinationCidrBlock: '0.0.0.0/0',
        natGatewayId: natgw.ref,
      })
    }
    // 1Subnetに、NAT GWを置くパタン
  }
}
```

```typescript:bin/cdk-samples.ts
#!/usr/bin/env node
import 'source-map-support/register'
import * as cdk from 'aws-cdk-lib'
import { VPCStack } from '../lib/VPCStack'

const app = new cdk.App()

const vpcStack = new VPCStack(app, 'VPCStack')
```



```json:cdk.json(主要な部分だけ)
{
  "app": "npx ts-node --prefer-ts-exts bin/cdk-samples.ts",
  ...
  "context": {
    ...
    "dev": {
      "name": "dev-20230815"
    }
  }
}
```

ちなみに上記の``"dev":{"name": "dev-20230815"}`` によって
``tags: [{ key: 'Name', value: `${p.name}-vpc` }],``などのプレースホルダが
``dev-20230815-vpc`` に置換されるようにしてあります。


### 実行してみる

コードを作ったら、では実行してみます。

```console
$ yarn cdk deploy VPCStack
... しばらくかかります
$
```

ちゃんとできているか一応確認します。

```console
$ aws ec2 describe-vpcs \
 --query "Vpcs[*].[(Tags[?Key=='Name'])[0].Value,VpcId,CidrBlock]" \
 --filters "Name=tag:Name,Values=dev-20230815-vpc" \
 --output table
-----------------------------------------------------------------
|                         DescribeVpcs                          |
+------------------+-------------------------+------------------+
|  dev-20230815-vpc|  vpc-0461b6f7baff8f8d1  |  192.168.0.0/16  |
+------------------+-------------------------+------------------+

$ aws ec2 describe-subnets \
--query "Subnets[*].[(Tags[?Key=='Name'])[0].Value,CidrBlock,SubnetId]" \
--filters "Name=vpc-id, Values=vpc-0461b6f7baff8f8d1" \
--output table
---------------------------------------------------------------------------------
|                                DescribeSubnets                                |
+--------------------------------+-----------------+----------------------------+
|  dev-20230815-public-subnet-0  |  192.168.0.0/24 |  subnet-0ec370f96cfb4e93b  |
|  dev-20230815-private-subnet-2 |  192.168.5.0/24 |  subnet-0f7dcb24813414db1  |
|  dev-20230815-private-subnet-1 |  192.168.3.0/24 |  subnet-03587fc0cf1fb9ae8  |
|  dev-20230815-public-subnet-1  |  192.168.2.0/24 |  subnet-0c05e26c010e2fa3c  |
|  dev-20230815-public-subnet-2  |  192.168.4.0/24 |  subnet-0e2cda5c21e1e6370  |
|  dev-20230815-private-subnet-0 |  192.168.1.0/24 |  subnet-0a5ec4312d172c4b1  |
+--------------------------------+-----------------+----------------------------+
$
```

ちゃんとできていそうですね。お疲れさまでした！



## 関連リンク

- https://github.com/masatomix/cdk-samples/tree/vpc_base 今回のソースコード
- [AWS CDK で Infrastructure as Code する](https://qiita.com/masatomix/items/8fc3d52ee89489967c09)これで環境をつくってください。
- [AWS のアーキテクチャ図を描きたい ! でもどうすれば良いの ?](https://aws.amazon.com/jp/builders-flash/202204/way-to-draw-architecture/?awsf.filter-name=*all)
- [AWS CDKのTIPS集](https://qiita.com/masatomix/items/7087e7813637ff5e9c36)