Skip to content

Commit 254952f

Browse files
committed
Merge branch 'mr/trespeuch/vpc-no-priv-subnets' into 'master'
Add option to create VPCv2 without private subnets See merge request it/e3-aws!84
2 parents 0bf5163 + b314865 commit 254952f

File tree

3 files changed

+189
-13
lines changed

3 files changed

+189
-13
lines changed

src/e3/aws/troposphere/ec2/__init__.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,7 @@ def __init__(
679679
cidr_block: str | None = None,
680680
interface_endpoints: list[tuple[str, PolicyDocument | None]] | None = None,
681681
s3_endpoint_policy_document: PolicyDocument | None = None,
682+
with_private_subnets: bool = True,
682683
) -> None:
683684
"""Initialize an VPCv2 instance.
684685
@@ -691,6 +692,8 @@ def __init__(
691692
tuples for each interface endpoint to create in the VPC endpoints subnet.
692693
:param s3_endpoint_policy_document: policy for the s3 endpoint. If none is
693694
given no s3 endpoint is created.
695+
:param with_private_subnets: If True create a private subnet and a NAT
696+
gateway per AZ.
694697
"""
695698
self.name_prefix = name_prefix
696699
self.availability_zones = availability_zones
@@ -712,9 +715,11 @@ def __init__(
712715
prefixlen_diff=int.bit_length(number_of_subnet_slots)
713716
)
714717
# Assign networks to private and public subnets
715-
self.private_subnet_ip_networks = {
716-
az: next(self.subnet_ip_networks) for az in availability_zones
717-
}
718+
self.private_subnet_ip_networks = (
719+
{az: next(self.subnet_ip_networks) for az in availability_zones}
720+
if with_private_subnets
721+
else {}
722+
)
718723
self.public_subnet_ip_networks = {
719724
az: next(self.subnet_ip_networks) for az in availability_zones
720725
}
@@ -916,15 +921,19 @@ def s3_gateway_endpoint(self) -> ec2.VPCEndpoint | None:
916921
Note that this endpoint is also needed when using ECR as ECR stores
917922
images on S3.
918923
"""
924+
# Compute resources requiring access to the Gateway endpoint
925+
# are expected to run in the private subnets if they exist.
926+
route_tables: ec2.RouteTable
927+
if self.private_subnets:
928+
route_tables = self.private_subnet_route_tables.values()
929+
else:
930+
route_tables = [self.public_subnets_route_table]
919931
return (
920932
ec2.VPCEndpoint(
921933
name_to_id(f"{self.name_prefix}S3Endpoint"),
922934
PolicyDocument=self.s3_endpoint_policy_document.as_dict,
923935
# Attach the endpoints to all private subnets
924-
RouteTableIds=[
925-
Ref(private_subnet_rt)
926-
for private_subnet_rt in self.private_subnet_route_tables.values()
927-
],
936+
RouteTableIds=[Ref(subnet_rt) for subnet_rt in route_tables],
928937
ServiceName=f"com.amazonaws.{self.region}.s3",
929938
VpcEndpointType="Gateway",
930939
VpcId=Ref(self.vpc),
@@ -1012,16 +1021,21 @@ def resources(self, stack: Stack) -> list[AWSObject]:
10121021
self.internet_gateway_attachment,
10131022
self.public_subnets_route_table,
10141023
self.public_route_to_internet,
1015-
*self.private_subnets.values(),
10161024
*self.public_subnets.values(),
1017-
*self.nat_eips.values(),
1018-
*self.nat_gateways.values(),
1019-
*self.private_subnet_route_tables.values(),
1020-
*self.private_routes_to_internet.values(),
1021-
*self.private_route_table_assocs.values(),
10221025
*self.public_route_table_assocs.values(),
10231026
self.vpc,
10241027
]
1028+
if self.private_subnets:
1029+
res.extend(
1030+
[
1031+
*self.private_subnets.values(),
1032+
*self.nat_eips.values(),
1033+
*self.nat_gateways.values(),
1034+
*self.private_subnet_route_tables.values(),
1035+
*self.private_routes_to_internet.values(),
1036+
*self.private_route_table_assocs.values(),
1037+
]
1038+
)
10251039
if self.interface_endpoints_subnet:
10261040
res.append(self.interface_endpoints_subnet)
10271041
if self.s3_gateway_endpoint:

tests/tests_e3_aws/troposphere/ec2/ec2_test.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,26 @@ def test_vpc_v2_with_endpoints(stack: Stack) -> None:
205205
expected_template = json.load(fd)
206206

207207
assert stack.export()["Resources"] == expected_template
208+
209+
210+
def test_vpc_v2_without_priv_subnets(stack: Stack) -> None:
211+
"""Test VPCv2 without private subnets."""
212+
s3_endpoint_pd = PolicyDocument(
213+
statements=[
214+
Allow(action=["s3:PutObject", "s3:GetObject"], resource="*", principal="*"),
215+
Allow(action="s3:ListBucket", resource="*", principal="*"),
216+
]
217+
)
218+
vpc = VPCv2(
219+
name_prefix="TestVPC",
220+
availability_zones=["eu-west-1a", "eu-west-1b"],
221+
# When there is no private subnet, we expect the route to the gateway endpoint
222+
# to be added to the public subnet route table instead of the private route
223+
# tables
224+
s3_endpoint_policy_document=s3_endpoint_pd,
225+
with_private_subnets=False,
226+
)
227+
stack.add(vpc)
228+
with open(os.path.join(TEST_DIR, "vpc_v2_without_priv_subnets.json")) as fd:
229+
expected_template = json.load(fd)
230+
assert stack.export()["Resources"] == expected_template
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
{
2+
"TestVPCInternetGW": {
3+
"Type": "AWS::EC2::InternetGateway"
4+
},
5+
"TestVPCInternetGWAttachment": {
6+
"Properties": {
7+
"InternetGatewayId": {
8+
"Ref": "TestVPCInternetGW"
9+
},
10+
"VpcId": {
11+
"Ref": "TestVPCVPC"
12+
}
13+
},
14+
"Type": "AWS::EC2::VPCGatewayAttachment"
15+
},
16+
"TestVPCPublicRouteTable": {
17+
"Properties": {
18+
"VpcId": {
19+
"Ref": "TestVPCVPC"
20+
}
21+
},
22+
"Type": "AWS::EC2::RouteTable"
23+
},
24+
"TestVPCPublicRouteToInternet": {
25+
"Properties": {
26+
"RouteTableId": {
27+
"Ref": "TestVPCPublicRouteTable"
28+
},
29+
"DestinationCidrBlock": "0.0.0.0/0",
30+
"GatewayId": {
31+
"Ref": "TestVPCInternetGW"
32+
}
33+
},
34+
"Type": "AWS::EC2::Route"
35+
},
36+
"TestVPCPublicSubnetA": {
37+
"Properties": {
38+
"VpcId": {
39+
"Ref": "TestVPCVPC"
40+
},
41+
"CidrBlock": "10.10.0.0/19",
42+
"Tags": [
43+
{
44+
"Key": "Name",
45+
"Value": "TestVPCPublicSubnetA"
46+
}
47+
],
48+
"AvailabilityZone": "eu-west-1a"
49+
},
50+
"Type": "AWS::EC2::Subnet"
51+
},
52+
"TestVPCPublicSubnetB": {
53+
"Properties": {
54+
"VpcId": {
55+
"Ref": "TestVPCVPC"
56+
},
57+
"CidrBlock": "10.10.32.0/19",
58+
"Tags": [
59+
{
60+
"Key": "Name",
61+
"Value": "TestVPCPublicSubnetB"
62+
}
63+
],
64+
"AvailabilityZone": "eu-west-1b"
65+
},
66+
"Type": "AWS::EC2::Subnet"
67+
},
68+
"TestVPCPublicRouteTableAssocA": {
69+
"Properties": {
70+
"RouteTableId": {
71+
"Ref": "TestVPCPublicRouteTable"
72+
},
73+
"SubnetId": {
74+
"Ref": "TestVPCPublicSubnetA"
75+
}
76+
},
77+
"Type": "AWS::EC2::SubnetRouteTableAssociation"
78+
},
79+
"TestVPCPublicRouteTableAssocB": {
80+
"Properties": {
81+
"RouteTableId": {
82+
"Ref": "TestVPCPublicRouteTable"
83+
},
84+
"SubnetId": {
85+
"Ref": "TestVPCPublicSubnetB"
86+
}
87+
},
88+
"Type": "AWS::EC2::SubnetRouteTableAssociation"
89+
},
90+
"TestVPCVPC": {
91+
"Properties": {
92+
"CidrBlock": "10.10.0.0/16",
93+
"EnableDnsHostnames": true,
94+
"EnableDnsSupport": true,
95+
"Tags": [
96+
{
97+
"Key": "Name",
98+
"Value": "TestVPCVPC"
99+
}
100+
]
101+
},
102+
"Type": "AWS::EC2::VPC"
103+
},
104+
"TestVPCS3Endpoint": {
105+
"Properties": {
106+
"PolicyDocument": {
107+
"Version": "2012-10-17",
108+
"Statement": [
109+
{
110+
"Effect": "Allow",
111+
"Principal": "*",
112+
"Action": [
113+
"s3:PutObject",
114+
"s3:GetObject"
115+
],
116+
"Resource": "*"
117+
},
118+
{
119+
"Effect": "Allow",
120+
"Principal": "*",
121+
"Action": "s3:ListBucket",
122+
"Resource": "*"
123+
}
124+
]
125+
},
126+
"RouteTableIds": [
127+
{
128+
"Ref": "TestVPCPublicRouteTable"
129+
}
130+
],
131+
"ServiceName": "com.amazonaws.eu-west-1.s3",
132+
"VpcEndpointType": "Gateway",
133+
"VpcId": {
134+
"Ref": "TestVPCVPC"
135+
}
136+
},
137+
"Type": "AWS::EC2::VPCEndpoint"
138+
}
139+
}

0 commit comments

Comments
 (0)