packages/aws-cdk-lib/aws-ec2/lib/nat.ts (298 lines of code) (raw):
import { Connections, IConnectable } from './connections';
import { Instance } from './instance';
import { InstanceArchitecture, InstanceType } from './instance-types';
import { IKeyPair } from './key-pair';
import { CpuCredits } from './launch-template';
import { AmazonLinuxCpuType, AmazonLinuxGeneration, AmazonLinuxImage, IMachineImage, LookupMachineImage } from './machine-image';
import { Port } from './port';
import { ISecurityGroup, SecurityGroup } from './security-group';
import { UserData } from './user-data';
import { PrivateSubnet, PublicSubnet, RouterType, Vpc } from './vpc';
import * as iam from '../../aws-iam';
import { Fn, Token, UnscopedValidationError } from '../../core';
/**
* Direction of traffic to allow all by default.
*/
export enum NatTrafficDirection {
/**
* Allow all outbound traffic and disallow all inbound traffic.
*/
OUTBOUND_ONLY = 'OUTBOUND_ONLY',
/**
* Allow all outbound and inbound traffic.
*/
INBOUND_AND_OUTBOUND = 'INBOUND_AND_OUTBOUND',
/**
* Disallow all outbound and inbound traffic.
*/
NONE = 'NONE',
}
/**
* Pair represents a gateway created by NAT Provider
*/
export interface GatewayConfig {
/**
* Availability Zone
*/
readonly az: string;
/**
* Identity of gateway spawned by the provider
*/
readonly gatewayId: string;
}
/**
* NAT providers
*
* Determines what type of NAT provider to create, either NAT gateways or NAT
* instance.
*
*
*/
export abstract class NatProvider {
/**
* Use NAT Gateways to provide NAT services for your VPC
*
* NAT gateways are managed by AWS.
*
* @see https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html
*/
public static gateway(props: NatGatewayProps = {}): NatProvider {
return new NatGatewayProvider(props);
}
/**
* Use NAT instances to provide NAT services for your VPC
*
* NAT instances are managed by you, but in return allow more configuration.
*
* Be aware that instances created using this provider will not be
* automatically replaced if they are stopped for any reason. You should implement
* your own NatProvider based on AutoScaling groups if you need that.
*
* @see https://docs.aws.amazon.com/vpc/latest/userguide/VPC_NAT_Instance.html
*
* @deprecated use instanceV2. 'instance' is deprecated since NatInstanceProvider
* uses a instance image that has reached EOL on Dec 31 2023
*/
public static instance(props: NatInstanceProps): NatInstanceProvider {
return new NatInstanceProvider(props);
}
/**
* Use NAT instances to provide NAT services for your VPC
*
* NAT instances are managed by you, but in return allow more configuration.
*
* Be aware that instances created using this provider will not be
* automatically replaced if they are stopped for any reason. You should implement
* your own NatProvider based on AutoScaling groups if you need that.
*
* @see https://docs.aws.amazon.com/vpc/latest/userguide/VPC_NAT_Instance.html
*/
public static instanceV2(props: NatInstanceProps): NatInstanceProviderV2 {
return new NatInstanceProviderV2(props);
}
/**
* Return list of gateways spawned by the provider
*/
public abstract readonly configuredGateways: GatewayConfig[];
/**
* Called by the VPC to configure NAT
*
* Don't call this directly, the VPC will call it automatically.
*/
public abstract configureNat(options: ConfigureNatOptions): void;
/**
* Configures subnet with the gateway
*
* Don't call this directly, the VPC will call it automatically.
*/
public abstract configureSubnet(subnet: PrivateSubnet): void;
}
/**
* Options passed by the VPC when NAT needs to be configured
*
*
*/
export interface ConfigureNatOptions {
/**
* The VPC we're configuring NAT for
*/
readonly vpc: Vpc;
/**
* The public subnets where the NAT providers need to be placed
*/
readonly natSubnets: PublicSubnet[];
/**
* The private subnets that need to route through the NAT providers.
*
* There may be more private subnets than public subnets with NAT providers.
*/
readonly privateSubnets: PrivateSubnet[];
}
/**
* Properties for a NAT gateway
*
*/
export interface NatGatewayProps {
/**
* EIP allocation IDs for the NAT gateways
*
* @default - No fixed EIPs allocated for the NAT gateways
*/
readonly eipAllocationIds?: string[];
}
/**
* Properties for a NAT instance
*
*
*/
export interface NatInstanceProps {
/**
* The machine image (AMI) to use
*
* By default, will do an AMI lookup for the latest NAT instance image.
*
* If you have a specific AMI ID you want to use, pass a `GenericLinuxImage`. For example:
*
* ```ts
* ec2.NatProvider.instance({
* instanceType: new ec2.InstanceType('t3.micro'),
* machineImage: new ec2.GenericLinuxImage({
* 'us-east-2': 'ami-0f9c61b5a562a16af'
* })
* })
* ```
*
* @default - Latest NAT instance image
*/
readonly machineImage?: IMachineImage;
/**
* Instance type of the NAT instance
*/
readonly instanceType: InstanceType;
/**
* Whether to associate a public IP address to the primary network interface attached to this instance.
*
* @default undefined - No public IP address associated
*/
readonly associatePublicIpAddress?: boolean;
/**
* Name of SSH keypair to grant access to instance
*
* @default - No SSH access will be possible.
* @deprecated - Use `keyPair` instead - https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2-readme.html#using-an-existing-ec2-key-pair
*/
readonly keyName?: string;
/**
* The SSH keypair to grant access to the instance.
*
* @default - No SSH access will be possible.
*/
readonly keyPair?: IKeyPair;
/**
* Security Group for NAT instances
*
* @default - A new security group will be created
* @deprecated - Cannot create a new security group before the VPC is created,
* and cannot create the VPC without the NAT provider.
* Set {@link defaultAllowedTraffic} to {@link NatTrafficDirection.NONE}
* and use {@link NatInstanceProviderV2.gatewayInstances} to retrieve
* the instances on the fly and add security groups
*
* @example
* const natGatewayProvider = ec2.NatProvider.instanceV2({
* instanceType: new ec2.InstanceType('t3.small'),
* defaultAllowedTraffic: ec2.NatTrafficDirection.NONE,
* });
* const vpc = new ec2.Vpc(this, 'Vpc', { natGatewayProvider });
*
* const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', {
* vpc,
* allowAllOutbound: false,
* });
* securityGroup.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443));
* for (const gatewayInstance of natGatewayProvider.gatewayInstances) {
* gatewayInstance.addSecurityGroup(securityGroup);
* }
*/
readonly securityGroup?: ISecurityGroup;
/**
* Allow all inbound traffic through the NAT instance
*
* If you set this to false, you must configure the NAT instance's security
* groups in another way, either by passing in a fully configured Security
* Group using the `securityGroup` property, or by configuring it using the
* `.securityGroup` or `.connections` members after passing the NAT Instance
* Provider to a Vpc.
*
* @default true
* @deprecated - Use `defaultAllowedTraffic`.
*/
readonly allowAllTraffic?: boolean;
/**
* Direction to allow all traffic through the NAT instance by default.
*
* By default, inbound and outbound traffic is allowed.
*
* If you set this to another value than INBOUND_AND_OUTBOUND, you must
* configure the NAT instance's security groups in another way, either by
* passing in a fully configured Security Group using the `securityGroup`
* property, or by configuring it using the `.securityGroup` or
* `.connections` members after passing the NAT Instance Provider to a Vpc.
*
* @default NatTrafficDirection.INBOUND_AND_OUTBOUND
*/
readonly defaultAllowedTraffic?: NatTrafficDirection;
/**
* Specifying the CPU credit type for burstable EC2 instance types (T2, T3, T3a, etc).
* The unlimited CPU credit option is not supported for T3 instances with dedicated host (`host`) tenancy.
*
* @default - T2 instances are standard, while T3, T4g, and T3a instances are unlimited.
*/
readonly creditSpecification?: CpuCredits;
/**
* Custom user data to run on the NAT instances
*
* @default UserData.forLinux().addCommands(...NatInstanceProviderV2.DEFAULT_USER_DATA_COMMANDS); - Appropriate user data commands to initialize and configure the NAT instances
* @see https://docs.aws.amazon.com/vpc/latest/userguide/VPC_NAT_Instance.html#create-nat-ami
*/
readonly userData?: UserData;
}
/**
* Provider for NAT Gateways
*/
export class NatGatewayProvider extends NatProvider {
private gateways: PrefSet<string> = new PrefSet<string>();
constructor(private readonly props: NatGatewayProps = {}) {
super();
}
public configureNat(options: ConfigureNatOptions) {
if (
this.props.eipAllocationIds != null
&& !Token.isUnresolved(this.props.eipAllocationIds)
&& this.props.eipAllocationIds.length < options.natSubnets.length
) {
throw new UnscopedValidationError(`Not enough NAT gateway EIP allocation IDs (${this.props.eipAllocationIds.length} provided) for the requested subnet count (${options.natSubnets.length} needed).`);
}
// Create the NAT gateways
let i = 0;
for (const sub of options.natSubnets) {
const eipAllocationId = this.props.eipAllocationIds ? pickN(i, this.props.eipAllocationIds) : undefined;
const gateway = sub.addNatGateway(eipAllocationId);
this.gateways.add(sub.availabilityZone, gateway.ref);
i++;
}
// 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 => ({ az: x[0], gatewayId: x[1] }));
}
}
/**
* NAT provider which uses NAT Instances
*
* @deprecated use NatInstanceProviderV2. NatInstanceProvider is deprecated since
* the instance image used has reached EOL on Dec 31 2023
*/
export class NatInstanceProvider extends NatProvider implements IConnectable {
private gateways: PrefSet<Instance> = new PrefSet<Instance>();
private _securityGroup?: ISecurityGroup;
private _connections?: Connections;
constructor(private readonly props: NatInstanceProps) {
super();
if (props.defaultAllowedTraffic !== undefined && props.allowAllTraffic !== undefined) {
throw new UnscopedValidationError('Can not specify both of \'defaultAllowedTraffic\' and \'defaultAllowedTraffic\'; prefer \'defaultAllowedTraffic\'');
}
if (props.keyName && props.keyPair) {
throw new UnscopedValidationError('Cannot specify both of \'keyName\' and \'keyPair\'; prefer \'keyPair\'');
}
}
public configureNat(options: ConfigureNatOptions) {
const defaultDirection = this.props.defaultAllowedTraffic ??
(this.props.allowAllTraffic ?? true ? NatTrafficDirection.INBOUND_AND_OUTBOUND : NatTrafficDirection.OUTBOUND_ONLY);
// Create the NAT instances. They can share a security group and a Role.
const machineImage = this.props.machineImage ?? new NatInstanceImage();
this._securityGroup = this.props.securityGroup ?? new SecurityGroup(options.vpc, 'NatSecurityGroup', {
vpc: options.vpc,
description: 'Security Group for NAT instances',
allowAllOutbound: isOutboundAllowed(defaultDirection),
});
this._connections = new Connections({ securityGroups: [this._securityGroup] });
if (isInboundAllowed(defaultDirection)) {
this.connections.allowFromAnyIpv4(Port.allTraffic());
}
// FIXME: Ideally, NAT instances don't have a role at all, but
// 'Instance' does not allow that right now.
const role = new iam.Role(options.vpc, 'NatRole', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
});
for (const sub of options.natSubnets) {
const natInstance = new Instance(sub, 'NatInstance', {
instanceType: this.props.instanceType,
machineImage,
sourceDestCheck: false, // Required for NAT
vpc: options.vpc,
vpcSubnets: { subnets: [sub] },
securityGroup: this._securityGroup,
role,
keyPair: this.props.keyPair,
keyName: this.props.keyName,
creditSpecification: this.props.creditSpecification,
});
// NAT instance routes all traffic, both ways
this.gateways.add(sub.availabilityZone, natInstance);
}
// Add routes to them in the private subnets
for (const sub of options.privateSubnets) {
this.configureSubnet(sub);
}
}
/**
* The Security Group associated with the NAT instances
*/
public get securityGroup(): ISecurityGroup {
if (!this._securityGroup) {
throw new UnscopedValidationError('Pass the NatInstanceProvider to a Vpc before accessing \'securityGroup\'');
}
return this._securityGroup;
}
/**
* Manage the Security Groups associated with the NAT instances
*/
public get connections(): Connections {
if (!this._connections) {
throw new UnscopedValidationError('Pass the NatInstanceProvider to a Vpc before accessing \'connections\'');
}
return this._connections;
}
public get configuredGateways(): GatewayConfig[] {
return this.gateways.values().map(x => ({ az: x[0], gatewayId: x[1].instanceId }));
}
public configureSubnet(subnet: PrivateSubnet) {
const az = subnet.availabilityZone;
const gatewayId = this.gateways.pick(az).instanceId;
subnet.addRoute('DefaultRoute', {
routerType: RouterType.INSTANCE,
routerId: gatewayId,
enablesInternetConnectivity: true,
});
}
}
/**
* Preferential set
*
* Picks the value with the given key if available, otherwise distributes
* evenly among the available options.
*/
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 UnscopedValidationError('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;
}
}
/**
* Modern NAT provider which uses NAT Instances.
* The instance uses Amazon Linux 2023 as the operating system.
*/
export class NatInstanceProviderV2 extends NatProvider implements IConnectable {
/**
* Amazon Linux 2023 NAT instance user data commands
* Enable iptables on the instance, enable persistent IP forwarding, configure NAT on instance
* @see https://docs.aws.amazon.com/vpc/latest/userguide/VPC_NAT_Instance.html#create-nat-ami
*/
public static readonly DEFAULT_USER_DATA_COMMANDS = [
'yum install iptables-services -y',
'systemctl enable iptables',
'systemctl start iptables',
'echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/custom-ip-forwarding.conf',
'sudo sysctl -p /etc/sysctl.d/custom-ip-forwarding.conf',
"sudo /sbin/iptables -t nat -A POSTROUTING -o $(route | awk '/^default/{print $NF}') -j MASQUERADE",
'sudo /sbin/iptables -F FORWARD',
'sudo service iptables save',
];
private gateways: PrefSet<Instance> = new PrefSet<Instance>();
private _securityGroup?: ISecurityGroup;
private _connections?: Connections;
/**
* Array of gateway instances spawned by the provider after internal configuration
*/
public get gatewayInstances(): Instance[] {
return this.gateways.values().map(([, instance]) => instance);
}
constructor(private readonly props: NatInstanceProps) {
super();
if (props.defaultAllowedTraffic !== undefined && props.allowAllTraffic !== undefined) {
throw new UnscopedValidationError('Can not specify both of \'defaultAllowedTraffic\' and \'defaultAllowedTraffic\'; prefer \'defaultAllowedTraffic\'');
}
if (props.keyName && props.keyPair) {
throw new UnscopedValidationError('Cannot specify both of \'keyName\' and \'keyPair\'; prefer \'keyPair\'');
}
}
public configureNat(options: ConfigureNatOptions) {
const defaultDirection = this.props.defaultAllowedTraffic ??
(this.props.allowAllTraffic ?? true ? NatTrafficDirection.INBOUND_AND_OUTBOUND : NatTrafficDirection.OUTBOUND_ONLY);
// Create the NAT instances. They can share a security group and a Role. The new NAT instance created uses latest
// Amazon Linux 2023 image. This is important since the original NatInstanceProvider uses an instance image that has
// reached EOL on Dec 31 2023
const machineImage = this.props.machineImage || new AmazonLinuxImage({
generation: AmazonLinuxGeneration.AMAZON_LINUX_2023,
cpuType: this.props.instanceType.architecture == InstanceArchitecture.ARM_64 ? AmazonLinuxCpuType.ARM_64 : undefined,
});
this._securityGroup = this.props.securityGroup ?? new SecurityGroup(options.vpc, 'NatSecurityGroup', {
vpc: options.vpc,
description: 'Security Group for NAT instances',
allowAllOutbound: isOutboundAllowed(defaultDirection),
});
this._connections = new Connections({ securityGroups: [this._securityGroup] });
if (isInboundAllowed(defaultDirection)) {
this.connections.allowFromAnyIpv4(Port.allTraffic());
}
let userData = this.props.userData;
if (!userData) {
userData = UserData.forLinux();
userData.addCommands(...NatInstanceProviderV2.DEFAULT_USER_DATA_COMMANDS);
}
for (const sub of options.natSubnets) {
const natInstance = new Instance(sub, 'NatInstance', {
instanceType: this.props.instanceType,
machineImage,
sourceDestCheck: false, // Required for NAT
vpc: options.vpc,
vpcSubnets: { subnets: [sub] },
associatePublicIpAddress: this.props.associatePublicIpAddress,
securityGroup: this._securityGroup,
keyPair: this.props.keyPair,
keyName: this.props.keyName,
creditSpecification: this.props.creditSpecification,
userData,
});
// NAT instance routes all traffic, both ways
this.gateways.add(sub.availabilityZone, natInstance);
}
// Add routes to them in the private subnets
for (const sub of options.privateSubnets) {
this.configureSubnet(sub);
}
}
/**
* The Security Group associated with the NAT instances
*/
public get securityGroup(): ISecurityGroup {
if (!this._securityGroup) {
throw new UnscopedValidationError('Pass the NatInstanceProvider to a Vpc before accessing \'securityGroup\'');
}
return this._securityGroup;
}
/**
* Manage the Security Groups associated with the NAT instances
*/
public get connections(): Connections {
if (!this._connections) {
throw new UnscopedValidationError('Pass the NatInstanceProvider to a Vpc before accessing \'connections\'');
}
return this._connections;
}
public get configuredGateways(): GatewayConfig[] {
return this.gateways.values().map(x => ({ az: x[0], gatewayId: x[1].instanceId }));
}
public configureSubnet(subnet: PrivateSubnet) {
const az = subnet.availabilityZone;
const gatewayId = this.gateways.pick(az).instanceId;
subnet.addRoute('DefaultRoute', {
routerType: RouterType.INSTANCE,
routerId: gatewayId,
enablesInternetConnectivity: true,
});
}
}
/**
* Machine image representing the latest NAT instance image
*
*
*/
export class NatInstanceImage extends LookupMachineImage {
constructor() {
super({
name: 'amzn-ami-vpc-nat-*',
owners: ['amazon'],
});
}
}
function isOutboundAllowed(direction: NatTrafficDirection) {
return direction === NatTrafficDirection.INBOUND_AND_OUTBOUND ||
direction === NatTrafficDirection.OUTBOUND_ONLY;
}
function isInboundAllowed(direction: NatTrafficDirection) {
return direction === NatTrafficDirection.INBOUND_AND_OUTBOUND;
}
/**
* Token-aware pick index function
*/
function pickN(i: number, xs: string[]) {
if (Token.isUnresolved(xs)) { return Fn.select(i, xs); }
if (i >= xs.length) {
throw new UnscopedValidationError(`Cannot get element ${i} from ${xs}`);
}
return xs[i];
}