From 4161a2c5902d8082a675c1a1cac834bcfa291fe1 Mon Sep 17 00:00:00 2001 From: Pierre Trespeuch Date: Mon, 21 Oct 2024 20:00:28 +0200 Subject: [PATCH] Make S3 interface endpoint depends on Gateway endpoint The S3 interface must be created after the Gateway endpoint or we get the following error at deployment: The VPC vpc-x must have a Gateway endpoint for the service. ref #2 --- src/e3/aws/troposphere/ec2/__init__.py | 41 +++++++++++++++++- .../tests_e3_aws/troposphere/ec2/ec2_test.py | 1 + .../ec2/vpc_v2_with_endpoints.json | 42 +++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/src/e3/aws/troposphere/ec2/__init__.py b/src/e3/aws/troposphere/ec2/__init__.py index 43c1231..deb471e 100644 --- a/src/e3/aws/troposphere/ec2/__init__.py +++ b/src/e3/aws/troposphere/ec2/__init__.py @@ -718,10 +718,21 @@ def __init__( self.public_subnet_ip_networks = { az: next(self.subnet_ip_networks) for az in availability_zones } + + # Handle S3 interface endpoint from this construct instead of from the + # VPCEndpointsSubnet construct as it requires a cross configuration with + # the S3 gateway endpoint + self.s3_interface_endpoint_config = None + if interface_endpoints is not None: + for idx, itf_endpoint in enumerate(interface_endpoints): + if itf_endpoint[0] == "s3": + self.s3_interface_endpoint_config = interface_endpoints.pop(idx) + break + # Add a subnet for VPC endpoints if requested self.interface_endpoints_subnet = ( None - if interface_endpoints is None + if interface_endpoints is None and self.s3_interface_endpoint_config is None else VPCEndpointsSubnet( name=f"{self.name_prefix}Endpoints", region=self.region, @@ -922,6 +933,32 @@ def s3_gateway_endpoint(self) -> ec2.VPCEndpoint | None: else None ) + @cached_property + def s3_interface_endpoint(self) -> ec2.VPCEndPoint | None: + """Return S3 VPC interface endpoint.""" + if self.s3_interface_endpoint_config: + # If no specific policy is requested use the same policy + # as the S3 gateway endpoint policy. + if not (policy_document := self.s3_interface_endpoint_config[1]): + policy_document = self.s3_endpoint_policy_document + assert policy_document is not None + assert self.s3_gateway_endpoint is not None + assert self.interface_endpoints_subnet is not None + return ec2.VPCEndpoint( + name_to_id(f"{self.name_prefix}S3InterfaceEndpoint"), + PolicyDocument=policy_document.as_dict, + PrivateDnsEnabled="true", + SecurityGroupIds=[Ref(self.interface_endpoints_subnet.security_group)], + ServiceName=f"com.amazonaws.{self.region}.s3", + SubnetIds=[Ref(self.interface_endpoints_subnet.subnet)], + VpcEndpointType="Interface", + VpcId=Ref(self.vpc), + # S3 interface endpoint requires the Gateway endpoint + # to be created first + DependsOn=self.s3_gateway_endpoint.title, + ) + return None + @cached_property def egress_to_vpc_endpoints(self) -> list[ec2.SecurityGroupRule]: """Return egress rules allowing traffic to VPC endpoints. @@ -989,4 +1026,6 @@ def resources(self, stack: Stack) -> list[AWSObject]: res.append(self.interface_endpoints_subnet) if self.s3_gateway_endpoint: res.append(self.s3_gateway_endpoint) + if self.s3_interface_endpoint: + res.append(self.s3_interface_endpoint) return res diff --git a/tests/tests_e3_aws/troposphere/ec2/ec2_test.py b/tests/tests_e3_aws/troposphere/ec2/ec2_test.py index 357798c..bbdd51c 100644 --- a/tests/tests_e3_aws/troposphere/ec2/ec2_test.py +++ b/tests/tests_e3_aws/troposphere/ec2/ec2_test.py @@ -196,6 +196,7 @@ def test_vpc_v2_with_endpoints(stack: Stack) -> None: interface_endpoints=[ ("email-smtp", None), ("logs", cloudwatch_endpoint_pd), + ("s3", None), ], s3_endpoint_policy_document=s3_endpoint_pd, ) diff --git a/tests/tests_e3_aws/troposphere/ec2/vpc_v2_with_endpoints.json b/tests/tests_e3_aws/troposphere/ec2/vpc_v2_with_endpoints.json index 686758c..5fcfda8 100644 --- a/tests/tests_e3_aws/troposphere/ec2/vpc_v2_with_endpoints.json +++ b/tests/tests_e3_aws/troposphere/ec2/vpc_v2_with_endpoints.json @@ -406,5 +406,47 @@ } }, "Type": "AWS::EC2::VPCEndpoint" + }, + "TestVPCS3InterfaceEndpoint": { + "Properties": { + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": [ + "s3:PutObject", + "s3:GetObject" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:ListBucket", + "Resource": "*" + } + ] + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Ref": "TestVPCEndpointsSecurityGroup" + } + ], + "ServiceName": "com.amazonaws.eu-west-1.s3", + "SubnetIds": [ + { + "Ref": "TestVPCEndpointsSubnet" + } + ], + "VpcEndpointType": "Interface", + "VpcId": { + "Ref": "TestVPCVPC" + } + }, + "Type": "AWS::EC2::VPCEndpoint", + "DependsOn": "TestVPCS3Endpoint" } } \ No newline at end of file