Aws-cdk: Elastic IP association for generated NAT in VPC

Created on 13 Sep 2019  路  8Comments  路  Source: aws/aws-cdk

:rocket: Feature Request

General Information

  • [ ] :wave: I may be able to implement this feature request
  • [ ] :warning: This feature might incur a breaking change

Description


It would be great to be able to create a NAT gateway and associate an EIP with it. Currently I create a VPC and that automatically generates a NAT for me. But it's not possible to alter the NAT or to associate an EIP with the generated NAT.

My use case is that I need a Fargate outbound request mapped to a static IP. This IP will be whitelisted in our on-premise datacenter.

Proposed Solution

new Vpc(this, "myVpc", {
    maxAzs: 2,
    cidr: '10.0.0.0/16',
    natGateways: 1,
    allocationIDs: ['eipalloc-12abcde34a5fab67']           
    subnetConfiguration: [
        {
            cidrMask: 24,
            name: 'sonar_nat_lb',
            subnetType: SubnetType.PUBLIC
         },
         {
            cidrMask: 24,
            name: 'sonar_fargate',
            subnetType: SubnetType.PRIVATE
          }
    ]
});

Allocation ID can be optional as the NAT Gateway will default create its own EIP.

Environment

  • CDK CLI Version: 1.8.0
  • Module Version: 1.8.0
  • OS: all
  • Language: TypeScript

Other information


On a sidenote, it is also not possible to create an EIP with CDK, but that's a different feature-request.

@aws-cdaws-ec2 feature-request

Most helpful comment

Hello.

_(Sorry for posting on a closed issue, but it's one of the very few entries coming up in a Google around that topic)_

Inspired by @kevin-lindsay-1 's entry, I did what is essentially the same using an escape hatch.
It's probably more future-proof

In Typescript:

// Specify your EIP allocations here
// You could have CDK create them too, but that kind of defeats the purpose
// of being able to reuse them outside of the lifespan of the stack.
const allocationIds: string[] = ['eipalloc-123456xxx', 'eipalloc-234567xxx'];

// Create the VPC
const vpc = new ec2.Vpc(this, 'VPC', {
  cidr: "10.0.0.0/16",
  maxAzs: 2,
})

vpc.publicSubnets.forEach((subnet, index) => {
    // Find the NAT Gateway in the subnet construct children
    var natGateway = subnet.node.children.find(child => child.node.id == 'NATGateway') as ec2.CfnNatGateway
    // Delete the default EIP created by CDK
    subnet.node.tryRemoveChild('EIP')
    // Override the allocationId on the NATGateway
    natGateway.allocationId = allocationIds[index]
})

@rix0rrr You mention the feature won't be added because it (A NATGateway with an EIP alloc.) can be done via a NatProvider. Any chance you could provide an example?

Thank you.

All 8 comments

@rafiek I still need to test this, but I have a super jank workaround until this is addressed.
@rix0rrr apologies for what you're about to witness.

// PrefSet is not exported, so grabbing it for this workaround
class PrefSet<A> {
  private readonly map: Record<string, A> = {};
  private readonly vals = new Array<A>();
  private next = 0;

  public add(pref: string, value: A) {
    this.map[pref] = value;
    this.vals.push(value);
  }

  public pick(pref: string): A {
    if (this.vals.length === 0) {
      throw new Error("Cannot pick, set is empty");
    }

    if (pref in this.map) {
      return this.map[pref];
    }
    return this.vals[this.next++ % this.vals.length];
  }
}

// allocation ids of EIPs
const allocationIds = ["eipalloc-xxx1", "eipalloc-xxx2"];

/**
 * create default NAT Gateway Provider, overriding the `configureNat` method
 * to manually specify the allocation IDs
 */
const natGatewayProvider = NatProvider.gateway();
natGatewayProvider.configureNat = options => {
  const gatewayIds = new PrefSet<string>();

  // modified
  let count = 0;

  for (const sub of options.natSubnets) {
    sub.addNatGateway = () => {
      // Create a NAT Gateway in this public subnet
      const ngw = new CfnNatGateway(sub, `NATGateway`, {
        subnetId: sub.subnetId,

        // modified
        allocationId: allocationIds[count]
      });
      return ngw;
    };
    const gateway = sub.addNatGateway();
    gatewayIds.add(sub.availabilityZone, gateway.ref);

    // modified
    count += 1;
  }

  // Add routes to them in the private subnets
  for (const sub of options.privateSubnets) {
    sub.addRoute("DefaultRoute", {
      routerType: RouterType.NAT_GATEWAY,
      routerId: gatewayIds.pick(sub.availabilityZone),
      enablesInternetConnectivity: true
    });
  }
};

const vpc = new Vpc(this, "Vpc", {
  natGatewayProvider
});

My suggestion would indeed have been to implement your custom NatProvider subclass, which configures the provider exactly the way you want it to.

@kevin-lindsay-1 has as good as done that, except done it by instantiating the existing class and then replacing its implementation, which JavaScript totally lets you do :).

