From 6ba0614241ca19cbd3cded06d35bf873fabdbdca Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 17 Aug 2018 14:29:10 +0200 Subject: [PATCH 01/11] feat(aws-lambda): allow placing Lambda in VPC Fixes #580. --- packages/@aws-cdk/aws-lambda/lib/lambda.ts | 102 +++++++++++++++++++-- packages/@aws-cdk/aws-lambda/package.json | 1 + 2 files changed, 96 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda.ts b/packages/@aws-cdk/aws-lambda/lib/lambda.ts index 620ffe1ceff55..19b3350ce8358 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda.ts @@ -1,3 +1,4 @@ +import ec2 = require('@aws-cdk/aws-ec2'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { Code } from './code'; @@ -89,6 +90,31 @@ export interface FunctionProps { * Both supplied and generated roles can always be changed by calling `addToRolePolicy`. */ role?: iam.Role; + + /** + * VPC network to place Lambda network interfaces + * + * Specify this if the Lambda function needs to access resources in a VPC. + */ + vpc?: ec2.VpcNetworkRef; + + /** + * Where to place the network interfaces within the VPC. + * + * Only used if 'vpc' is supplied. + * + * @default Private subnets + */ + vpcPlacement?: ec2.VpcPlacementStrategy; + + /** + * What security group to associate with the Lambda's network interfaces. + * + * Only used if 'vpc' is supplied. + * + * @default A unique security group is created for this Lambda function. + */ + securityGroup?: ec2.SecurityGroupRef; } /** @@ -102,7 +128,7 @@ export interface FunctionProps { * This construct does not yet reproduce all features from the underlying resource * library. */ -export class Function extends FunctionRef { +export class Function extends FunctionRef implements ec2.IConnectable { /** * Name of this function */ @@ -128,6 +154,13 @@ export class Function extends FunctionRef { */ public readonly handler: string; + /** + * The security group associated with this function + * + * (Only set if associated with a VPC). + */ + public securityGroup?: ec2.SecurityGroupRef; + protected readonly canCreatePermissions = true; /** @@ -135,21 +168,38 @@ export class Function extends FunctionRef { */ private readonly environment?: { [key: string]: any }; + private _connections?: ec2.Connections; + constructor(parent: cdk.Construct, name: string, props: FunctionProps) { super(parent, name); this.environment = props.environment || { }; - this.role = props.role || new iam.Role(this, 'ServiceRole', { - assumedBy: new cdk.ServicePrincipal('lambda.amazonaws.com'), - // the arn is in the form of - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - managedPolicyArns: [ cdk.Arn.fromComponents({ + const managedPolicyArns = new Array(); + + // the arn is in the form of - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + managedPolicyArns.push(cdk.Arn.fromComponents({ + service: "iam", + region: "", // no region for managed policy + account: "aws", // the account for a managed policy is 'aws' + resource: "policy", + resourceName: "service-role/AWSLambdaBasicExecutionRole" + })); + + if (props.vpc) { + // Policy that will have ENI creation permissions + managedPolicyArns.push(cdk.Arn.fromComponents({ service: "iam", region: "", // no region for managed policy account: "aws", // the account for a managed policy is 'aws' resource: "policy", - resourceName: "service-role/AWSLambdaBasicExecutionRole", - })], + resourceName: "service-role/AWSLambdaVPCAccessExecutionRole" + })); + } + + this.role = props.role || new iam.Role(this, 'ServiceRole', { + assumedBy: new cdk.ServicePrincipal('lambda.amazonaws.com'), + managedPolicyArns, }); for (const statement of (props.initialPolicy || [])) { @@ -166,6 +216,7 @@ export class Function extends FunctionRef { role: this.role.roleArn, environment: new cdk.Token(() => this.renderEnvironment()), memorySize: props.memorySize, + vpcConfig: this.addToVpc(props), }); resource.addDependency(this.role); @@ -217,6 +268,18 @@ export class Function extends FunctionRef { }); } + /** + * Access the Connections object + * + * Will fail if not a VPC-enabled Lambda Function + */ + public get connections(): ec2.Connections { + if (!this._connections) { + throw new Error('Only VPC-associated Lambda Functions can have their security groups managed.'); + } + return this.connections; + } + private renderEnvironment() { if (!this.environment || Object.keys(this.environment).length === 0) { return undefined; @@ -226,4 +289,29 @@ export class Function extends FunctionRef { variables: this.environment }; } + + /** + * If configured, set up the VPC-related properties + * + * Returns the VpcConfig that should be added to the + * Lambda creation properties. + */ + private addToVpc(props: FunctionProps): cloudformation.FunctionResource.VpcConfigProperty | undefined { + if (!props.vpc) { return undefined; } + + this.securityGroup = props.securityGroup; + if (!this.securityGroup) { + this.securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { + vpc: props.vpc, + description: 'Automatic security group for Lambda Function ' + this.uniqueId, + }); + } + + this._connections = new ec2.Connections({ securityGroup: this.securityGroup }); + + return { + subnetIds: props.vpc.subnets(props.vpcPlacement).map(s => s.subnetId), + securityGroupIds: [this.securityGroup.securityGroupId] + }; + } } diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index a4cee9cbd8c36..14df723c3b4cf 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -59,6 +59,7 @@ "@aws-cdk/assets": "^0.8.2", "@aws-cdk/aws-cloudwatch": "^0.8.2", "@aws-cdk/aws-codepipeline-api": "^0.8.2", + "@aws-cdk/aws-ec2": "^0.8.2", "@aws-cdk/aws-events": "^0.8.2", "@aws-cdk/aws-iam": "^0.8.2", "@aws-cdk/aws-logs": "^0.8.2", From cfefb7e39a558fab504952ba8f8879819b666e3f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 20 Aug 2018 13:40:07 +0200 Subject: [PATCH 02/11] Reverse dependency edge between aws-sns and aws-ec2 packages This is because otherwise the new dependency of aws-lambda => aws-ec2 would introduce the following cycle: Cycle: @aws-cdk/aws-ec2 => @aws-cdk/aws-sns => @aws-cdk/aws-lambda => @aws-cdk/aws-ec2 --- .../aws-ec2/lib/auto-scaling-group.ts | 21 ++++++++++++++++--- packages/@aws-cdk/aws-ec2/package.json | 1 - packages/@aws-cdk/aws-sns/lib/topic-ref.ts | 9 +++++++- packages/@aws-cdk/aws-sns/package.json | 1 + 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-ec2/lib/auto-scaling-group.ts index 2da8662dff415..4dfea36d32331 100644 --- a/packages/@aws-cdk/aws-ec2/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-ec2/lib/auto-scaling-group.ts @@ -1,6 +1,5 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); import iam = require('@aws-cdk/aws-iam'); -import sns = require('@aws-cdk/aws-sns'); import cdk = require('@aws-cdk/cdk'); import { Connections, IConnectable } from './connections'; import { InstanceType } from './instance-types'; @@ -62,7 +61,7 @@ export interface AutoScalingGroupProps { * SNS topic to send notifications about fleet changes * @default No fleet change notifications will be sent. */ - notificationsTopic?: sns.cloudformation.TopicResource; + notificationsTopic?: IAutoScalingNotificationTarget; /** * Whether the instances can initiate connections to anywhere by default @@ -152,7 +151,7 @@ export class AutoScalingGroup extends cdk.Construct implements IClassicLoadBalan if (props.notificationsTopic) { asgProps.notificationConfigurations = []; asgProps.notificationConfigurations.push({ - topicArn: props.notificationsTopic.ref, + topicArn: props.notificationsTopic.asAutoScalingNotificationTarget(), notificationTypes: [ "autoscaling:EC2_INSTANCE_LAUNCH", "autoscaling:EC2_INSTANCE_LAUNCH_ERROR", @@ -192,3 +191,19 @@ export class AutoScalingGroup extends cdk.Construct implements IClassicLoadBalan this.role.addToPolicy(statement); } } + +/** + * Interface for subscribing an SNS topic to AutoScalingGroup notifications + * + * (This interface exists to reverse the dependency between the aws-sns + * and aws-ec2 packages.) + */ +export interface IAutoScalingNotificationTarget { + /** + * ARN of the topic to send notifications to. + */ + asAutoScalingNotificationTarget(): cdk.Arn; + + // NOTE: this cannot just be "topicArn: Arn" because of lack of return type + // covariance in jsii. +} diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index f1d3ec2c0d6ce..2cae2a7769a7e 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -56,7 +56,6 @@ "@aws-cdk/aws-autoscaling": "^0.8.2", "@aws-cdk/aws-elasticloadbalancing": "^0.8.2", "@aws-cdk/aws-iam": "^0.8.2", - "@aws-cdk/aws-sns": "^0.8.2", "@aws-cdk/cdk": "^0.8.2", "@aws-cdk/util": "^0.8.2" }, diff --git a/packages/@aws-cdk/aws-sns/lib/topic-ref.ts b/packages/@aws-cdk/aws-sns/lib/topic-ref.ts index ec16ea348d5a3..522e1ca19909c 100644 --- a/packages/@aws-cdk/aws-sns/lib/topic-ref.ts +++ b/packages/@aws-cdk/aws-sns/lib/topic-ref.ts @@ -1,4 +1,5 @@ import cloudwatch = require('@aws-cdk/aws-cloudwatch'); +import ec2 = require('@aws-cdk/aws-ec2'); import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import lambda = require('@aws-cdk/aws-lambda'); @@ -17,7 +18,9 @@ export class TopicArn extends cdk.Arn { } /** * Either a new or imported Topic */ -export abstract class TopicRef extends cdk.Construct implements events.IEventRuleTarget, cloudwatch.IAlarmAction, s3n.IBucketNotificationDestination { +export abstract class TopicRef extends cdk.Construct implements events.IEventRuleTarget, cloudwatch.IAlarmAction, s3n.IBucketNotificationDestination, + ec2.IAutoScalingNotificationTarget { + /** * Import a Topic defined elsewhere */ @@ -306,6 +309,10 @@ export abstract class TopicRef extends cdk.Construct implements events.IEventRul dependencies: [ this.policy! ] // make sure the topic policy resource is created before the notification config }; } + + public asAutoScalingNotificationTarget(): cdk.Arn { + return this.topicArn; + } } /** diff --git a/packages/@aws-cdk/aws-sns/package.json b/packages/@aws-cdk/aws-sns/package.json index 42f318b6a292f..2937587f50eed 100644 --- a/packages/@aws-cdk/aws-sns/package.json +++ b/packages/@aws-cdk/aws-sns/package.json @@ -55,6 +55,7 @@ }, "dependencies": { "@aws-cdk/aws-cloudwatch": "^0.8.2", + "@aws-cdk/aws-ec2": "^0.8.2", "@aws-cdk/aws-events": "^0.8.2", "@aws-cdk/aws-iam": "^0.8.2", "@aws-cdk/aws-lambda": "^0.8.2", From d4ef24cd64e8a0f9acc04cf4291217e87e7e8486 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 20 Aug 2018 15:45:17 +0200 Subject: [PATCH 03/11] WIP --- packages/@aws-cdk/aws-ec2/lib/connections.ts | 10 +- .../@aws-cdk/aws-lambda/lib/lambda-ref.ts | 39 +++++- packages/@aws-cdk/aws-lambda/lib/lambda.ts | 33 +---- .../aws-lambda/test/test.vpc-lambda.ts | 116 ++++++++++++++++++ 4 files changed, 169 insertions(+), 29 deletions(-) create mode 100644 packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts diff --git a/packages/@aws-cdk/aws-ec2/lib/connections.ts b/packages/@aws-cdk/aws-ec2/lib/connections.ts index 6bcdd5fe2975e..38a518c5f73eb 100644 --- a/packages/@aws-cdk/aws-ec2/lib/connections.ts +++ b/packages/@aws-cdk/aws-ec2/lib/connections.ts @@ -61,8 +61,16 @@ export interface ConnectionsProps { * */ export class Connections { + /** + * Underlying securityGroup for this Connections object, if present + * + * May be empty if this Connections object is not managing a SecurityGroup, + * but simply representing a Connectable peer. + */ + public readonly securityGroup?: SecurityGroupRef; + private readonly securityGroupRule: ISecurityGroupRule; - private readonly securityGroup?: SecurityGroupRef; + private readonly defaultPortRange?: IPortRange; constructor(props: ConnectionsProps) { diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts index 505562b050785..49c84e30bd03f 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts @@ -1,4 +1,5 @@ import cloudwatch = require('@aws-cdk/aws-cloudwatch'); +import ec2 = require('@aws-cdk/aws-ec2'); import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import logs = require('@aws-cdk/aws-logs'); @@ -22,10 +23,16 @@ export interface FunctionRefProps { * If the role is not specified, any role-related operations will no-op. */ role?: iam.Role; + + /** + * SecurityGroupId of the securityGroup for this Lambda, if configured. + */ + securityGroupId?: ec2.SecurityGroupId; } export abstract class FunctionRef extends cdk.Construct - implements events.IEventRuleTarget, logs.ILogSubscriptionDestination, s3n.IBucketNotificationDestination { + implements events.IEventRuleTarget, logs.ILogSubscriptionDestination, s3n.IBucketNotificationDestination, + ec2.IConnectable { /** * Creates a Lambda function object which represents a function not defined @@ -134,6 +141,13 @@ export abstract class FunctionRef extends cdk.Construct */ protected abstract readonly canCreatePermissions: boolean; + /** + * Actual connections object for this Lambda + * + * May be unset, in which case this Lambda is not configured use in a VPC. + */ + protected _connections?: ec2.Connections; + /** * Indicates if the policy that allows CloudWatch logs to publish to this lambda has been added. */ @@ -170,6 +184,18 @@ export abstract class FunctionRef extends cdk.Construct this.role.addToPolicy(statement); } + /** + * Access the Connections object + * + * Will fail if not a VPC-enabled Lambda Function + */ + public get connections(): ec2.Connections { + if (!this._connections) { + throw new Error('Only VPC-associated Lambda Functions can have their security groups managed.'); + } + return this._connections; + } + /** * Returns a RuleTarget that can be used to trigger this Lambda as a * result from a CloudWatch event. @@ -261,6 +287,9 @@ export abstract class FunctionRef extends cdk.Construct public export(): FunctionRefProps { return { functionArn: new cdk.Output(this, 'FunctionArn', { value: this.functionArn }).makeImportValue(), + securityGroupId: this._connections && this._connections.securityGroup + ? new cdk.Output(this, 'SecurityGroupId', { value: this._connections.securityGroup.securityGroupId }).makeImportValue() + : undefined }; } @@ -322,6 +351,14 @@ class LambdaRefImport extends FunctionRef { this.functionArn = props.functionArn; this.functionName = this.extractNameFromArn(props.functionArn); this.role = props.role; + + if (props.securityGroupId) { + this._connections = new ec2.Connections({ + securityGroup: ec2.SecurityGroupRef.import(this, 'SecurityGroup', { + securityGroupId: props.securityGroupId + }) + }); + } } /** diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda.ts b/packages/@aws-cdk/aws-lambda/lib/lambda.ts index 19b3350ce8358..408c11d1467c7 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda.ts @@ -128,7 +128,7 @@ export interface FunctionProps { * This construct does not yet reproduce all features from the underlying resource * library. */ -export class Function extends FunctionRef implements ec2.IConnectable { +export class Function extends FunctionRef { /** * Name of this function */ @@ -154,13 +154,6 @@ export class Function extends FunctionRef implements ec2.IConnectable { */ public readonly handler: string; - /** - * The security group associated with this function - * - * (Only set if associated with a VPC). - */ - public securityGroup?: ec2.SecurityGroupRef; - protected readonly canCreatePermissions = true; /** @@ -168,8 +161,6 @@ export class Function extends FunctionRef implements ec2.IConnectable { */ private readonly environment?: { [key: string]: any }; - private _connections?: ec2.Connections; - constructor(parent: cdk.Construct, name: string, props: FunctionProps) { super(parent, name); @@ -268,18 +259,6 @@ export class Function extends FunctionRef implements ec2.IConnectable { }); } - /** - * Access the Connections object - * - * Will fail if not a VPC-enabled Lambda Function - */ - public get connections(): ec2.Connections { - if (!this._connections) { - throw new Error('Only VPC-associated Lambda Functions can have their security groups managed.'); - } - return this.connections; - } - private renderEnvironment() { if (!this.environment || Object.keys(this.environment).length === 0) { return undefined; @@ -299,19 +278,19 @@ export class Function extends FunctionRef implements ec2.IConnectable { private addToVpc(props: FunctionProps): cloudformation.FunctionResource.VpcConfigProperty | undefined { if (!props.vpc) { return undefined; } - this.securityGroup = props.securityGroup; - if (!this.securityGroup) { - this.securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { + let securityGroup = props.securityGroup; + if (!securityGroup) { + securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: props.vpc, description: 'Automatic security group for Lambda Function ' + this.uniqueId, }); } - this._connections = new ec2.Connections({ securityGroup: this.securityGroup }); + this._connections = new ec2.Connections({ securityGroup }); return { subnetIds: props.vpc.subnets(props.vpcPlacement).map(s => s.subnetId), - securityGroupIds: [this.securityGroup.securityGroupId] + securityGroupIds: [securityGroup.securityGroupId] }; } } diff --git a/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts new file mode 100644 index 0000000000000..e0e60efa375b0 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts @@ -0,0 +1,116 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { ICallbackFunction, Test } from 'nodeunit'; +import lambda = require('../lib'); + +export = { + 'lambda in a VPC': classFixture(class Henk { + private readonly stack: cdk.Stack; + private readonly vpc: ec2.VpcNetwork; + private readonly lambda: lambda.Function; + + constructor() { + // GIVEN + this.stack = new cdk.Stack(); + this.vpc = new ec2.VpcNetwork(this.stack, 'VPC'); + + // WHEN + this.lambda = new lambda.Function(this.stack, 'Lambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS610, + vpc: this.vpc + }); + } + + public 'has subnet and securitygroup'(test: Test) { + // THEN + expect(this.stack).to(haveResource('AWS::Lambda::Function', { + VpcConfig: { + SecurityGroupIds: [ + {"Fn::GetAtt": [ "LambdaSecurityGroupE74659A1", "GroupId" ]} + ], + SubnetIds: [ + {Ref: "VPCPrivateSubnet1Subnet8BCA10E0"}, + {Ref: "VPCPrivateSubnet2SubnetCFCDAA7A"}, + {Ref: "VPCPrivateSubnet3Subnet3EDCD457"} + ] + } + })); + + test.done(); + } + + public 'participates in Connections objects'(test: Test) { + // GIVEN + const securityGroup = new ec2.SecurityGroup(this.stack, 'SomeSecurityGroup'); + const somethingConnectable = new SomethingConnectable(new ec2.Connections({ securityGroup })); + + // WHEN + this.lambda.connections.allowTo(somethingConnectable, new ec2.TcpAllPorts(), 'Lambda can call ASG'); + + // THEN: rule to generated security group to connect to imported + expect(this.stack).to(haveResource("AWS::EC2::SecurityGroupEgress", { + GroupId: { "Fn::GetAtt": [ "ASGInstanceSecurityGroup0525485D", "GroupId" ] }, + IpProtocol: "tcp", + Description: "Connect there", + DestinationSecurityGroupId: "sg-12345", + FromPort: 0, + ToPort: 65535 + })); + + // THEN: rule to imported security group to allow connections from generated + expect(this.stack).to(haveResource("AWS::EC2::SecurityGroupIngress", { + IpProtocol: "tcp", + Description: "Connect there", + FromPort: 0, + GroupId: "sg-12345", + SourceSecurityGroupId: { "Fn::GetAtt": [ "ASGInstanceSecurityGroup0525485D", "GroupId" ] }, + ToPort: 65535 + })); + + test.done(); + } + + public 'can still make Connections after export/import'(test: Test) { + test.done(); + } + }), +}; + +/** + * Use a class as test fixture + * + * setUp() will be mapped to the (synchronous) constructor. tearDown(cb) will be called if available. + */ +function classFixture(klass: any) { + let fixture: any; + + const ret: any = { + setUp(cb: ICallbackFunction) { + fixture = new klass(); + cb(); + }, + + tearDown(cb: ICallbackFunction) { + if (fixture.tearDown) { + fixture.tearDown(cb); + } else { + cb(); + } + } + }; + + const testNames = Reflect.ownKeys(klass.prototype).filter(m => m !== 'tearDown' && m !== 'constructor'); + for (const testName of testNames) { + ret[testName] = (test: Test) => fixture[testName](test); + } + + return ret; +} + +class SomethingConnectable implements ec2.IConnectable { + constructor(public readonly connections: ec2.Connections) { + } +} From 5f9e0f648a21c17969f739c22a832d968f26dda6 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 20 Aug 2018 17:00:33 +0200 Subject: [PATCH 04/11] Add tests --- .../test/integ.vpc-lambda.expected.json | 355 ++++++++++++++++++ .../aws-lambda/test/integ.vpc-lambda.ts | 19 + .../aws-lambda/test/test.vpc-lambda.ts | 66 +++- 3 files changed, 430 insertions(+), 10 deletions(-) create mode 100644 packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.expected.json create mode 100644 packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.ts diff --git a/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.expected.json new file mode 100644 index 0000000000000..f0204bf43247a --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.expected.json @@ -0,0 +1,355 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "VPCPublicSubnet1RouteTableAssociatioin249B4093": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "VPCPublicSubnet2RouteTableAssociatioin766225D7": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2EIP4947BC00": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2DefaultRouteB7481BBA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "VPCPrivateSubnet1RouteTableAssociatioin77F7CA18": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "VPCPrivateSubnet2RouteTableAssociatioinC31995B4": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway" + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "MyLambdaServiceRole4539ECB6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AWSLambdaBasicExecutionRole" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AWSLambdaVPCAccessExecutionRole" + ] + ] + } + ] + } + }, + "MyLambdaSecurityGroup1E71A818": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatic security group for Lambda Function awscdkvpclambdaMyLambda057789B0", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Talk to everyone", + "FromPort": 0, + "IpProtocol": "tcp", + "ToPort": 65535 + } + ], + "SecurityGroupIngress": [], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "MyLambdaCCE802FB": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "def main(event, context): pass" + }, + "Handler": "index.main", + "Role": { + "Fn::GetAtt": [ + "MyLambdaServiceRole4539ECB6", + "Arn" + ] + }, + "Runtime": "python3.6", + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "MyLambdaSecurityGroup1E71A818", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + ] + } + }, + "DependsOn": [ + "MyLambdaServiceRole4539ECB6" + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.ts b/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.ts new file mode 100644 index 0000000000000..68e1e3e64a293 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.ts @@ -0,0 +1,19 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import lambda = require('../lib'); + +const app = new cdk.App(process.argv); + +const stack = new cdk.Stack(app, 'aws-cdk-vpc-lambda'); +const vpc = new ec2.VpcNetwork(stack, 'VPC', { maxAZs: 2 }); + +const fn = new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('def main(event, context): pass'), + handler: 'index.main', + runtime: lambda.Runtime.Python36, + vpc +}); + +fn.connections.allowToAnyIPv4(new ec2.TcpAllPorts(), 'Talk to everyone'); + +process.stdout.write(app.run()); diff --git a/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts index e0e60efa375b0..e9cdcb3da7474 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts @@ -44,29 +44,29 @@ export = { public 'participates in Connections objects'(test: Test) { // GIVEN - const securityGroup = new ec2.SecurityGroup(this.stack, 'SomeSecurityGroup'); + const securityGroup = new ec2.SecurityGroup(this.stack, 'SomeSecurityGroup', { vpc: this.vpc }); const somethingConnectable = new SomethingConnectable(new ec2.Connections({ securityGroup })); // WHEN - this.lambda.connections.allowTo(somethingConnectable, new ec2.TcpAllPorts(), 'Lambda can call ASG'); + this.lambda.connections.allowTo(somethingConnectable, new ec2.TcpAllPorts(), 'Lambda can call connectable'); - // THEN: rule to generated security group to connect to imported + // THEN: SomeSecurityGroup accepts connections from Lambda expect(this.stack).to(haveResource("AWS::EC2::SecurityGroupEgress", { - GroupId: { "Fn::GetAtt": [ "ASGInstanceSecurityGroup0525485D", "GroupId" ] }, + GroupId: {"Fn::GetAtt": ["LambdaSecurityGroupE74659A1", "GroupId"]}, IpProtocol: "tcp", - Description: "Connect there", - DestinationSecurityGroupId: "sg-12345", + Description: "Lambda can call connectable", + DestinationSecurityGroupId: {"Fn::GetAtt": [ "SomeSecurityGroupEF219AD6", "GroupId" ]}, FromPort: 0, ToPort: 65535 })); - // THEN: rule to imported security group to allow connections from generated + // THEN: Lambda can connect to SomeSecurityGroup expect(this.stack).to(haveResource("AWS::EC2::SecurityGroupIngress", { IpProtocol: "tcp", - Description: "Connect there", + Description: "Lambda can call connectable", FromPort: 0, - GroupId: "sg-12345", - SourceSecurityGroupId: { "Fn::GetAtt": [ "ASGInstanceSecurityGroup0525485D", "GroupId" ] }, + GroupId: { "Fn::GetAtt": ["SomeSecurityGroupEF219AD6", "GroupId"] }, + SourceSecurityGroupId: {"Fn::GetAtt": ["LambdaSecurityGroupE74659A1", "GroupId" ]}, ToPort: 65535 })); @@ -74,9 +74,55 @@ export = { } public 'can still make Connections after export/import'(test: Test) { + // GIVEN + const stack2 = new cdk.Stack(); + const securityGroup = new ec2.SecurityGroup(stack2, 'SomeSecurityGroup', { vpc: this.vpc }); + const somethingConnectable = new SomethingConnectable(new ec2.Connections({ securityGroup })); + + // WHEN + const importedLambda = lambda.FunctionRef.import(stack2, 'Lambda', this.lambda.export()); + importedLambda.connections.allowTo(somethingConnectable, new ec2.TcpAllPorts(), 'Lambda can call connectable'); + + // THEN: SomeSecurityGroup accepts connections from Lambda + expect(stack2).to(haveResource("AWS::EC2::SecurityGroupEgress", { + GroupId: { "Fn::ImportValue": "LambdaSecurityGroupId9A2717B3" }, + IpProtocol: "tcp", + Description: "Lambda can call connectable", + DestinationSecurityGroupId: { "Fn::GetAtt": [ "SomeSecurityGroupEF219AD6", "GroupId" ] }, + FromPort: 0, + ToPort: 65535 + })); + + // THEN: Lambda can connect to SomeSecurityGroup + expect(stack2).to(haveResource("AWS::EC2::SecurityGroupIngress", { + IpProtocol: "tcp", + Description: "Lambda can call connectable", + FromPort: 0, + GroupId: { "Fn::GetAtt": [ "SomeSecurityGroupEF219AD6", "GroupId" ] }, + SourceSecurityGroupId: { "Fn::ImportValue": "LambdaSecurityGroupId9A2717B3" }, + ToPort: 65535 + })); + test.done(); } }), + + 'lambda without VPC throws Error upon accessing connections'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const lambdaFn = new lambda.Function(stack, 'Lambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS610, + }); + + // WHEN + test.throws(() => { + lambdaFn.connections.allowToAnyIPv4(new ec2.TcpAllPorts(), 'Reach for the world Lambda!'); + }); + + test.done(); + } }; /** From d078fecfb4bc20be5ae24c2321876687fbe0f14c Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 21 Aug 2018 12:06:39 +0200 Subject: [PATCH 05/11] Revert "Reverse dependency edge between aws-sns and aws-ec2 packages" This reverts commit cfefb7e39a558fab504952ba8f8879819b666e3f. --- .../aws-autoscaling/lib/auto-scaling-group.ts | 16 ---------------- packages/@aws-cdk/aws-sns/lib/topic-ref.ts | 9 +-------- packages/@aws-cdk/aws-sns/package.json | 1 - 3 files changed, 1 insertion(+), 25 deletions(-) diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 6fdd9bee25773..f1596d5572d9b 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -187,19 +187,3 @@ export class AutoScalingGroup extends cdk.Construct implements ec2.IClassicLoadB this.role.addToPolicy(statement); } } - -/** - * Interface for subscribing an SNS topic to AutoScalingGroup notifications - * - * (This interface exists to reverse the dependency between the aws-sns - * and aws-ec2 packages.) - */ -export interface IAutoScalingNotificationTarget { - /** - * ARN of the topic to send notifications to. - */ - asAutoScalingNotificationTarget(): cdk.Arn; - - // NOTE: this cannot just be "topicArn: Arn" because of lack of return type - // covariance in jsii. -} diff --git a/packages/@aws-cdk/aws-sns/lib/topic-ref.ts b/packages/@aws-cdk/aws-sns/lib/topic-ref.ts index 522e1ca19909c..ec16ea348d5a3 100644 --- a/packages/@aws-cdk/aws-sns/lib/topic-ref.ts +++ b/packages/@aws-cdk/aws-sns/lib/topic-ref.ts @@ -1,5 +1,4 @@ import cloudwatch = require('@aws-cdk/aws-cloudwatch'); -import ec2 = require('@aws-cdk/aws-ec2'); import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import lambda = require('@aws-cdk/aws-lambda'); @@ -18,9 +17,7 @@ export class TopicArn extends cdk.Arn { } /** * Either a new or imported Topic */ -export abstract class TopicRef extends cdk.Construct implements events.IEventRuleTarget, cloudwatch.IAlarmAction, s3n.IBucketNotificationDestination, - ec2.IAutoScalingNotificationTarget { - +export abstract class TopicRef extends cdk.Construct implements events.IEventRuleTarget, cloudwatch.IAlarmAction, s3n.IBucketNotificationDestination { /** * Import a Topic defined elsewhere */ @@ -309,10 +306,6 @@ export abstract class TopicRef extends cdk.Construct implements events.IEventRul dependencies: [ this.policy! ] // make sure the topic policy resource is created before the notification config }; } - - public asAutoScalingNotificationTarget(): cdk.Arn { - return this.topicArn; - } } /** diff --git a/packages/@aws-cdk/aws-sns/package.json b/packages/@aws-cdk/aws-sns/package.json index 2937587f50eed..42f318b6a292f 100644 --- a/packages/@aws-cdk/aws-sns/package.json +++ b/packages/@aws-cdk/aws-sns/package.json @@ -55,7 +55,6 @@ }, "dependencies": { "@aws-cdk/aws-cloudwatch": "^0.8.2", - "@aws-cdk/aws-ec2": "^0.8.2", "@aws-cdk/aws-events": "^0.8.2", "@aws-cdk/aws-iam": "^0.8.2", "@aws-cdk/aws-lambda": "^0.8.2", From 66900d30ca42eea6711b9ecb203dfa6cd99595a4 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 23 Aug 2018 10:31:39 +0200 Subject: [PATCH 06/11] Add isBoundToVpc() method, minimal abstraction for managed policy ARNs --- packages/@aws-cdk/aws-iam/lib/index.ts | 1 + .../@aws-cdk/aws-iam/lib/managed-policy.ts | 29 +++++++++++++++++++ .../aws-iam/test/test.managed-policy.ts | 29 +++++++++++++++++++ .../@aws-cdk/aws-lambda/lib/lambda-ref.ts | 11 ++++++- packages/@aws-cdk/aws-lambda/lib/lambda.ts | 16 ++-------- 5 files changed, 71 insertions(+), 15 deletions(-) create mode 100644 packages/@aws-cdk/aws-iam/lib/managed-policy.ts create mode 100644 packages/@aws-cdk/aws-iam/test/test.managed-policy.ts diff --git a/packages/@aws-cdk/aws-iam/lib/index.ts b/packages/@aws-cdk/aws-iam/lib/index.ts index ef9b4d1a5f3a4..1792fa972602c 100644 --- a/packages/@aws-cdk/aws-iam/lib/index.ts +++ b/packages/@aws-cdk/aws-iam/lib/index.ts @@ -1,3 +1,4 @@ +export * from './managed-policy'; export * from './role'; export * from './policy'; export * from './user'; diff --git a/packages/@aws-cdk/aws-iam/lib/managed-policy.ts b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts new file mode 100644 index 0000000000000..05dcbbbb93648 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts @@ -0,0 +1,29 @@ +import cdk = require('@aws-cdk/cdk'); + +/** + * A policy managed by AWS + * + * For this managed policy, you only need to know the name to be able to use it. + * + * Some managed policy names start with "service-role/", some start with + * "job-function/", and some don't start with anything. Do include the + * prefix when constructing this object. + */ +export class AWSManagedPolicy { + constructor(private readonly managedPolicyName: string) { + } + + /** + * The Arn of this managed policy + */ + public get policyArn(): cdk.Arn { + // the arn is in the form of - arn:aws:iam::aws:policy/ + return cdk.Arn.fromComponents({ + service: "iam", + region: "", // no region for managed policy + account: "aws", // the account for a managed policy is 'aws' + resource: "policy", + resourceName: this.managedPolicyName + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts b/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts new file mode 100644 index 0000000000000..fbcc055e311d8 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts @@ -0,0 +1,29 @@ +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import { AWSManagedPolicy } from '../lib'; + +export = { + 'simple managed policy'(test: Test) { + const mp = new AWSManagedPolicy("service-role/SomePolicy"); + + test.deepEqual(cdk.resolve(mp.policyArn), { + "Fn::Join": ['', [ + 'arn', + ':', + { Ref: 'AWS::Partition' }, + ':', + 'iam', + ':', + '', + ':', + 'aws', + ':', + 'policy', + '/', + 'service-role/SomePolicy' + ]] + }); + + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts index 49c84e30bd03f..610250b6e31f6 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts @@ -32,7 +32,7 @@ export interface FunctionRefProps { export abstract class FunctionRef extends cdk.Construct implements events.IEventRuleTarget, logs.ILogSubscriptionDestination, s3n.IBucketNotificationDestination, - ec2.IConnectable { + ec2.IConnectable { /** * Creates a Lambda function object which represents a function not defined @@ -196,6 +196,15 @@ export abstract class FunctionRef extends cdk.Construct return this._connections; } + /** + * Whether or not this Lambda function was bound to a VPC + * + * If this is is `false`, trying to access the `connections` object will fail. + */ + public get isBoundToVpc(): boolean { + return !!this._connections; + } + /** * Returns a RuleTarget that can be used to trigger this Lambda as a * result from a CloudWatch event. diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda.ts b/packages/@aws-cdk/aws-lambda/lib/lambda.ts index 408c11d1467c7..72f6dfd237618 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda.ts @@ -169,23 +169,11 @@ export class Function extends FunctionRef { const managedPolicyArns = new Array(); // the arn is in the form of - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - managedPolicyArns.push(cdk.Arn.fromComponents({ - service: "iam", - region: "", // no region for managed policy - account: "aws", // the account for a managed policy is 'aws' - resource: "policy", - resourceName: "service-role/AWSLambdaBasicExecutionRole" - })); + managedPolicyArns.push(new iam.AWSManagedPolicy("service-role/AWSLambdaBasicExecutionRole").policyArn); if (props.vpc) { // Policy that will have ENI creation permissions - managedPolicyArns.push(cdk.Arn.fromComponents({ - service: "iam", - region: "", // no region for managed policy - account: "aws", // the account for a managed policy is 'aws' - resource: "policy", - resourceName: "service-role/AWSLambdaVPCAccessExecutionRole" - })); + managedPolicyArns.push(new iam.AWSManagedPolicy("service-role/AWSLambdaVPCAccessExecutionRole").policyArn); } this.role = props.role || new iam.Role(this, 'ServiceRole', { From 5cf128fa522810b7ade1635e68d55d1839e1253e Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 23 Aug 2018 13:43:12 +0200 Subject: [PATCH 07/11] Prohibit picking public subnets --- packages/@aws-cdk/aws-lambda/lib/lambda.ts | 14 ++++++++++++-- .../aws-lambda/test/test.vpc-lambda.ts | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda.ts b/packages/@aws-cdk/aws-lambda/lib/lambda.ts index 72f6dfd237618..c99aed7baa5ea 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda.ts @@ -101,7 +101,8 @@ export interface FunctionProps { /** * Where to place the network interfaces within the VPC. * - * Only used if 'vpc' is supplied. + * Only used if 'vpc' is supplied. Note: internet access for Lambdas + * requires a NAT gateway, so picking Public subnets is not allowed. * * @default Private subnets */ @@ -276,8 +277,17 @@ export class Function extends FunctionRef { this._connections = new ec2.Connections({ securityGroup }); + // Pick subnets, make sure they're not Public. Routing through an IGW + // won't work because the ENIs don't get a Public IP. + const subnets = props.vpc.subnets(props.vpcPlacement); + for (const subnet of subnets) { + if (props.vpc.publicSubnets.indexOf(subnet) > -1) { + throw new Error('Not possible to place Lambda Functions in a Public subnet'); + } + } + return { - subnetIds: props.vpc.subnets(props.vpcPlacement).map(s => s.subnetId), + subnetIds: subnets.map(s => s.subnetId), securityGroupIds: [securityGroup.securityGroupId] }; } diff --git a/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts index e9cdcb3da7474..e60798028fcba 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts @@ -121,6 +121,25 @@ export = { lambdaFn.connections.allowToAnyIPv4(new ec2.TcpAllPorts(), 'Reach for the world Lambda!'); }); + test.done(); + }, + + 'picking public subnets is not allowed'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + + // WHEN + test.throws(() => { + new lambda.Function(stack, 'Lambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS610, + vpc, + vpcPlacement: { usePublicSubnets: true } + }); + }); + test.done(); } }; From 37bd186e28ef79bcc93cf42c8cc917d4fb2e4018 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 3 Sep 2018 12:15:48 +0200 Subject: [PATCH 08/11] More explicit documentation --- packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts | 10 ++++++++-- packages/@aws-cdk/aws-lambda/lib/lambda.ts | 10 ++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts index 610250b6e31f6..2248011bbdd97 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts @@ -14,18 +14,23 @@ import { Permission } from './permission'; export interface FunctionRefProps { /** * The ARN of the Lambda function. + * * Format: arn::lambda:::function: */ functionArn: FunctionArn; /** * The IAM execution role associated with this function. + * * If the role is not specified, any role-related operations will no-op. */ role?: iam.Role; /** - * SecurityGroupId of the securityGroup for this Lambda, if configured. + * Id of the securityGroup for this Lambda, if in a VPC. + * + * This needs to be given in order to support allowing connections + * to this Lambda. */ securityGroupId?: ec2.SecurityGroupId; } @@ -191,7 +196,8 @@ export abstract class FunctionRef extends cdk.Construct */ public get connections(): ec2.Connections { if (!this._connections) { - throw new Error('Only VPC-associated Lambda Functions can have their security groups managed.'); + // tslint:disable-next-line:max-line-length + throw new Error('Only VPC-associated Lambda Functions have security groups to manage. Supply the "vpc" parameter when creating the Lambda, or "securityGroupId" when importing it.'); } return this._connections; } diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda.ts b/packages/@aws-cdk/aws-lambda/lib/lambda.ts index c99aed7baa5ea..5651b47b8cb9c 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda.ts @@ -104,7 +104,7 @@ export interface FunctionProps { * Only used if 'vpc' is supplied. Note: internet access for Lambdas * requires a NAT gateway, so picking Public subnets is not allowed. * - * @default Private subnets + * @default All private subnets */ vpcPlacement?: ec2.VpcPlacementStrategy; @@ -113,7 +113,9 @@ export interface FunctionProps { * * Only used if 'vpc' is supplied. * - * @default A unique security group is created for this Lambda function. + * @default If the function is placed within a VPC and a security group is + * not specified, a dedicated security group will be created for this + * function. */ securityGroup?: ec2.SecurityGroupRef; } @@ -196,7 +198,7 @@ export class Function extends FunctionRef { role: this.role.roleArn, environment: new cdk.Token(() => this.renderEnvironment()), memorySize: props.memorySize, - vpcConfig: this.addToVpc(props), + vpcConfig: this.configureVpc(props), }); resource.addDependency(this.role); @@ -264,7 +266,7 @@ export class Function extends FunctionRef { * Returns the VpcConfig that should be added to the * Lambda creation properties. */ - private addToVpc(props: FunctionProps): cloudformation.FunctionResource.VpcConfigProperty | undefined { + private configureVpc(props: FunctionProps): cloudformation.FunctionResource.VpcConfigProperty | undefined { if (!props.vpc) { return undefined; } let securityGroup = props.securityGroup; From 305da53ddca81b43e3528dac251493a053a44f5c Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 7 Sep 2018 16:53:00 +0200 Subject: [PATCH 09/11] Rename AWSManagedPolicy --- packages/@aws-cdk/aws-iam/lib/managed-policy.ts | 4 ++-- packages/@aws-cdk/aws-iam/test/test.managed-policy.ts | 6 +++--- packages/@aws-cdk/aws-lambda/lib/lambda.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-iam/lib/managed-policy.ts b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts index 05dcbbbb93648..41d6e762b560e 100644 --- a/packages/@aws-cdk/aws-iam/lib/managed-policy.ts +++ b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts @@ -9,7 +9,7 @@ import cdk = require('@aws-cdk/cdk'); * "job-function/", and some don't start with anything. Do include the * prefix when constructing this object. */ -export class AWSManagedPolicy { +export class AwsManagedPolicy { constructor(private readonly managedPolicyName: string) { } @@ -26,4 +26,4 @@ export class AWSManagedPolicy { resourceName: this.managedPolicyName }); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts b/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts index fbcc055e311d8..d54b27d3999cd 100644 --- a/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts +++ b/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts @@ -1,10 +1,10 @@ import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; -import { AWSManagedPolicy } from '../lib'; +import { AwsManagedPolicy } from '../lib'; export = { 'simple managed policy'(test: Test) { - const mp = new AWSManagedPolicy("service-role/SomePolicy"); + const mp = new AwsManagedPolicy("service-role/SomePolicy"); test.deepEqual(cdk.resolve(mp.policyArn), { "Fn::Join": ['', [ @@ -26,4 +26,4 @@ export = { test.done(); }, -}; \ No newline at end of file +}; diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda.ts b/packages/@aws-cdk/aws-lambda/lib/lambda.ts index 1b41b3406de2b..b2c8e917901e3 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda.ts @@ -188,11 +188,11 @@ export class Function extends FunctionRef { const managedPolicyArns = new Array(); // the arn is in the form of - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - managedPolicyArns.push(new iam.AWSManagedPolicy("service-role/AWSLambdaBasicExecutionRole").policyArn); + managedPolicyArns.push(new iam.AwsManagedPolicy("service-role/AWSLambdaBasicExecutionRole").policyArn); if (props.vpc) { // Policy that will have ENI creation permissions - managedPolicyArns.push(new iam.AWSManagedPolicy("service-role/AWSLambdaVPCAccessExecutionRole").policyArn); + managedPolicyArns.push(new iam.AwsManagedPolicy("service-role/AWSLambdaVPCAccessExecutionRole").policyArn); } this.role = props.role || new iam.Role(this, 'ServiceRole', { From 30f78dc0906b1154277a1c237f7d791f7391eb6c Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 7 Sep 2018 17:03:46 +0200 Subject: [PATCH 10/11] Line length --- packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts index e9c082d22ae43..5bdcedbe0c622 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts @@ -303,8 +303,9 @@ export abstract class FunctionRef extends cdk.Construct return { functionArn: new FunctionArn(new cdk.Output(this, 'FunctionArn', { value: this.functionArn }).makeImportValue()), securityGroupId: this._connections && this._connections.securityGroup - ? new ec2.SecurityGroupId(new cdk.Output(this, 'SecurityGroupId', { value: this._connections.securityGroup.securityGroupId }).makeImportValue()) - : undefined + ? new ec2.SecurityGroupId(new cdk.Output(this, 'SecurityGroupId', { + value: this._connections.securityGroup.securityGroupId + }).makeImportValue()) : undefined }; } From 4eb363ab4589ce62220a0764ffecb5242b52120c Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Sun, 9 Sep 2018 14:36:58 +0200 Subject: [PATCH 11/11] UPdate integ expectation --- .../test/integ.vpc-lambda.expected.json | 97 ++++++++++++++++--- 1 file changed, 85 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.expected.json index f0204bf43247a..396988864f19f 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.expected.json @@ -7,7 +7,12 @@ "EnableDnsHostnames": true, "EnableDnsSupport": true, "InstanceTenancy": "default", - "Tags": [] + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC" + } + ] } }, "VPCPublicSubnet1SubnetB4246D30": { @@ -18,7 +23,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1a", - "MapPublicIpOnLaunch": true + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PublicSubnet1" + } + ] } }, "VPCPublicSubnet1RouteTableFEE4B781": { @@ -26,7 +37,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PublicSubnet1" + } + ] } }, "VPCPublicSubnet1RouteTableAssociatioin249B4093": { @@ -57,7 +74,13 @@ }, "SubnetId": { "Ref": "VPCPublicSubnet1SubnetB4246D30" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PublicSubnet1" + } + ] } }, "VPCPublicSubnet1DefaultRoute91CEF279": { @@ -80,7 +103,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1b", - "MapPublicIpOnLaunch": true + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PublicSubnet2" + } + ] } }, "VPCPublicSubnet2RouteTable6F1A15F1": { @@ -88,7 +117,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PublicSubnet2" + } + ] } }, "VPCPublicSubnet2RouteTableAssociatioin766225D7": { @@ -119,7 +154,13 @@ }, "SubnetId": { "Ref": "VPCPublicSubnet2Subnet74179F39" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PublicSubnet2" + } + ] } }, "VPCPublicSubnet2DefaultRouteB7481BBA": { @@ -142,7 +183,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1a", - "MapPublicIpOnLaunch": false + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PrivateSubnet1" + } + ] } }, "VPCPrivateSubnet1RouteTableBE8A6027": { @@ -150,7 +197,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PrivateSubnet1" + } + ] } }, "VPCPrivateSubnet1RouteTableAssociatioin77F7CA18": { @@ -184,7 +237,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1b", - "MapPublicIpOnLaunch": false + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PrivateSubnet2" + } + ] } }, "VPCPrivateSubnet2RouteTable0A19E10E": { @@ -192,7 +251,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PrivateSubnet2" + } + ] } }, "VPCPrivateSubnet2RouteTableAssociatioinC31995B4": { @@ -219,7 +284,15 @@ } }, "VPCIGWB7E252D3": { - "Type": "AWS::EC2::InternetGateway" + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC" + } + ] + } }, "VPCVPCGW99B986DC": { "Type": "AWS::EC2::VPCGatewayAttachment",