Skip to content

Commit

Permalink
feat(aws-ec2): add VPC context provider
Browse files Browse the repository at this point in the history
Add a context provider for looking up existing VPCs in an account. This
is useful if the VPC is defined outside of your CDK app, such as in a
different CDK app, by hand or in a CloudFormation template.

Addresses some need of #1095.
  • Loading branch information
rix0rrr committed Nov 13, 2018
1 parent 0cb1adf commit 89d5266
Show file tree
Hide file tree
Showing 20 changed files with 621 additions and 142 deletions.
13 changes: 13 additions & 0 deletions docs/src/context.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,16 @@ The |cdk| currently supports the following context providers.
.. code:: js
const ami: string = new SSMParameterProvider(this).getString('my-awesome-value');
:py:class:`VpcNetworkProvider <@aws-cdk/aws-ec2.VpcNetworkProvider>`
Use this provider to look up and reference existing VPC in your accounts.
For example, the follow code imports a VPC by tag name:

.. code:: js
const provider = new VpcNetworkProvider(this, {
tags: {
Purpose: 'WebServices'
}
});
const vpc = VpcNetworkRef.import(this, 'VPC', provider.vpcProps);
12 changes: 12 additions & 0 deletions packages/@aws-cdk/aws-ec2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,18 @@ The `VpcNetwork` above will have the exact same subnet definitions as listed
above. However, this time the VPC will have only 1 NAT Gateway and all
Application subnets will route to the NAT Gateway.

#### Sharing VPCs across stacks

If you are creating multiple `Stack`s inside the same CDK application,
you can reuse a VPC from one Stack in another by using `export()` and
`import()`:

[sharing VPCs between stacks](test/example.share-vpcs.lit.ts)

If your VPC is created outside your CDK app, you can use `importFromContext()`:

[importing existing VPCs](test/integ.import-default-vpc.lit.ts)

### Allowing Connections

In AWS, all network traffic in and out of **Elastic Network Interfaces** (ENIs)
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 @@ -5,6 +5,7 @@ export * from './security-group';
export * from './security-group-rule';
export * from './vpc';
export * from './vpc-ref';
export * from './vpc-network-provider';

// AWS::EC2 CloudFormation Resources:
export * from './ec2.generated';
76 changes: 76 additions & 0 deletions packages/@aws-cdk/aws-ec2/lib/vpc-network-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import cdk = require('@aws-cdk/cdk');
import cxapi = require('@aws-cdk/cx-api');
import { VpcNetworkRefProps } from './vpc-ref';

/**
* Properties for looking up an existing VPC.
*
* The combination of properties must specify filter down to exactly one
* non-default VPC, otherwise an error is raised.
*/
export interface VpcNetworkProviderProps {
/**
* The ID of the VPC
*
* If given, will import exactly this VPC.
*
* @default Don't filter on vpcId
*/
vpcId?: string;

/**
* The name of the VPC
*
* If given, will import the VPC with this name.
*
* @default Don't filter on vpcName
*/
vpcName?: string;

/**
* Tags on the VPC
*
* The VPC must have all of these tags
*
* @default Don't filter on tags
*/
tags?: {[key: string]: string};

/**
* Whether to match the default VPC
*
* @default Don't care whether we return the default VPC
*/
isDefault?: boolean;
}

/**
* Context provider to discover and import existing VPCs
*/
export class VpcNetworkProvider {
private provider: cdk.ContextProvider;

constructor(context: cdk.Construct, props: VpcNetworkProviderProps) {
this.provider = new cdk.ContextProvider(context, cxapi.VPC_PROVIDER, props as cxapi.VpcContextQuery);
}

/**
* Return the VPC import props matching the filter
*/
public get vpcProps(): VpcNetworkRefProps {
const ret: cxapi.VpcContextResponse = this.provider.getValue(DUMMY_VPC_PROPS);
return ret;
}
}

/**
* There are returned when the provider has not supplied props yet
*
* It's only used for testing and on the first run-through.
*/
const DUMMY_VPC_PROPS: cxapi.VpcContextResponse = {
availabilityZones: ['dummy-1a', 'dummy-1b'],
vpcId: 'vpc-12345',
publicSubnetIds: ['s-12345', 's-67890'],
privateSubnetIds: ['p-12345', 'p-67890'],
};
8 changes: 8 additions & 0 deletions packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Construct, IDependable, Output } from "@aws-cdk/cdk";
import { ExportSubnetGroup, ImportSubnetGroup, subnetName } from './util';
import { VpcNetworkProvider, VpcNetworkProviderProps } from './vpc-network-provider';

