From 319e0cc752714e8da3710fe886f31ac4a174b360 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 20 Aug 2018 17:33:35 +0200 Subject: [PATCH] fix(aws-ec2): VPC properly uses complex subnet config It is now possible to pick Isolated subnets for the VPC placement strategy. Also, Isolated subnets are properly exported and imported using the VPC export()/import() functions, and it's now possible not have all three of public/private/isolated subnets in the export/import. Fixes #597. --- packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts | 126 ++++++++++++++++--- packages/@aws-cdk/aws-ec2/lib/vpc.ts | 52 +------- packages/@aws-cdk/aws-ec2/test/test.fleet.ts | 1 + packages/@aws-cdk/aws-ec2/test/test.vpc.ts | 55 ++++++++ 4 files changed, 168 insertions(+), 66 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts index 7a7500fe4b538..a6dc66fdba02b 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts @@ -1,5 +1,41 @@ import { Construct, IDependable, Output, StringListOutput, Token } from "@aws-cdk/cdk"; +/** + * The type of Subnet + */ +export enum SubnetType { + /** + * Isolated Subnets do not route Outbound traffic + * + * This can be good for subnets with RDS or + * Elasticache endpoints + */ + Isolated = 1, + + /** + * Subnet that routes to the internet, but not vice versa. + * + * Instances in a private subnet can connect to the Internet, but will not + * allow connections to be initiated from the Internet. + * + * Outbound traffic will be routed via a NAT Gateway. Preference being in + * the same AZ, but if not available will use another AZ. This is common for + * experimental cost conscious accounts or accounts where HA outbound + * traffic is not needed. + */ + Private = 2, + + /** + * Subnet connected to the Internet + * + * Instances in a Public subnet can connect to the Internet and can be + * connected to from the Internet as long as they are launched with public IPs. + * + * Public subnets route outbound traffic via an Internet Gateway. + */ + Public = 3 +} + /** * Customize how instances are placed inside a VPC * @@ -8,13 +44,13 @@ import { Construct, IDependable, Output, StringListOutput, Token } from "@aws-cd */ export interface VpcPlacementStrategy { /** - * Whether to use the VPC's public subnets to start instances + * What subnet type to place the instances in * - * If false, the instances are started in the private subnets. + * By default, the instances are placed in the private subnets. * - * @default false + * @default SubnetType.Private */ - usePublicSubnets?: boolean; + subnetsToUse?: SubnetType; } /** @@ -43,6 +79,16 @@ export abstract class VpcNetworkRef extends Construct implements IDependable { */ public abstract readonly privateSubnets: VpcSubnetRef[]; + /** + * List of isolated subnets in this VPC + */ + public abstract readonly isolatedSubnets: VpcSubnetRef[]; + + /** + * AZs for this VPC + */ + public abstract readonly availabilityZones: string[]; + /** * Parts of the VPC that constitute full construction */ @@ -51,9 +97,13 @@ export abstract class VpcNetworkRef extends Construct implements IDependable { /** * Return the subnets appropriate for the placement strategy */ - public subnets(placement?: VpcPlacementStrategy): VpcSubnetRef[] { - if (!placement) { return this.privateSubnets; } - return placement.usePublicSubnets ? this.publicSubnets : this.privateSubnets; + public subnets(placement: VpcPlacementStrategy = {}): VpcSubnetRef[] { + if (placement.subnetsToUse === undefined) { return this.privateSubnets; } + return { + [SubnetType.Isolated]: this.isolatedSubnets, + [SubnetType.Private]: this.privateSubnets, + [SubnetType.Public]: this.publicSubnets, + }[placement.subnetsToUse]; } /** @@ -62,11 +112,17 @@ export abstract class VpcNetworkRef extends Construct implements IDependable { public export(): VpcNetworkRefProps { return { vpcId: new Output(this, 'VpcId', { value: this.vpcId }).makeImportValue(), - availabilityZones: this.publicSubnets.map(s => s.availabilityZone), - publicSubnetIds: new StringListOutput(this, 'PublicSubnetIDs', { values: this.publicSubnets.map(s => s.subnetId) }).makeImportValues(), - privateSubnetIds: new StringListOutput(this, 'PrivateSubnetIDs', { values: this.privateSubnets.map(s => s.subnetId) }).makeImportValues(), + availabilityZones: this.availabilityZones, + publicSubnetIds: this.exportSubnetIds('PublicSubnetIDs', this.publicSubnets), + privateSubnetIds: this.exportSubnetIds('PrivateSubnetIDs', this.privateSubnets), + isolatedSubnetIds: this.exportSubnetIds('IsolatedSubnetIDs', this.isolatedSubnets), }; } + + private exportSubnetIds(name: string, subnets: VpcSubnetRef[]): Token[] | undefined { + if (subnets.length === 0) { return undefined; } + return new StringListOutput(this, name, { values: subnets.map(s => s.subnetId) }).makeImportValues(); + } } /** @@ -88,27 +144,50 @@ class ImportedVpcNetwork extends VpcNetworkRef { */ public readonly privateSubnets: VpcSubnetRef[]; + /** + * List of isolated subnets in this VPC + */ + public readonly isolatedSubnets: VpcSubnetRef[]; + + /** + * AZs for this VPC + */ + public readonly availabilityZones: string[]; + constructor(parent: Construct, name: string, props: VpcNetworkRefProps) { super(parent, name); this.vpcId = props.vpcId; + this.availabilityZones = props.availabilityZones; + + const privateSubnetIds = props.privateSubnetIds || []; + const publicSubnetIds = props.publicSubnetIds || []; + const isolatedSubnetIds = props.isolatedSubnetIds || []; - if (props.availabilityZones.length !== props.publicSubnetIds.length) { - throw new Error('Availability zone and public subnet ID arrays must be same length'); + if (publicSubnetIds.length > 0 && this.availabilityZones.length !== publicSubnetIds.length) { + throw new Error('Must have Public subnet for every AZ'); } - if (props.availabilityZones.length !== props.privateSubnetIds.length) { - throw new Error('Availability zone and private subnet ID arrays must be same length'); + if (privateSubnetIds.length > 0 && this.availabilityZones.length !== privateSubnetIds.length) { + throw new Error('Must have Private subnet for every AZ'); + } + + if (isolatedSubnetIds.length > 0 && this.availabilityZones.length !== isolatedSubnetIds.length) { + throw new Error('Must have Isolated subnet for every AZ'); } const n = props.availabilityZones.length; this.publicSubnets = range(n).map(i => VpcSubnetRef.import(this, `PublicSubnet${i}`, { - availabilityZone: props.availabilityZones[i], - subnetId: props.publicSubnetIds[i] + availabilityZone: this.availabilityZones[i], + subnetId: publicSubnetIds[i] })); this.privateSubnets = range(n).map(i => VpcSubnetRef.import(this, `PrivateSubnet${i}`, { - availabilityZone: props.availabilityZones[i], - subnetId: props.privateSubnetIds[i] + availabilityZone: this.availabilityZones[i], + subnetId: privateSubnetIds[i] + })); + this.isolatedSubnets = range(n).map(i => VpcSubnetRef.import(this, `IsolatedSubnet${i}`, { + availabilityZone: this.availabilityZones[i], + subnetId: isolatedSubnetIds[i] })); } } @@ -135,14 +214,21 @@ export interface VpcNetworkRefProps { * * Must match the availability zones and private subnet ids in length and order. */ - publicSubnetIds: VpcSubnetId[]; + publicSubnetIds?: VpcSubnetId[]; /** * List of private subnet IDs, one for every subnet * * Must match the availability zones and public subnet ids in length and order. */ - privateSubnetIds: VpcSubnetId[]; + privateSubnetIds?: VpcSubnetId[]; + + /** + * List of isolated subnet IDs, one for every subnet + * + * Must match the availability zones and public subnet ids in length and order. + */ + isolatedSubnetIds?: VpcSubnetId[]; } /** diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 17c2c33a368a1..03a0b9db630c3 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -2,7 +2,7 @@ import cdk = require('@aws-cdk/cdk'); import { Obj } from '@aws-cdk/util'; import { cloudformation } from './ec2.generated'; import { NetworkBuilder } from './network-util'; -import { VpcNetworkId, VpcNetworkRef, VpcSubnetId, VpcSubnetRef } from './vpc-ref'; +import { SubnetType, VpcNetworkId, VpcNetworkRef, VpcSubnetId, VpcSubnetRef } from './vpc-ref'; /** * VpcNetworkProps allows you to specify configuration options for a VPC */ @@ -117,44 +117,6 @@ export enum DefaultInstanceTenancy { Dedicated = 'dedicated' } -/** - * The type of Subnet - */ -export enum SubnetType { - - /** - * Isolated Subnets do not route Outbound traffic - * - * This can be good for subnets with RDS or - * Elasticache endpoints - */ - Isolated = 1, - - /** - * Subnet that routes to the internet, but not vice versa. - * - * Instances in a private subnet can connect to the Internet, but will not - * allow connections to be initiated from the Internet. - * - * Outbound traffic will be routed via a NAT Gateway. Preference being in - * the same AZ, but if not available will use another AZ. This is common for - * experimental cost conscious accounts or accounts where HA outbound - * traffic is not needed. - */ - Private = 2, - - /** - * Subnet connected to the Internet - * - * Instances in a Public subnet can connect to the Internet and can be - * connected to from the Internet as long as they are launched with public IPs. - * - * Public subnets route outbound traffic via an Internet Gateway. - */ - Public = 3 - -} - /** * Specify configuration parameters for a VPC to be built */ @@ -248,6 +210,11 @@ export class VpcNetwork extends VpcNetworkRef { */ public readonly isolatedSubnets: VpcSubnetRef[] = []; + /** + * AZs for this VPC + */ + public readonly availabilityZones: string[]; + /** * Maximum Number of NAT Gateways used to control cost * @@ -275,13 +242,6 @@ export class VpcNetwork extends VpcNetworkRef { */ private subnetConfiguration: SubnetConfiguration[] = []; - /** - * Maximum AZs to Uses for this VPC - * - * @default All - */ - private availabilityZones: string[]; - /** * VpcNetwork creates a VPC that spans a whole region. * It will automatically divide the provided VPC CIDR range, and create public and private subnets per Availability Zone. diff --git a/packages/@aws-cdk/aws-ec2/test/test.fleet.ts b/packages/@aws-cdk/aws-ec2/test/test.fleet.ts index 71314266b7f01..68f24ffa6b7d1 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.fleet.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.fleet.ts @@ -241,5 +241,6 @@ function mockVpc(stack: Stack) { availabilityZones: [ 'az1' ], publicSubnetIds: [ new VpcSubnetId('pub1') ], privateSubnetIds: [ new VpcSubnetId('pri1') ], + isolatedSubnetIds: [], }); } diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts index 81c51eeba65c9..5161552747032 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts @@ -255,7 +255,62 @@ export = { })); test.done(); } + }, + + 'can select public subnets'(test: Test) { + // GIVEN + const stack = getTestStack(); + const vpc = new VpcNetwork(stack, 'VPC'); + + // WHEN + const nets = vpc.subnets({ subnetsToUse: SubnetType.Public }); + + // THEN + test.deepEqual(nets, vpc.publicSubnets); + + test.done(); + }, + + 'can select isolated subnets'(test: Test) { + // GIVEN + const stack = getTestStack(); + const vpc = new VpcNetwork(stack, 'VPC', { + subnetConfiguration: [ + { subnetType: SubnetType.Private, name: 'Private' }, + { subnetType: SubnetType.Isolated, name: 'Isolated' }, + ] + }); + + // WHEN + const nets = vpc.subnets({ subnetsToUse: SubnetType.Isolated }); + + // THEN + test.deepEqual(nets, vpc.isolatedSubnets); + + test.done(); + }, + + 'can select isolated subnets after exporting and importing'(test: Test) { + // GIVEN + const stack1 = getTestStack(); + const stack2 = getTestStack(); + const vpc1 = new VpcNetwork(stack1, 'VPC', { + subnetConfiguration: [ + { subnetType: SubnetType.Private, name: 'Private' }, + { subnetType: SubnetType.Isolated, name: 'Isolated' }, + ] + }); + + const importedVpc = VpcNetwork.import(stack2, 'VPC', vpc1.export()); + + // WHEN + const nets = importedVpc.subnets({ subnetsToUse: SubnetType.Isolated }); + + // THEN + test.equal(3, importedVpc.isolatedSubnets.length); + test.deepEqual(nets, importedVpc.isolatedSubnets); + test.done(); }, };