概要
CDKでVPC周りをPythonで実装したので、紹介します。
基本的にCloudformationレベルのコンストラクトで実装しています。
CDKサンプルコード
以下はパブリックサブネット、プライベートサブネット、プロテクトサブネットをそれぞれ二つずつ作る構成です。Cfnレベルにしているので、インターネットゲートウェイ、NATゲートウェイ、ルートテーブルも一緒に明示的に指定して作成します。
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)をインポートできます。
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"