/**
* The type of Subnet
Expand Down Expand Up @@ -81,6 +82,13 @@ export abstract class VpcNetworkRef extends Construct implements IDependable {
return new ImportedVpcNetwork(parent, name, props);
}

/**
* Import an existing VPC from context
*/
public static importFromContext(parent: Construct, name: string, props: VpcNetworkProviderProps): VpcNetworkRef {
return VpcNetworkRef.import(parent, name, new VpcNetworkProvider(parent, props).vpcProps);
}

/**
* Identifier for this VPC
*/
Expand Down
22 changes: 22 additions & 0 deletions packages/@aws-cdk/aws-ec2/lib/vpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,24 +407,46 @@ export class VpcNetwork extends VpcNetworkRef implements cdk.ITaggable {
tags: subnetConfig.tags,
};

let subnet: VpcSubnet;
switch (subnetConfig.subnetType) {
case SubnetType.Public:
const publicSubnet = new VpcPublicSubnet(this, name, subnetProps);
this.publicSubnets.push(publicSubnet);
subnet = publicSubnet;
break;
case SubnetType.Private:
const privateSubnet = new VpcPrivateSubnet(this, name, subnetProps);
this.privateSubnets.push(privateSubnet);
subnet = privateSubnet;
break;
case SubnetType.Isolated:
const isolatedSubnet = new VpcPrivateSubnet(this, name, subnetProps);
isolatedSubnet.tags.setTag(SUBNETTYPE_TAG, subnetTypeTagValue(subnetConfig.subnetType));
this.isolatedSubnets.push(isolatedSubnet);
subnet = isolatedSubnet;
break;
default:
throw new Error(`Unrecognized subnet type: ${subnetConfig.subnetType}`);
}

// These values will be used to recover the config upon provider import
subnet.tags.setTag(SUBNETNAME_TAG, subnetConfig.name);
subnet.tags.setTag(SUBNETTYPE_TAG, subnetTypeTagValue(subnetConfig.subnetType));
});
}
}

const SUBNETTYPE_TAG = 'aws-cdk:SubnetType';
const SUBNETNAME_TAG = 'aws-cdk:SubnetName';

function subnetTypeTagValue(type: SubnetType) {
switch (type) {
case SubnetType.Public: return 'Public';
case SubnetType.Private: return 'Private';
case SubnetType.Isolated: return 'Isolated';
}
}

