ec2: vpc interface endpoint not attaching to selected subnets
- Dominant language
- TypeScript
- Stars
- 12.9k
- Forks
- 4.6k
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 83
Description
### Describe the bug
When I run `cdk synth` the generated yaml template does not use the subnets that I specified for the VPC interface endpoint.
### Regression Issue
- [ ] Select this option if this issue appears to be a regression.
### Last Known Working CDK Library Version
_No response_
### Expected Behavior
The generated yaml template will use the subnets that I specify, e.g. the subnet I have provided to the `ec2.SubnetSelection` object, when creating the VPC interface endpoint.
### Current Behavior
Running `cdk synth` generates a yaml template like the following where there are no subnets selected in the `SubnetIds` property. This results in a VPC endpoint that does not work because it is not attached to subnets and there is therefore no route to it.
```yaml
ExampleVpcSSMEndpointB39410AA:
Type: AWS::EC2::VPCEndpoint
Properties:
PrivateDnsEnabled: true
SecurityGroupIds:
- Fn::GetAtt:
- ExampleVpcSSMEndpointSecurityGroup8A8CCA72
- GroupId
ServiceName:
Fn::Join:
- ""
- - com.amazonaws.
- Ref: AWS::Region
- .ssm
SubnetIds: []
Tags:
- Key: Name
Value: ExampleAppStack/ExampleVpc
VpcEndpointType: Interface
VpcId:
Ref: ExampleVpc7799291B
Metadata:
aws:cdk:path: ExampleAppStack/ExampleVpc/SSMEndpoint/Resource
```
I have other stacks that use the exact same code for subnet selection and they synth correctly, the only difference being is that those stacks only have a single private subnet. My understanding is that it's fine to have more than one private subnet in a VPC, but you can only associate a single subnet per AZ with the VPC interface, so it feels like this should work since I am only selecting a single subnet.
### Reproduction Steps
Example reproduction stack (created from `cdk init app --language python` with the following code pasted into `example_app_stack.py`)
```python
from aws_cdk import (
Stack,
aws_ec2 as ec2
)
from constructs import Construct
class ExampleAppStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
self.vpc = ec2.Vpc(
self,
"ExampleVpc",
ip_addresses=ec2.IpAddresses.cidr("10.0.0.0/16"),
max_azs=3, # Use up to 3 AZs for high availability
nat_gateways=0,
subnet_configuration=[],
create_internet_gateway=False,
enable_dns_support=True,
enable_dns_hostnames=True,
)
self.default_network_acl = ec2.CfnNetworkAcl(
self,
"DefaultNetworkAcl",
vpc_id=self.vpc.vpc_id,
)
self.igw = ec2.CfnInternetGateway(self, "InternetGateway")
# Attach Internet Gateway to VPC
self.igw_attachment = ec2.CfnVPCGatewayAttachment(
self,
"IGWAttachment",
vpc_id=self.vpc.vpc_id,
internet_gateway_id=self.igw.ref,
)
# Ensure IGW is created before attachment
self.igw_attachment.add_dependency(self.igw)
# Create public subnet
self.public_subnet = ec2.CfnSubnet(
self,
"PublicSubnet",
vpc_id=self.vpc.vpc_id,
cidr_block="10.0.1.0/24",
availability_zone="ap-southeast-2a",
map_public_ip_on_launch=False,
)
self.eip = ec2.CfnEIP(self, "NATGatewayEIP", domain="vpc")
self.nat_gateway = ec2.CfnNatGateway(
self,
"NATGateway",
subnet_id=self.public_subnet.ref,
allocation_id=self.eip.attr_allocation_id,
)
self.public_route_table = ec2.CfnRouteTable(
self,
"PublicRouteTable",
vpc_id=self.vpc.vpc_id,
)
self.public_route = ec2.CfnRoute(
self,
"PublicRoute",
route_table_id=self.public_route_table.ref,
destination_cidr_block="0.0.0.0/0",
gateway_id=self.igw.ref,
)
self.public_route.add_dependency(self.igw_attachment)
ec2.CfnSubnetRouteTableAssociation(
self,
"PublicSubnetRouteTableAssociation",
subnet_id=self.public_subnet.ref,
route_table_id=self.public_route_table.ref,
)
# Create private subnets
self.private_subnet_a = ec2.CfnSubnet(
self,
id="PrivateSubnetA",
vpc_id=self.vpc.vpc_id,
cidr_block="10.0.2.0/24",
availability_zone="ap-southeast-2a",
map_public_ip_on_launch=False,
)
self.private_subnet_b = ec2.CfnSubnet(
self,
id="PrivateSubnetB",
vpc_id=self.vpc.vpc_id,
cidr_block="10.0.3.0/24",
availability_zone="ap-southeast-2a",
map_public_ip_on_launch=False,
)
self.private_route_table = ec2.CfnRouteTable(
self,
"PrivateRouteTable",
vpc_id=self.vpc.vpc_id,
)
self.private_route = ec2.CfnRoute(
self,
"PrivateRoute",
route_table_id=self.private_route_table.ref,
destination_cidr_block="0.0.0.0/0",
nat_gateway_id=self.nat_gateway.ref,
)
# Ensure the NAT Gateway is created before creating the route
self.private_route.add_dependency(self.nat_gateway)
# Associate private route table with private subnets
ec2.CfnSubnetRouteTableAssociation(
self,
"PrivateSubnetARouteTableAssociation",
subnet_id=self.private_subnet_a.ref,
route_table_id=self.private_route_table.ref,
)
ec2.CfnSubnetRouteTableAssociation(
self,
"PrivateSubnetBRouteTableAssociation",
subnet_id=self.private_subnet_b.ref,
route_table_id=self.private_route_table.ref,
)
# SSM endpoint
self.ssm_endpoint = self.vpc.add_interface_endpoint(
"SSMEndpoint",
service=ec2.InterfaceVpcEndpointAwsService.SSM,
subnets=ec2.SubnetSelection(subnets=[self.private_subnet_a]),
)
# SSM Messages endpoint
self.ssm_messages_endpoint = self.vpc.add_interface_endpoint(
"SSMMessagesEndpoint",
service=ec2.InterfaceVpcEndpointAwsService.SSM_MESSAGES,
subnets=ec2.SubnetSelection(subnets=[self.private_subnet_a]),
)
# EC2 Messages endpoint
self.ec2_messages_endpoint = self.vpc.add_interface_endpoint(
"EC2MessagesEndpoint",
service=ec2.InterfaceVpcEndpointAwsService.EC2_MESSAGES,
subnets=ec2.SubnetSelection(subnets=[self.private_subnet_a]),
)
```
### Possible Solution
_No response_
### Additional Information/Context
requirements.txt has the following versions:
```
aws-cdk-lib>=2.239.0,<3.0.0
constructs>=10.5.0,<11.0.0
```
### AWS CDK Library version (aws-cdk-lib)
2.241.0
### AWS CDK CLI version
2.1107.0 (build e51b1ae)
### Node.js Version
v24.11.1
### OS
Ubuntu-24.04 running in windows subsystem for linux
### Language
Python
### Language Version
3.10.19
### Other information
_No response_
Contributor guide
Research direction
Start with the reproduction in example_app_stack.py and the Vpc.add_interface_endpoint entry point, then run cdk synth and inspect the generated AWS CloudFormation template. Done means each interface endpoint's SubnetIds contains the selected private_subnet_a instead of an empty list.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- aws, python, typescript
- Domain
- cloud, infrastructure, networking
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100