I don't think this is a feature we'll add to the core library. We've added an extension point that will let you inject your customization at the point where you need it, which is the NatProvider base class.

Is the NatGatewayProvider class not exported on purpose? I'm looking at doing this janky workaround myself and wishing I could simply extend that class. Did that maybe contribute to some of the jank in the workaround by @kevin-lindsay-1?

I hope cdk can implement this feature.
Cuz on many commercial use case as far I know, they only allow whitelist IPs to call their Api. Its kinda ugly to work around.

Will run cdk deploy and force to run modifyIP.js to rewrite cloudformation template

const vpc = new ec2.Vpc(this, `VPC-${environment.ENV}`, {
      cidr: '10.0.0.0/16',
      maxAzs: 3,
      natGateways: 2,
      enableDnsHostnames: true,
      enableDnsSupport: true,
    });

    // Iterate the private subnets
    const selectionPrivate = vpc.selectSubnets({
      subnetType: ec2.SubnetType.PRIVATE,
    });
    const selectionPublic = vpc.selectSubnets({
      subnetType: ec2.SubnetType.PUBLIC,
    });

    selectionPublic.subnets[0].node.defaultChild['cidrBlock'] = '10.0.0.0/24';
    selectionPrivate.subnets[0].node.defaultChild['cidrBlock'] = '10.0.10.0/24';

    selectionPublic.subnets[1].node.defaultChild['cidrBlock'] = '10.0.1.0/24';
    selectionPrivate.subnets[1].node.defaultChild['cidrBlock'] = '10.0.11.0/24';

