【AWS】CDKでVPCをCfnレベルで作成＆サブネットをインポートして他で指定する方法

## 概要

CDKでVPC周りをPythonで実装したので、紹介します。
基本的にCloudformationレベルのコンストラクトで実装しています。

## CDKサンプルコード

以下はパブリックサブネット、プライベートサブネット、プロテクトサブネットをそれぞれ二つずつ作る構成です。Cfnレベルにしているので、インターネットゲートウェイ、NATゲートウェイ、ルートテーブルも一緒に明示的に指定して作成します。

```py
class SampleApiStack(Stack):

    def __init__(self, scope: Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        system_tag = "sample-api"

        ##############################
        # VPC
        ##############################

        vpc = ec2.CfnVPC(self, f"{system_tag}-VPC",
            cidr_block="10.0.0.0/16",
            enable_dns_support=True,
            enable_dns_hostnames=True,
            tags=[{
                "key": "Name",
                "value": f"{system_tag}"
            }]
        )

        internet_gateway = ec2.CfnInternetGateway(self, f"{system_tag}-InternetGateway",
            tags=[{
                "key": "Name",
                "value": f"{system_tag}"
            }]
        )

        ec2.CfnVPCGatewayAttachment(self, f"{system_tag}-InternetGatewayAttachment",
            vpc_id=vpc.ref,
            internet_gateway_id=internet_gateway.ref
        )

        availability_zones = [f"{self.region}a", f"{self.region}c"]

        public_subnets = []
        private_subnets = []
        protected_subnets = []

        for i, az in enumerate(availability_zones):
            public_subnet = ec2.CfnSubnet(self, f"{system_tag}-PublicSubnet-{i+1}",
                vpc_id=vpc.ref,
                cidr_block=f"10.0.{i+1}.0/24",
                availability_zone=az,
                map_public_ip_on_launch=True,
                tags=[{
                    "key": "Name",
                    "value": f"{system_tag}-PublicSubnet-{i+1}"
                }]
            )
            public_subnets.append(public_subnet)

            private_subnet = ec2.CfnSubnet(self, f"{system_tag}-PrivateSubnet-{i+1}",
                vpc_id=vpc.ref,
                cidr_block=f"10.0.{i+3}.0/24",
                availability_zone=az,
                tags=[{
                    "key": "Name",
                    "value": f"{system_tag}-PrivateSubnet-{i+1}"
                }]
            )
            private_subnets.append(private_subnet)

            protected_subnet = ec2.CfnSubnet(self, f"{system_tag}-ProtectedSubnet-{i+1}",
                vpc_id=vpc.ref,
                cidr_block=f"10.0.{i+5}.0/24",
                availability_zone=az,
                tags=[{
                    "key": "Name",
                    "value": f"{system_tag}-ProtectedSubnet-{i+1}"
                }]
            )
            protected_subnets.append(protected_subnet)

        public_route_table = ec2.CfnRouteTable(self, f"{system_tag}-PublicRouteTable",
            vpc_id=vpc.ref,
            tags=[{
                "key": "Name",
                "value": f"{system_tag}-PublicRouteTable"
            }]
        )

        ec2.CfnRoute(self, f"{system_tag}-PublicRoute",
            route_table_id=public_route_table.ref,
            destination_cidr_block="0.0.0.0/0",
            gateway_id=internet_gateway.ref
        )

        for i, public_subnet in enumerate(public_subnets):
            ec2.CfnSubnetRouteTableAssociation(self, f"{system_tag}-PublicSubnetAssociation-{i+1}",
                subnet_id=public_subnet.ref,
                route_table_id=public_route_table.ref
            )

        nat_eips = []
        for i in range(len(availability_zones)):
            nat_eip = ec2.CfnEIP(self, f"{system_tag}-NatEIP-{i+1}",
                tags=[{
                    "key": "Name",
                    "value": f"{system_tag}-NatEIP-{i+1}"
                }]
            )
            nat_eips.append(nat_eip)

        nat_gateways = []
        for i, public_subnet in enumerate(public_subnets):
            nat_gateway = ec2.CfnNatGateway(self, f"{system_tag}-NatGateway-{i+1}",
                subnet_id=public_subnet.ref,
                allocation_id=nat_eips[i].attr_allocation_id,
                tags=[{
                    "key": "Name",
                    "value": f"{system_tag}-Public{i+1}"
                }]
            )
            nat_gateways.append(nat_gateway)

        private_route_tables = []
        for i, nat_gateway in enumerate(nat_gateways):
            private_route_table = ec2.CfnRouteTable(self, f"{system_tag}-PrivateRouteTable-{i+1}",
                vpc_id=vpc.ref,
                tags=[{
                    "key": "Name",
                    "value": f"{system_tag}-Private{i+1}"
                }]
            )
            private_route_tables.append(private_route_table)

            ec2.CfnRoute(self, f"{system_tag}-PrivateRoute-{i+1}",
                route_table_id=private_route_table.ref,
                destination_cidr_block="0.0.0.0/0",
                nat_gateway_id=nat_gateway.ref
            )

        for i, private_subnet in enumerate(private_subnets):
            ec2.CfnSubnetRouteTableAssociation(self, f"{system_tag}-PrivateSubnetAssociation-{i+1}",
                subnet_id=private_subnet.ref,
                route_table_id=private_route_tables[i].ref
            )
```