/**
* Specify configuration parameters for a VPC subnet
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-ec2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@
},
"dependencies": {
"@aws-cdk/aws-iam": "^0.16.0",
"@aws-cdk/cx-api": "^0.16.0",
"@aws-cdk/cdk": "^0.16.0"
},
"homepage": "https://github.com/awslabs/aws-cdk",
"peerDependencies": {
"@aws-cdk/cx-api": "^0.16.0",
"@aws-cdk/cdk": "^0.16.0"
}
}
40 changes: 40 additions & 0 deletions packages/@aws-cdk/aws-ec2/test/example.share-vpcs.lit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import cdk = require('@aws-cdk/cdk');
import ec2 = require("../lib");

const app = new cdk.App();

/// !show
class Stack1 extends cdk.Stack {
public readonly vpcProps: ec2.VpcNetworkRefProps;

constructor(parent: cdk.App, id: string, props?: cdk.StackProps) {
super(parent, id, props);

const vpc = new ec2.VpcNetwork(this, 'VPC');

// Export the VPC to a set of properties
this.vpcProps = vpc.export();
}
}

interface Stack2Props extends cdk.StackProps {
vpcProps: ec2.VpcNetworkRefProps;
}

class Stack2 extends cdk.Stack {
constructor(parent: cdk.App, id: string, props: Stack2Props) {
super(parent, id, props);

// Import the VPC from a set of properties
const vpc = ec2.VpcNetworkRef.import(this, 'VPC', props.vpcProps);
}
}

const stack1 = new Stack1(app, 'Stack1');
const stack2 = new Stack2(app, 'Stack2', {
vpcProps: stack1.vpcProps
});
/// !hide

app.run();

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"Resources": {
"SecurityGroupDD263621": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"GroupDescription": "aws-cdk-ec2-import/SecurityGroup",
"SecurityGroupEgress": [
{
"CidrIp": "0.0.0.0/0",
"Description": "Allow all outbound traffic by default",
"IpProtocol": "-1"
}
],
"SecurityGroupIngress": [],
"VpcId": "vpc-60900905"
}
}
},
"Outputs": {
"PublicSubnets": {
"Value": "ids:subnet-e19455ca,subnet-e0c24797,subnet-ccd77395",
"Export": {
"Name": "aws-cdk-ec2-import:PublicSubnets"
}
},
"PrivateSubnets": {
"Value": "ids:",
"Export": {
"Name": "aws-cdk-ec2-import:PrivateSubnets"
}
}
}
}
24 changes: 24 additions & 0 deletions packages/@aws-cdk/aws-ec2/test/integ.import-default-vpc.lit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import cdk = require('@aws-cdk/cdk');
import ec2 = require("../lib");

const app = new cdk.App();
const stack = new cdk.Stack(app, 'aws-cdk-ec2-import');

/// !show
const vpc = ec2.VpcNetworkRef.importFromContext(stack, 'VPC', {
// This imports the default VPC but you can also
// specify a 'vpcName' or 'tags'.
isDefault: true
});
/// !hide

// The only thing in this library that takes a VPC as an argument :)
new ec2.SecurityGroup(stack, 'SecurityGroup', {
vpc
});

// Try subnet selection
new cdk.Output(stack, 'PublicSubnets', { value: 'ids:' + vpc.subnets({ subnetsToUse: ec2.SubnetType.Public }).map(s => s.subnetId).join(',') });
new cdk.Output(stack, 'PrivateSubnets', { value: 'ids:' + vpc.subnets({ subnetsToUse: ec2.SubnetType.Private }).map(s => s.subnetId).join(',') });

app.run();
21 changes: 21 additions & 0 deletions packages/@aws-cdk/cx-api/lib/context/vpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const VPC_PROVIDER = 'vpc-provider';

export interface VpcContextQuery {
region: string;
account: string;
vpcId?: string;
vpcName?: string;
tags?: {[key: string]: string};
isDefault?: boolean;
}

export interface VpcContextResponse {
vpcId: string;
availabilityZones: string[];
publicSubnetIds?: string[];
publicSubnetNames?: string[];
privateSubnetIds?: string[];
privateSubnetNames?: string[];
isolatedSubnetIds?: string[];
isolatedSubnetNames?: string[];
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/cx-api/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './cxapi';
export * from './environment';
export * from './context/vpc';
8 changes: 1 addition & 7 deletions packages/aws-cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,6 @@ async function initCommandLine() {
ec2creds: argv.ec2creds,
});

const availableContextProviders: contextplugins.ProviderMap = {
'availability-zones': new contextplugins.AZContextProviderPlugin(aws),
'ssm': new contextplugins.SSMContextProviderPlugin(aws),
'hosted-zone': new contextplugins.HostedZoneContextProviderPlugin(aws),
};

const defaultConfig = new Settings({ versionReporting: true });
const userConfig = await new Settings().load(PER_USER_DEFAULTS);
const projectConfig = await new Settings().load(DEFAULTS);
Expand Down Expand Up @@ -384,7 +378,7 @@ async function initCommandLine() {
if (!cdkUtil.isEmpty(allMissing)) {
debug(`Some context information is missing. Fetching...`);

await contextplugins.provideContextValues(allMissing, projectConfig, availableContextProviders);
await contextplugins.provideContextValues(allMissing, projectConfig, aws);

// Cache the new context to disk
await projectConfig.save(DEFAULTS);
Expand Down
22 changes: 22 additions & 0 deletions packages/aws-cdk/lib/context-providers/availability-zones.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Mode, SDK } from '../api';
import { debug } from '../logging';
import { ContextProviderPlugin } from './provider';

/**
* Plugin to retrieve the Availability Zones for the current account
*/
export class AZContextProviderPlugin implements ContextProviderPlugin {
constructor(private readonly aws: SDK) {
}

public async getValue(args: {[key: string]: any}) {
const region = args.region;
const account = args.account;
debug(`Reading AZs for ${account}:${region}`);
const ec2 = await this.aws.ec2(account, region, Mode.ForReading);
const response = await ec2.describeAvailabilityZones().promise();
if (!response.AvailabilityZones) { return []; }
const azs = response.AvailabilityZones.filter(zone => zone.State === 'available').map(zone => zone.ZoneName);
return azs;
}
}
Loading

0 comments on commit 89d5266

Please sign in to comment.