```javascript
// modifiyIP.js
const path1 = './packages/infra/cdk.out/infra-dev.template.json';
const rawdata = fs.readFileSync(path);
const te = JSON.parse(rawdata);
const newJson = te;
const publicIP1 = 'eipalloc-0cxxxxxd66';
const publicIP2 = 'eipalloc-0930xxxx8a6a';
delete newJson.Resources.BridgeVPCdevPublicSubnet1EIPAAC8BBEA;
delete newJson.Resources.BridgeVPCdevPublicSubnet2EIP4C41D39A;
newJson.Resources.BridgeVPCdevPublicSubnet1NATGateway90C16FAD.Properties.AllocationId = publicIP1;
newJson.Resources.BridgeVPCdevPublicSubnet2NATGatewayA1415266.Properties.AllocationId = publicIP2;
fs.writeFile(path, JSON.stringify(newJson), err => {
if (err) throw err;
console.log("It's saved!");
});

Is the NatGatewayProvider class not exported on purpose? I'm looking at doing this janky workaround myself and wishing I could simply extend that class. Did that maybe contribute to some of the jank in the workaround by @kevin-lindsay-1?

Hi @rix0rrr

I'm wondering the same question with @lbjay
As you described at https://github.com/aws/aws-cdk/issues/8327#issuecomment-638169014 the solution is to create our own NatGatewayProvider
I've look at the code of the NatGatewayProvider and I just want to override 1 line when creating the NatGateway at https://github.com/aws/aws-cdk/blob/a93534f42cb6ecf8bdde1987f0d85919c55dbacb/packages/%40aws-cdk/aws-ec2/lib/nat.ts#L173

I think, it will be more clean create a method, lets say createNat in the NatGatewayProvider and export the NatGatewayProvider

public configureNat(options: ConfigureNatOptions) {
  // Create the NAT gateways
  for (const sub of options.natSubnets) {
    const gateway = this.createNat(sub); // this line changed
    this.gateways.add(sub.availabilityZone, gateway.ref);
  }

  // Add routes to them in the private subnets
  for (const sub of options.privateSubnets) {
    this.configureSubnet(sub);
  }
}

protected function crateNat(sub) {
  return sub.addNatGateway();
}

so we can extend it by ourselves and configure the EIP

class MyNatGatewayProvider extends NatGatewayProvider {
  protected function crateNat(sub) {
    const gateway = new CfnNatGateway(sub, 'NATGateway', {
        // our own setting
    });
    return gateway;
  }
}

Without this, I have to copy all of the code of NatGatewayProvider to implement it.

Can you suggest if this ok or not?

Hello.

_(Sorry for posting on a closed issue, but it's one of the very few entries coming up in a Google around that topic)_

Inspired by @kevin-lindsay-1 's entry, I did what is essentially the same using an escape hatch.
It's probably more future-proof

In Typescript:

// Specify your EIP allocations here
// You could have CDK create them too, but that kind of defeats the purpose
// of being able to reuse them outside of the lifespan of the stack.
const allocationIds: string[] = ['eipalloc-123456xxx', 'eipalloc-234567xxx'];

// Create the VPC
const vpc = new ec2.Vpc(this, 'VPC', {
  cidr: "10.0.0.0/16",
  maxAzs: 2,
})

vpc.publicSubnets.forEach((subnet, index) => {
    // Find the NAT Gateway in the subnet construct children
    var natGateway = subnet.node.children.find(child => child.node.id == 'NATGateway') as ec2.CfnNatGateway
    // Delete the default EIP created by CDK
    subnet.node.tryRemoveChild('EIP')
    // Override the allocationId on the NATGateway
    natGateway.allocationId = allocationIds[index]
})

@rix0rrr You mention the feature won't be added because it (A NATGateway with an EIP alloc.) can be done via a NatProvider. Any chance you could provide an example?

Thank you.

funny that i have the exact same issue as @ralovely just a few hours later. I think i solved it with this, but if feels somehow hacky:

let vpc = new Vpc(this, "dsfsdf", {maxAzs: 1, natGateways: 0});
        let myPublicSubnet = vpc.publicSubnets[0];


        new CfnNatGateway(this, "myNatGw", {
            allocationId: "eipalloc-0327763839e2691a6",
            subnetId: myPublicSubnet.subnetId
        });

Furthermore i get an Isolated Subnet instead of a private one, which is not desireable. Will try other ways outlined perfectly in this issue. Thanks everyone for the valuable feedback.

@xian13 @rix0rrr @logemann

A raw example using a NatProvider. Here only one EIP was created using the console.
It needs some improvements in order to manage more EIPs and options.

import { NatProvider, CfnNatGateway } from '@aws-cdk/aws-ec2';



export interface MyNatGatewayProps {
   allocationIds: string[];
}

export class MyNatGatewayProvider extends ec2.NatProvider {

  private gateways: PrefSet<string> = new PrefSet<string>();

  private allocationIds: string[] = [];

  constructor(private props: MyNatGatewayProps) {
    super();
    this.allocationIds = props.allocationIds;
  }

  public configureNat(options: ec2.ConfigureNatOptions) {
    // Create the NAT gateways
    for (const sub of options.natSubnets) {

       if(this.allocationIds.length > 0){
          sub.addNatGateway = () => {

            const test = this.allocationIds[0]

            const ngw = new CfnNatGateway(sub, `NATGateway`, {
              subnetId: sub.subnetId,
              allocationId: this.allocationIds[0]
            });
            this.allocationIds.shift();
            return ngw;
          };
      } 

      const gateway = sub.addNatGateway();
      this.gateways.add(sub.availabilityZone, gateway.ref);
    }
    // Add routes to them in the private subnets
    for (const sub of options.privateSubnets) {
      this.configureSubnet(sub);
    }
  }

  public configureSubnet(subnet: ec2.PrivateSubnet) {
    const az = subnet.availabilityZone;
    const gatewayId = this.gateways.pick(az);
    subnet.addRoute('DefaultRoute', {
      routerType: ec2.RouterType.NAT_GATEWAY,
      routerId: gatewayId,
      enablesInternetConnectivity: true,
    });
  }

  public get configuredGateways(): ec2.GatewayConfig[] {
    return this.gateways.values().map((x: any[]) => ({ az: x[0], gatewayId: x[1] }));
  }

}

class PrefSet<A> {
  private readonly map: Record<string, A> = {};
  private readonly vals = new Array<[string, A]>();
  private next: number = 0;

  public add(pref: string, value: A) {
    this.map[pref] = value;
    this.vals.push([pref, value]);
  }

  public pick(pref: string): A {
    if (this.vals.length === 0) {
      throw new Error('Cannot pick, set is empty');
    }

    if (pref in this.map) { return this.map[pref]; }
    return this.vals[this.next++ % this.vals.length][1];
  }

  public values(): Array<[string, A]> {
    return this.vals;
  }
}



export class CdkStack extends cdk.Stack {

  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const vpc = new ec2.Vpc(this, 'VPC', 
      {
        natGatewayProvider: new MyNatGatewayProvider({
          allocationIds: ["eipalloc-03db274e099d85676"]
        }),
        cidr: "10.200.0.0/16",
        subnetConfiguration: [
          {
            cidrMask: 24,
            name: 'PUBLIC',
            subnetType: ec2.SubnetType.PUBLIC,
          },
          {
            cidrMask: 24,
            name: 'PRIVATE',
            subnetType: ec2.SubnetType.PRIVATE,
          }
        ]
      }
    );
Was this page helpful?
0 / 5 - 0 ratings