How to ensure that AWS Elastic IPs remain the same after deploying a CDK Stack containing a VPC?

357 Views Asked by At

I am deploying a CDK Stack that includes a VPC with static Elastic IPs in AWS.

I want to ensure that the IPs remain the same even if I redeploy the whole Stack containing the VPC, as I need to avoid having users update their API keys with the new whitelist IPs (re-generated on deploy).

I am currently "creating" the Elastic IPs using the CDK code below:

const natGatewayProvider = NatInstanceProvider.instance({
    instanceType: new InstanceType('t3.micro')
});
const vpcFargate = new Vpc(stack, 'WhiteVpcFargate', {
    vpcName: 'white-vpc-fargate',
    natGateways: 1, //  Automatically creates an Elastic IP
    natGatewayProvider: natGatewayProvider,
    maxAzs: 2
});

// I'm creating more EIPs and associating to that NAT
const allEIPs = natGatewayProvider.configuredGateways.map((nat, index) =>{
        for(let i=0;i<3;i++){
            new CfnEIP(stack, `NatInstanceEIP${index + 1}_${i}`, {
                instanceId: nat.gatewayId,
                tags: [
                    { key: 'Name', value: `NatInstanceEIP${index + 1}_${i}` },
                ],
            })
        }
    }
)

What is the best practice to ensure that the Elastic IPs remain the same even after redeploying the whole Stack containing the VPC?

I think I should create the EIPs in a different CDK Stack, create them manually with the CLI, or create them on the Cloud Console, and then somehow reference those EIPs on the NAT Gateway definition in AWS CDK doing the Associations using CfnEIPAssociation, but I'm not really sure if that's the right path to follow.

1

There are 1 best solutions below

0
Florian Sabani On

Probably not the best solution but I end up creating the EIPs and then associating them with the NAT.

The first part of EIPs creation has to be done outside CDK, so I'm using an aws-sdk one-time NodeJs script.

EIPs One Time Creation:


import AWS, {EC2} from 'aws-sdk';
const ec2 = new AWS.EC2({region: 'use-your-region-here'});

const createElasticIPs = async (count: number): Promise<any[]> => {
    const elasticIps: any[] = [];
    for (let i = 0; i < count; i++) {
        const result = await ec2.allocateAddress({
            Domain: 'vpc'
        }).promise();
        console.log(`Created Elastic IP ${result.PublicIp} with Allocation ID ${result.AllocationId}`);
        elasticIps.push({
            AllocationId: result.AllocationId ?? 'no-allocation-id',
            PublicIp: result.PublicIp ?? 'no-public-ip'
        });
    }
    return elasticIps;
};

I run this method and used the AllocationIds printed in the output to setup the NAT Gateway using AWS CDK. (If you lost the logs of the execution of the createElasticIPs method, you can still see the AllocationId of each EIP using AWS Console).

CDK NAT Gateway Definition:

    const natGatewayProvider = new CustomNatProvider({
        allocationIds: [
            'eipalloc-1', // <-- Use those your one-time script logged
            'eipalloc-2', // <-- Use those your one-time script logged
            'eipalloc-3', // <-- Use those your one-time script logged
        ]
    });

    const vpcFargate = new Vpc(stack, 'WhiteVpcFargate', {
        natGateways: 1,
        natGatewayProvider: natGatewayProvider,
        maxAzs: 2
    });

Probably instead of setting them hard-coded I should pull the ids using CDK...

Here the implementation of the CustomNatProvider:

import {
    CfnNatGateway,
    ConfigureNatOptions,
    GatewayConfig,
    NatProvider,
    PrivateSubnet,
    RouterType
} from "aws-cdk-lib/aws-ec2";

export interface MyNatGatewayProps {
    allocationIds: string[];
}

export class CustomNatProvider extends NatProvider {

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

    private allocationIds: string[] = [];

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

    public configureNat(options: 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: PrivateSubnet) {
        const az = subnet.availabilityZone;
        const gatewayId = this.gateways.pick(az);
        subnet.addRoute('DefaultRoute', {
            routerType: RouterType.NAT_GATEWAY,
            routerId: gatewayId,
            enablesInternetConnectivity: true,
        });
    }

    public get configuredGateways(): 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;
    }
}