## No routeTableId was provided to...警告について
ちなみに、以下のようにすることで仮想的にVPCのサブネット（private subnet）をインポートできます。

```py
        private_subnets_vpc = ec2.Vpc.from_vpc_attributes(
            self, f"{system_tag}-PrivateSubnetsVPC",
            vpc_id=vpc.ref,
            availability_zones=availability_zones,
            private_subnet_ids=[private_subnet.ref for private_subnet in private_subnets],
            private_subnet_route_table_ids=[private_route_table.ref for private_route_table in private_route_tables]
        )
```

`private_subnet_route_table_ids`がない場合、以下のwarningが出ました。

> No routeTableId was provided to the subnet at 'xxxx-PrivateSubnetsVPC/PrivateSubnet1'. Attempting to read its .routeTable.routeTableId will return null/undefined.

このwarningは、`ec2.Vpc.from_vpc_attributes`を使用して仮想的にインポートした VPC のサブネットに対して、`routeTableId`が指定されていないために発生する警告。

AWS CDK はサブネットのルートテーブル情報を正しく解釈するために`routeTableId`を必要としますが、指定されていない場合は`null`または`undefined`を返す可能性がありますよ、ということを伝えてくれています。

`private_subnet_route_table_ids`を指定してルートテーブル ID を明示的に渡すことで、この警告を解消できました。

ちなみに、仮想的にインポートされたVPCのサブネットは、以下のリソースで指定できました。

- セキュリティグループ（`ec2.SecurityGroup`）
- EC2インスタンス（`ec2.Instance`）
- Lambda関数（`_lambda.Function`）


## その他エラー

RDS インスタンスが作成される VPC と、関連付けられているセキュリティグループが異なる VPC に存在してしまうと以下のエラーにも何度か遭遇しました。上述の通りにすることで、以下のエラーが出ることは無くなりました。

> Resource handler returned message: "The DB instance and EC2 security group are in different VPCs. The DB instance is in vpc-xxxxxxxx and the EC2 security group is in vpc-xxxxxxxxxxxxxxxx

また、

- vpc に`ec2.CfnVPC`（L1 コンストラクト）を渡している
- `ec2.SecurityGroup`（L2 コンストラクト）は`ec2.Vpc`（L2 コンストラクト）を期待している

という状態だと以下のエラーになります。
L1 コンストラクトと L2 コンストラクトの混在は、CDK作成の際に留意するポイントですね。

> "SecurityGroupEgress cannot be specified without VpcId"