Skip to content

Commit

Permalink
feat(ec2): support NAT instances, AMI lookups (#4898)
Browse files Browse the repository at this point in the history
Add support for NAT instances (as opposed to NAT gateways) on VPCs. This
change introduces the concept of a 'NAT provider', and provides two
implementations out of the box: one for gateways, one for instances.

Instances are not guarded against termination; a future implementation
should use ASGs to make sure there are always instances running.

To make it easier to pick the right AMI for the NAT instance,
add an AMI context provider, which will look up AMIs available to
the user.

Fixes #4876.
  • Loading branch information
rix0rrr authored Nov 11, 2019
1 parent a99f398 commit dca9a24
Show file tree
Hide file tree
Showing 19 changed files with 1,365 additions and 51 deletions.
32 changes: 23 additions & 9 deletions packages/@aws-cdk/aws-ec2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,20 @@ itself to 2 Availability Zones.
Therefore, to get the VPC to spread over 3 or more availability zones, you
must specify the environment where the stack will be deployed.

### Using NAT instances

By default, the `Vpc` construct will create NAT *gateways* for you, which
are managed by AWS. If you would prefer to use your own managed NAT
*instances* instead, specify a different value for the `natGatewayProvider`
property, as follows:

[using NAT instances](test/integ.nat-instances.lit.ts)

The construct will automatically search for the most recent NAT gateway AMI.
If you prefer to use a custom AMI, pass a `GenericLinuxImage` instance
for the instance's `machineImage` parameter and configure the right AMI ID
for the regions you want to deploy to.

### Advanced Subnet Configuration

If the default VPC configuration (public and private subnets spanning the
Expand Down Expand Up @@ -341,19 +355,19 @@ fleet.connections.allowDefaultPortTo(rdsDatabase, 'Fleet can access database');
AMIs control the OS that gets launched when you start your EC2 instance. The EC2
library contains constructs to select the AMI you want to use.

Depending on the type of AMI, you select it a different way.

The latest version of Amazon Linux and Microsoft Windows images are
selectable by instantiating one of these classes:
Depending on the type of AMI, you select it a different way. Here are some
examples of things you might want to use:

[example of creating images](test/example.images.lit.ts)

> NOTE: The Amazon Linux images selected will be cached in your `cdk.json`, so that your
> AutoScalingGroups don't automatically change out from under you when you're making unrelated
> changes. To update to the latest version of Amazon Linux, remove the cache entry from the `context`
> section of your `cdk.json`.
> NOTE: The AMIs selected by `AmazonLinuxImage` or `LookupImage` will be cached in
> `cdk.context.json`, so that your AutoScalingGroup instances aren't replaced while
> you are making unrelated changes to your CDK app.
>
> We will add command-line options to make this step easier in the future.
> To query for the latest AMI again, remove the relevant cache entry from
> `cdk.context.json`, or use the `cdk context` command. For more information, see
> [Runtime Context](https://docs.aws.amazon.com/cdk/latest/guide/context.html) in the CDK
> developer guide.
## VPN connections to a VPC

Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-ec2/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './connections';
export * from './instance-types';
export * from './instance';
export * from './machine-image';
export * from './nat';
export * from './network-acl';
export * from './network-acl-types';
export * from './port';
Expand Down
92 changes: 90 additions & 2 deletions packages/@aws-cdk/aws-ec2/lib/machine-image.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ssm = require('@aws-cdk/aws-ssm');
import { Construct, Stack, Token } from '@aws-cdk/core';
import { Construct, ContextProvider, Stack, Token } from '@aws-cdk/core';
import cxapi = require('@aws-cdk/cx-api');
import { UserData } from './user-data';
import { WindowsVersion } from './windows-versions';

Expand Down Expand Up @@ -230,7 +231,7 @@ export interface GenericLinuxImageProps {
/**
* Initial user data
*
* @default - Empty UserData for Windows machines
* @default - Empty UserData for Linux machines
*/
readonly userData?: UserData;
}
Expand Down Expand Up @@ -311,3 +312,90 @@ export enum OperatingSystemType {
LINUX,
WINDOWS,
}

/**
* A machine image whose AMI ID will be searched using DescribeImages.
*
* The most recent, available, launchable image matching the given filter
* criteria will be used. Looking up AMIs may take a long time; specify
* as many filter criteria as possible to narrow down the search.
*
* The AMI selected will be cached in `cdk.context.json` and the same value
* will be used on future runs. To refresh the AMI lookup, you will have to
* evict the value from the cache using the `cdk context` command. See
* https://docs.aws.amazon.com/cdk/latest/guide/context.html for more information.
*/
export class LookupMachineImage implements IMachineImage {
constructor(private readonly props: LookupMachineImageProps) {
}

public getImage(scope: Construct): MachineImageConfig {
// Need to know 'windows' or not before doing the query to return the right
// osType for the dummy value, so might as well add it to the filter.
const filters: Record<string, string[] | undefined> = {
'name': [this.props.name],
'state': ['available'],
'image-type': ['machine'],
'platform': this.props.windows ? ['windows'] : undefined,
};
Object.assign(filters, this.props.filters);

const value = ContextProvider.getValue(scope, {
provider: cxapi.AMI_PROVIDER,
props: {
owners: this.props.owners,
filters,
} as cxapi.AmiContextQuery,
dummyValue: 'ami-1234',
}).value as cxapi.AmiContextResponse;

if (typeof value !== 'string') {
throw new Error(`Response to AMI lookup invalid, got: ${value}`);
}

return {
imageId: value,
osType: this.props.windows ? OperatingSystemType.WINDOWS : OperatingSystemType.LINUX,
userData: this.props.userData
};
}
}

/**
* Properties for looking up an image
*/
export interface LookupMachineImageProps {
/**
* Name of the image (may contain wildcards)
*/
readonly name: string;

/**
* Owner account IDs or aliases
*
* @default - All owners
*/
readonly owners?: string[];

/**
* Additional filters on the AMI
*
* @see https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeImages.html
* @default - No additional filters
*/
readonly filters?: {[key: string]: string[]};

/**
* Look for Windows images
*
* @default false
*/
readonly windows?: boolean;

/**
* Custom userdata for this image
*
* @default - Empty user data appropriate for the platform type
*/
readonly userData?: UserData;
}
218 changes: 218 additions & 0 deletions packages/@aws-cdk/aws-ec2/lib/nat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import iam = require('@aws-cdk/aws-iam');
import { Instance } from './instance';
import { InstanceType } from './instance-types';
import { IMachineImage, LookupMachineImage } from "./machine-image";
import { Port } from './port';
import { SecurityGroup } from './security-group';
import { PrivateSubnet, PublicSubnet, RouterType, Vpc } from './vpc';

/**
* NAT providers
*
* Determines what type of NAT provider to create, either NAT gateways or NAT
* instance.
*
* @experimental
*/
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(): NatProvider {
return new NatGateway();
}

/**
* 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 instance(props: NatInstanceProps): NatProvider {
return new NatInstance(props);
}

/**
* Called by the VPC to configure NAT
*/
public abstract configureNat(options: ConfigureNatOptions): void;
}

/**
* Options passed by the VPC when NAT needs to be configured
*
* @experimental
*/
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 instance
*
* @experimental
*/
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
* NatProvider.instance({
* instanceType: new InstanceType('t3.micro'),
* machineImage: new GenericLinuxImage({
* 'us-east-2': 'ami-0f9c61b5a562a16af'
* })
* })
* ```
*
* @default - Latest NAT instance image
*/
readonly machineImage?: IMachineImage;

/**
* Instance type of the NAT instance
*/
readonly instanceType: InstanceType;

/**
* Name of SSH keypair to grant access to instance
*
* @default - No SSH access will be possible.
*/
readonly keyName?: string;
}

class NatGateway extends NatProvider {
public configureNat(options: ConfigureNatOptions) {
// Create the NAT gateways
const gatewayIds = new PrefSet<string>();
for (const sub of options.natSubnets) {
const gateway = sub.addNatGateway();
gatewayIds.add(sub.availabilityZone, gateway.ref);
}

// 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,
});
}
}
}

class NatInstance extends NatProvider {
constructor(private readonly props: NatInstanceProps) {
super();
}

public configureNat(options: ConfigureNatOptions) {
// Create the NAT instances. They can share a security group and a Role.
const instances = new PrefSet<Instance>();
const machineImage = this.props.machineImage || new NatInstanceImage();
const sg = new SecurityGroup(options.vpc, 'NatSecurityGroup', {
vpc: options.vpc,
description: 'Security Group for NAT instances',
});
sg.connections.allowFromAnyIpv4(Port.allTcp());

// 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: sg,
role,
keyName: this.props.keyName
});
// NAT instance routes all traffic, both ways
instances.add(sub.availabilityZone, natInstance);
}

// Add routes to them in the private subnets
for (const sub of options.privateSubnets) {
sub.addRoute('DefaultRoute', {
routerType: RouterType.INSTANCE,
routerId: instances.pick(sub.availabilityZone).instanceId,
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<A>();
private next: number = 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];
}
}

/**
* Machine image representing the latest NAT instance image
*
* @experimental
*/
export class NatInstanceImage extends LookupMachineImage {
constructor() {
super({
name: 'amzn-ami-vpc-nat-*',
owners: ['amazon'],
});
}
}
Loading

0 comments on commit dca9a24

Please sign in to comment.