AWS CDKなら、たった1行で高可用なVPCを構築できる

AWS CDK(Python3)でVPCを作るため、aws_cdk.aws_ec2.Vpcのオブジェクトを生成する (= 下記testinfra.stack.pyのec2.Vpc()の部分を実行する) 時、何が起きるのかを観察してみました。

CloudFormationでは、作ろうとするリソースに対応するコードの殆どを自分で書く必要がありますが、AWS CDKではわずか1行でデフォルトでいろんなものを作れました。

# 0. 前提知識
以下をお読みいただくと、Python3でのAWS CDK開発のイメージがわくはずなので、きっとスムーズに読めるはずですよ。(保証はしません)
[Cloud9にAWS CDK (Python3) をインストールする方法](https://qiita.com/nasuvitz/items/25938c3598168c9d5fd3)
[AWS CDK (Python3) でS3 Bucketを1個作って感じたこと](https://qiita.com/nasuvitz/items/38b6007df41f9112e98f)

# 1. テストコード
以下は、スタックを表すクラスです。
VPCのCidrには10.0.0.0/24を指定しています。
端的に言えば、わざわざ指定したパラメータは、このVPCのCidrだけです。

```python:testinfra_stack.py
from aws_cdk import core
from aws_cdk import (
    aws_ec2 as ec2
)

class CodeinfraStack(core.Stack):
  
    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)
        
        # Parameter
        vpcName = "test-vpc"
        vpcCidrBlock = "10.0.0.0/24"

        self.createVpc(vpcCidrBlock=vpcCidrBlock, vpcName=vpcName)
        
    # VPC
    def createVpc(self, vpcCidrBlock, vpcName):
        vpcNetworkAddress = ipaddress.ip_network(address=vpcCidrBlock)
        
        vpcNetwork = ec2.Vpc(self, vpcName, cidr=vpcCidrBlock)
```

以下は、app.pyになります。

```python:app.py
#!/usr/bin/env python3

from aws_cdk import core
from test.testinfra_stack import CodeinfraStack

app = core.App()
CodeinfraStack(app, "Test-Infra")

app.synth()
```

CloudFormationであれば、VPCそのものしか書いていないイメージです。


# 2. cdk synthしてできたもの
このコードでcdk synthしてみると、以下のCloudFormationテンプレートができました。
testinfra_stack.pyが約20行ですが、CloudFormationテンプレートは約440行でした。

```json:Test-Infra.template.json
{
  "Resources": {
    "testvpc8985080E": {
      "Type": "AWS::EC2::VPC",
      "Properties": {
        "CidrBlock": "10.0.0.0/24",
        "EnableDnsHostnames": true,
        "EnableDnsSupport": true,
        "InstanceTenancy": "default",
        "Tags": [
          {
            "Key": "Name",
            "Value": "Test-Infra/test-vpc"
          }
        ]
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/Resource"
      }
    },
    "testvpcPublicSubnet1Subnet01CF7554": {
      "Type": "AWS::EC2::Subnet",
      "Properties": {
        "CidrBlock": "10.0.0.0/26",
        "VpcId": {
          "Ref": "testvpc8985080E"
        },
        "AvailabilityZone": {
          "Fn::Select": [
            0,
            {
              "Fn::GetAZs": ""
            }
          ]
        },
        "MapPublicIpOnLaunch": true,
        "Tags": [
          {
            "Key": "Name",
            "Value": "Test-Infra/test-vpc/PublicSubnet1"
          },
          {
            "Key": "aws-cdk:subnet-name",
            "Value": "Public"
          },
          {
            "Key": "aws-cdk:subnet-type",
            "Value": "Public"
          }
        ]
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PublicSubnet1/Subnet"
      }
    },
    "testvpcPublicSubnet1RouteTable180BB588": {
      "Type": "AWS::EC2::RouteTable",
      "Properties": {
        "VpcId": {
          "Ref": "testvpc8985080E"
        },
        "Tags": [
          {
            "Key": "Name",
            "Value": "Test-Infra/test-vpc/PublicSubnet1"
          }
        ]
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PublicSubnet1/RouteTable"
      }
    },
    "testvpcPublicSubnet1RouteTableAssociation14A2D92F": {
      "Type": "AWS::EC2::SubnetRouteTableAssociation",
      "Properties": {
        "RouteTableId": {
          "Ref": "testvpcPublicSubnet1RouteTable180BB588"
        },
        "SubnetId": {
          "Ref": "testvpcPublicSubnet1Subnet01CF7554"
        }
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PublicSubnet1/RouteTableAssociation"
      }
    },
    "testvpcPublicSubnet1DefaultRouteB1E474AB": {
      "Type": "AWS::EC2::Route",
      "Properties": {
        "RouteTableId": {
          "Ref": "testvpcPublicSubnet1RouteTable180BB588"
        },
        "DestinationCidrBlock": "0.0.0.0/0",
        "GatewayId": {
          "Ref": "testvpcIGW2C2BA83F"
        }
      },
      "DependsOn": [
        "testvpcVPCGW7060AA15"
      ],
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PublicSubnet1/DefaultRoute"
      }
    },
    "testvpcPublicSubnet1EIP84634DA0": {
      "Type": "AWS::EC2::EIP",
      "Properties": {
        "Domain": "vpc"
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PublicSubnet1/EIP"
      }
    },
    "testvpcPublicSubnet1NATGateway50787A07": {
      "Type": "AWS::EC2::NatGateway",
      "Properties": {
        "AllocationId": {
          "Fn::GetAtt": [
            "testvpcPublicSubnet1EIP84634DA0",
            "AllocationId"
          ]
        },
        "SubnetId": {
          "Ref": "testvpcPublicSubnet1Subnet01CF7554"
        },
        "Tags": [
          {
            "Key": "Name",
            "Value": "Test-Infra/test-vpc/PublicSubnet1"
          }
        ]
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PublicSubnet1/NATGateway"
      }
    },
    "testvpcPublicSubnet2Subnet4E9D9728": {
      "Type": "AWS::EC2::Subnet",
      "Properties": {
        "CidrBlock": "10.0.0.64/26",
        "VpcId": {
          "Ref": "testvpc8985080E"
        },
        "AvailabilityZone": {
          "Fn::Select": [
            1,
            {
              "Fn::GetAZs": ""
            }
          ]
        },
        "MapPublicIpOnLaunch": true,
        "Tags": [
          {
            "Key": "Name",
            "Value": "Test-Infra/test-vpc/PublicSubnet2"
          },
          {
            "Key": "aws-cdk:subnet-name",
            "Value": "Public"
          },
          {
            "Key": "aws-cdk:subnet-type",
            "Value": "Public"
          }
        ]
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PublicSubnet2/Subnet"
      }
    },
    "testvpcPublicSubnet2RouteTable28A079F9": {
      "Type": "AWS::EC2::RouteTable",
      "Properties": {
        "VpcId": {
          "Ref": "testvpc8985080E"
        },
        "Tags": [
          {
            "Key": "Name",
            "Value": "Test-Infra/test-vpc/PublicSubnet2"
          }
        ]
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PublicSubnet2/RouteTable"
      }
    },
    "testvpcPublicSubnet2RouteTableAssociationACF92511": {
      "Type": "AWS::EC2::SubnetRouteTableAssociation",
      "Properties": {
        "RouteTableId": {
          "Ref": "testvpcPublicSubnet2RouteTable28A079F9"
        },
        "SubnetId": {
          "Ref": "testvpcPublicSubnet2Subnet4E9D9728"
        }
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PublicSubnet2/RouteTableAssociation"
      }
    },
    "testvpcPublicSubnet2DefaultRoute39BC0F35": {
      "Type": "AWS::EC2::Route",
      "Properties": {
        "RouteTableId": {
          "Ref": "testvpcPublicSubnet2RouteTable28A079F9"
        },
        "DestinationCidrBlock": "0.0.0.0/0",
        "GatewayId": {
          "Ref": "testvpcIGW2C2BA83F"
        }
      },
      "DependsOn": [
        "testvpcVPCGW7060AA15"
      ],
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PublicSubnet2/DefaultRoute"
      }
    },
    "testvpcPublicSubnet2EIP6819FC49": {
      "Type": "AWS::EC2::EIP",
      "Properties": {
        "Domain": "vpc"
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PublicSubnet2/EIP"
      }
    },
    "testvpcPublicSubnet2NATGateway8D7A9976": {
      "Type": "AWS::EC2::NatGateway",
      "Properties": {
        "AllocationId": {
          "Fn::GetAtt": [
            "testvpcPublicSubnet2EIP6819FC49",
            "AllocationId"
          ]
        },
        "SubnetId": {
          "Ref": "testvpcPublicSubnet2Subnet4E9D9728"
        },
        "Tags": [
          {
            "Key": "Name",
            "Value": "Test-Infra/test-vpc/PublicSubnet2"
          }
        ]
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PublicSubnet2/NATGateway"
      }
    },
    "testvpcPrivateSubnet1Subnet865FB50A": {
      "Type": "AWS::EC2::Subnet",
      "Properties": {
        "CidrBlock": "10.0.0.128/26",
        "VpcId": {
          "Ref": "testvpc8985080E"
        },
        "AvailabilityZone": {
          "Fn::Select": [
            0,
            {
              "Fn::GetAZs": ""
            }
          ]
        },
        "MapPublicIpOnLaunch": false,
        "Tags": [
          {
            "Key": "Name",
            "Value": "Test-Infra/test-vpc/PrivateSubnet1"
          },
          {
            "Key": "aws-cdk:subnet-name",
            "Value": "Private"
          },
          {
            "Key": "aws-cdk:subnet-type",
            "Value": "Private"
          }
        ]
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PrivateSubnet1/Subnet"
      }
    },
    "testvpcPrivateSubnet1RouteTableC6BCA266": {
      "Type": "AWS::EC2::RouteTable",
      "Properties": {
        "VpcId": {
          "Ref": "testvpc8985080E"
        },
        "Tags": [
          {
            "Key": "Name",
            "Value": "Test-Infra/test-vpc/PrivateSubnet1"
          }
        ]
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PrivateSubnet1/RouteTable"
      }
    },
    "testvpcPrivateSubnet1RouteTableAssociation0E625B49": {
      "Type": "AWS::EC2::SubnetRouteTableAssociation",
      "Properties": {
        "RouteTableId": {
          "Ref": "testvpcPrivateSubnet1RouteTableC6BCA266"
        },
        "SubnetId": {
          "Ref": "testvpcPrivateSubnet1Subnet865FB50A"
        }
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PrivateSubnet1/RouteTableAssociation"
      }
    },
    "testvpcPrivateSubnet1DefaultRouteF07B0F68": {
      "Type": "AWS::EC2::Route",
      "Properties": {
        "RouteTableId": {
          "Ref": "testvpcPrivateSubnet1RouteTableC6BCA266"
        },
        "DestinationCidrBlock": "0.0.0.0/0",
        "NatGatewayId": {
          "Ref": "testvpcPublicSubnet1NATGateway50787A07"
        }
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PrivateSubnet1/DefaultRoute"
      }
    },
    "testvpcPrivateSubnet2Subnet23D3396F": {
      "Type": "AWS::EC2::Subnet",
      "Properties": {
        "CidrBlock": "10.0.0.192/26",
        "VpcId": {
          "Ref": "testvpc8985080E"
        },
        "AvailabilityZone": {
          "Fn::Select": [
            1,
            {
              "Fn::GetAZs": ""
            }
          ]
        },
        "MapPublicIpOnLaunch": false,
        "Tags": [
          {
            "Key": "Name",
            "Value": "Test-Infra/test-vpc/PrivateSubnet2"
          },
          {
            "Key": "aws-cdk:subnet-name",
            "Value": "Private"
          },
          {
            "Key": "aws-cdk:subnet-type",
            "Value": "Private"
          }
        ]
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PrivateSubnet2/Subnet"
      }
    },
    "testvpcPrivateSubnet2RouteTable26C5E053": {
      "Type": "AWS::EC2::RouteTable",
      "Properties": {
        "VpcId": {
          "Ref": "testvpc8985080E"
        },
        "Tags": [
          {
            "Key": "Name",
            "Value": "Test-Infra/test-vpc/PrivateSubnet2"
          }
        ]
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PrivateSubnet2/RouteTable"
      }
    },
    "testvpcPrivateSubnet2RouteTableAssociationB60494EA": {
      "Type": "AWS::EC2::SubnetRouteTableAssociation",
      "Properties": {
        "RouteTableId": {
          "Ref": "testvpcPrivateSubnet2RouteTable26C5E053"
        },
        "SubnetId": {
          "Ref": "testvpcPrivateSubnet2Subnet23D3396F"
        }
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PrivateSubnet2/RouteTableAssociation"
      }
    },
    "testvpcPrivateSubnet2DefaultRouteC94968D3": {
      "Type": "AWS::EC2::Route",
      "Properties": {
        "RouteTableId": {
          "Ref": "testvpcPrivateSubnet2RouteTable26C5E053"
        },
        "DestinationCidrBlock": "0.0.0.0/0",
        "NatGatewayId": {
          "Ref": "testvpcPublicSubnet2NATGateway8D7A9976"
        }
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/PrivateSubnet2/DefaultRoute"
      }
    },
    "testvpcIGW2C2BA83F": {
      "Type": "AWS::EC2::InternetGateway",
      "Properties": {
        "Tags": [
          {
            "Key": "Name",
            "Value": "Test-Infra/test-vpc"
          }
        ]
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/IGW"
      }
    },
    "testvpcVPCGW7060AA15": {
      "Type": "AWS::EC2::VPCGatewayAttachment",
      "Properties": {
        "VpcId": {
          "Ref": "testvpc8985080E"
        },
        "InternetGatewayId": {
          "Ref": "testvpcIGW2C2BA83F"
        }
      },
      "Metadata": {
        "aws:cdk:path": "Test-Infra/test-vpc/VPCGW"
      }
    }
  }
}
```

# 3. CloudFormationコードに含まれていたもの
以下のものができました。

|Type|できたもの|備考|
|:---|:---|:---|
|AWS::EC2::VPC|VPC|10.0.0.0/24,EnableDnsHostnamesとEnableDnsSupportはTrue,InstanceTenancyはDefault|
|AWS::EC2::Subnet|PublicSubnet1Subnet|10.0.0.0/26,MapPublicIpOnLaunchはTrue|
|AWS::EC2::RouteTable|PublicSubnet1RouteTable|-|
|AWS::EC2::SubnetRouteTableAssociation|PublicSubnet1RouteTableAssociation|PublicSubnet1RouteTableをPublicSubnet1Subnetにアタッチ|
|AWS::EC2::Route|PublicSubnet1DefaultRoute|0.0.0.0/0→IGW|
|AWS::EC2::EIP|PublicSubnet1EIP|PublicSubnet1NATGateway用EIP|
|AWS::EC2::NatGateway|PublicSubnet1NATGateway|-|
|AWS::EC2::Subnet|PublicSubnet2Subnet|10.0.0.64/26|
|AWS::EC2::RouteTable|PublicSubnet2RouteTable|-|
|AWS::EC2::SubnetRouteTableAssociation|PublicSubnet2RouteTableAssociation|PublicSubnet2RouteTableをPublicSubnet2Subnetにアタッチ|
|AWS::EC2::Route|PublicSubnet2DefaultRoute|0.0.0.0/0→IGW|
|AWS::EC2::EIP|PublicSubnet2EIP|PublicSubnet2NATGateway用EIP|
|AWS::EC2::NatGateway|PublicSubnet2NATGateway|-|
|AWS::EC2::Subnet|PrivateSubnet1Subnet|10.0.0.128/26|
|AWS::EC2::RouteTable|PrivateSubnet1RouteTable|-|
|AWS::EC2::SubnetRouteTableAssociation|PrivateSubnet1RouteTableAssociation|PrivateSubnet1RouteTableをPrivateSubnet1Subnetにアタッチ|
|AWS::EC2::Route|PrivateSubnet1DefaultRoute|0.0.0.0/0→PublicSubnet1NATGateway|
|AWS::EC2::Subnet|PrivateSubnet2Subnet|10.0.0.192/26|
|AWS::EC2::RouteTable|PrivateSubnet2RouteTable|-|
|AWS::EC2::SubnetRouteTableAssociation|PrivateSubnet2RouteTableAssociation|PrivateSubnet2RouteTableをPrivateSubnet2Subnetにアタッチ|
|AWS::EC2::Route|PrivateSubnet2DefaultRoute|0.0.0.0/0→PublicSubnet2NATGateway|
|AWS::EC2::InternetGateway|IGW|-|
|AWS::EC2::VPCGatewayAttachment|VPCGW|IGWをVPCにアタッチ|

上記は、まさに以下の構成となります。
これは、AWSで良くベストプラクティスと言われているVPCの構成で、高可用性があるVPCのアーキテクチャとしても紹介されているものです。

![NAT Gateway](https://image.slidesharecdn.com/20180418aws-blackbeltvpc-180419084225/95/20180418-aws-black-belt-online-seminar-amazon-vpc-74-638.jpg)
引用: 「20180418 AWS Black Belt Online Seminar Amazon VPC」
https://www.slideshare.net/AmazonWebServicesJapan/20180418-aws-black-belt-online-seminar-amazon-vpc

**`ec2.Vpc(self, vpcName, cidr=vpcCidrBlock)`と1行書いただけ**で、これだけのものができてしまうのはすごいですね！

# 4. 公式APIドキュメントの記述
VPCのクラス定義が書かれたAPIドキュメントには、以下の記述がありました。

> VpcNetwork creates a VPC that spans a whole region. It will automatically divide the provided VPC CIDR range, and create public and private subnets per Availability Zone. Network routing for the public subnets will be configured to allow outbound access directly via an Internet Gateway. Network routing for the private subnets will be configured to allow outbound access via a set of resilient NAT Gateways (one per AZ).
https://docs.aws.amazon.com/cdk/api/latest/python/aws_cdk.aws_ec2/Vpc.html

Google翻訳すると、以下となります。
> VpcNetworkは、リージョン全体にわたるVPCを作成します。提供されたVPC CIDR範囲を自動的に分割し、アベイラビリティーゾーンごとにパブリックおよびプライベートサブネットを作成します。パブリックサブネットのネットワークルーティングは、インターネットゲートウェイ経由でのアウトバウンドアクセスを直接許可するように設定されます。プライベートサブネットのネットワークルーティングは、1組の回復力のあるNATゲートウェイ（AZごとに1つ）を介したアウトバウンドアクセスを許可するように設定されます。

VPCのクラスからオブジェクトを1つ作るコードを書くだけで、高可用性のある構成が自動的にできることが分かりました。

# 5. まとめ
AWS CDK(Python3)でVPCを作ると、CloudFormationだと約440行のコードが約20行となり、さらに単なるVPC1個だけでなく、高可用性を持った構成を一撃で作ることが出来ました。
CloudFormationを書いていて面倒と感じる、Association周りのコードを書かなくても良いので楽に感じます。

元々CloudFormationに慣れていると、必要なリソースを全てコードに書きたくなりますが、AWS CDKでは、まずVPCのように範囲の広いサービスだけのコードを書いてみて、**何がデフォルトでできるのかを観察**してから、細かくカスタマイズしていくのが良さそうです。
慣れるまでに時間はかかりそうですが、デフォルトで何ができるのかを知れば、そのデフォルトで許容できる場合は、その部分のコードを書く必要はありませんから、開発スピードが早くなりそうです。

# 6. 参考文献
[Docs » API Reference » aws_cdk.aws_ec2 » Vpc](https://docs.aws.amazon.com/cdk/api/latest/python/aws_cdk.aws_ec2/Vpc.html) (AWS CDK for PythonのAPIドキュメント)
[20180418 AWS Black Belt Online Seminar Amazon VPC](https://www.slideshare.net/AmazonWebServicesJapan/20180418-aws-black-belt-online-seminar-amazon-vpc)