Skip to content

(@aws-cdk/aws-ec2-alpha): vpc.addInternetGateway cannot handle multiple subnets with shared routetable #33672

Closed
@domestic-fennec

Description

@domestic-fennec

Describe the bug

If a VPC has 2 public subnets that share a single routing table, calling vpc.addInternetGateway with no options will attempt to add a route to the IGW to the route table for each subnet. This causes the cloudformation update to fail with a message like:

rtb-0c507470f627d6415|0.0.0.0/0 already exists in stack
I presume this error is coming from ec2 itself because it doesn't like 2 routes with the destination in a single route table.

It is possible to work around this by using seperate route tables for each subnet and whilst it's generally good practice to use one route table per subnet, shared route tables are legal.

It'a also possible to manually supplying single subnet to vpc.addInternetGateway, but it looks confusing semantically.

    const publicRouteTable = new ec2a.RouteTable(this, "RouteTable", {
      vpc: this.vpc,
    });

    const publicSubnetA = new ec2a.SubnetV2(this, "PublicSubnetA", {
      // abridged 
      routeTable: publicRouteTable,
    });

    const publicSubnetB = new ec2a.SubnetV2(this, "PublicSubnetB", {
     // abridged
      routeTable: publicRouteTable,
    });

   this.vpc.addInternetGateway({
     subnets: [publicSubnetA]
   });

Regression Issue

  • Select this option if this issue appears to be a regression.

Last Known Working CDK Version

No response

Expected Behavior

That the vpc.addInternetGateway should not try to add identical routes to the same routetable.

Current Behavior

That the vpc.addInternetGateway tries to add identical routes to the same routetable.

Reproduction Steps

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";

import * as ec2a from "@aws-cdk/aws-ec2-alpha";
import * as ec2 from "aws-cdk-lib/aws-ec2";

export interface NetworkProps {
  primaryAz: string;
  secondaryAz: string;
}

export class Network extends Construct {
  public readonly vpc: ec2a.VpcV2;

  constructor(scope: Construct, id: string, props: NetworkProps) {
    super(scope, id);

    this.vpc = new ec2a.VpcV2(this, "Vpc", {
      primaryAddressBlock: ec2a.IpAddresses.ipv4("192.168.0.0/20"),
    });

    const publicRouteTable = new ec2a.RouteTable(this, "RouteTable", {
      vpc: this.vpc,
    });

    const publicSubnetA = new ec2a.SubnetV2(this, "PublicSubnetA", {
      vpc: this.vpc,
      availabilityZone: props.primaryAz,
      subnetType: ec2.SubnetType.PUBLIC,
      ipv4CidrBlock: new ec2a.IpCidr("192.168.0.0/25"),
      routeTable: publicRouteTable,
    });

    const publicSubnetB = new ec2a.SubnetV2(this, "PublicSubnetB", {
      vpc: this.vpc,
      availabilityZone: props.secondaryAz,
      subnetType: ec2.SubnetType.PUBLIC,
      ipv4CidrBlock: new ec2a.IpCidr("192.168.0.128/25"),
      routeTable: publicRouteTable,
    });

   this.vpc.addInternetGateway();
  }
};

Possible Solution

As vpc.addInternetGateway internally iterates over subnets, and the method it uses internally is vpc.addDefaultInternetRoute, I'm not sure where the cleanest place to fix this is.
Maybe addInternetGateway should iterate the subnets to produce a unique list of route tables, and addDefaultInternetRoute should operate on route tables not subnets, as semantically the route is added to the table, not the subnet. You would need some way of telling addDefaultInternetRoute if it needed to added optional IPv6 routes as well.

Here is very niave approach:

  /**
   * Adds a new Internet Gateway to this VPC
   *
   * @default - creates a new route for public subnets(with all outbound access) to the Internet Gateway.
   */
  public addInternetGateway(options?: InternetGatewayOptions): void {
    if (this._internetGatewayId) {
      throw new Error("The Internet Gateway has already been enabled.");
    }

    const igw = new InternetGateway(this, "InternetGateway", {
      vpc: this,
      internetGatewayName: options?.internetGatewayName,
    });

    this._internetConnectivityEstablished.add(igw);
    this._internetGatewayId = igw.routerTargetId;

    // Use Map to ensure unique RouteTables, value is "hasIpV6CidrBlock"
    const routeTables = new Map<RouteTable, boolean>();

    // Add routes for subnets defined as an input
    if (options?.subnets) {
      // Use Set to ensure unique subnets
      const processedSubnets = new Set<string>();
      const subnets = flatten(options.subnets.map((s) => this.selectSubnets(s).subnets));
      subnets.forEach((subnet) => {
        if (!processedSubnets.has(subnet.node.id)) {
          if (!this.publicSubnets.includes(subnet)) {
            Annotations.of(this).addWarningV2(
              "InternetGatewayWarning",
              `Subnet ${subnet.node.id} is not a public subnet. Internet Gateway should be added only to public subnets.`,
            );
          }
          // If any previous subnet on this routetable has ipv6 then keep the ipv6 flag
          routeTables.set(subnet.routeTable, !!routeTables.get(subnet.routeTable) || !!subnet.ipv6CidrBlock);
          processedSubnets.add(subnet.node.id);
        }
      }); // If there are no input subnets defined, default route will be added to all public subnets
    } else if (!options?.subnets && this.publicSubnets) {
      this.publicSubnets.forEach((publicSubnets) =>
        routeTables.set(publicSubnets.routeTable, !!publicSubnets.ipv6CidrBlock),
      );
    }

    routeTables.forEach((ipv6Required, routeTable) =>
      this.addDefaultInternetRoute(routeTable, ipv6Required, igw, options),
    );
  }

  /**
   * Adds default route for the internet gateway
   * @internal
   */
  private addDefaultInternetRoute(
    routeTable: RouteTable,
    ipv6Required: boolean,
    igw: InternetGateway,
    options?: InternetGatewayOptions,
  ): void {
    // Add default route to IGW for IPv6
    if (ipv6Required) {
      new Route(this, `${subnet.node.id}-DefaultIPv6Route`, {
        routeTable: subnet.routeTable,
        destination: options?.ipv6Destination ?? "::/0",
        target: { gateway: igw },
        routeName: "CDKDefaultIPv6Route",
      });
    }
    // Add default route to IGW for IPv4
    new Route(this, `${subnet.node.id}-DefaultRoute`, {
      routeTable: subnet.routeTable,
      destination: options?.ipv4Destination ?? "0.0.0.0/0",
      target: { gateway: igw },
      routeName: "CDKDefaultIPv4Route",
    });
  }

Additional Information/Context

No response

CDK CLI Version

2.1000.3

Framework Version

No response

Node.js Version

v20.13.1

OS

OSX 15.3.1

Language

TypeScript

Language Version

TypeScript 5.6.3

Other information

No response

Metadata

Metadata

Assignees

Labels

@aws-cdk/aws-ec2Related to Amazon Elastic Compute CloudbugThis issue is a bug.effort/mediumMedium work item – several days of effortp1

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions