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-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..41d6e762b560e --- /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 + }); + } +} 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..d54b27d3999cd --- /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(); + }, +}; diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts index 559794eb6112d..5bdcedbe0c622 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'); @@ -13,19 +14,30 @@ 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; + + /** + * 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; } 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 +146,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 +189,28 @@ 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) { + // 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; + } + + /** + * 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. @@ -261,6 +302,10 @@ export abstract class FunctionRef extends cdk.Construct public export(): FunctionRefProps { 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 }; } @@ -322,6 +367,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 + }) + }); + } } /** @@ -341,4 +394,4 @@ class LambdaRefImport extends FunctionRef { return new cdk.FnSelect(6, new cdk.FnSplit(':', arn)); } -} \ 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 fa2c81b7412b1..b2c8e917901e3 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 sqs = require('@aws-cdk/aws-sqs'); import cdk = require('@aws-cdk/cdk'); @@ -91,6 +92,34 @@ export interface FunctionProps { */ 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. Note: internet access for Lambdas + * requires a NAT gateway, so picking Public subnets is not allowed. + * + * @default All private subnets + */ + vpcPlacement?: ec2.VpcPlacementStrategy; + + /** + * What security group to associate with the Lambda's network interfaces. + * + * Only used if 'vpc' is supplied. + * + * @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; + /** * Enabled DLQ. If `deadLetterQueue` is undefined, * an SQS queue with default options will be defined for your Function. @@ -105,7 +134,6 @@ export interface FunctionProps { * @default SQS queue with 14 day retention period if `deadLetterQueueEnabled` is `true` */ deadLetterQueue?: sqs.QueueRef; - } /** @@ -157,16 +185,19 @@ export class Function extends FunctionRef { this.environment = props.environment || { }; + 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); + + if (props.vpc) { + // Policy that will have ENI creation permissions + managedPolicyArns.push(new iam.AwsManagedPolicy("service-role/AWSLambdaVPCAccessExecutionRole").policyArn); + } + 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({ - 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, }); for (const statement of (props.initialPolicy || [])) { @@ -183,6 +214,7 @@ export class Function extends FunctionRef { role: this.role.roleArn, environment: new cdk.Token(() => this.renderEnvironment()), memorySize: props.memorySize, + vpcConfig: this.configureVpc(props), deadLetterConfig: this.buildDeadLetterConfig(props), }); @@ -245,6 +277,40 @@ export class Function extends FunctionRef { }; } + /** + * If configured, set up the VPC-related properties + * + * Returns the VpcConfig that should be added to the + * Lambda creation properties. + */ + private configureVpc(props: FunctionProps): cloudformation.FunctionResource.VpcConfigProperty | undefined { + if (!props.vpc) { return undefined; } + + 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 }); + + // 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: subnets.map(s => s.subnetId), + securityGroupIds: [securityGroup.securityGroupId] + }; + } + private buildDeadLetterConfig(props: FunctionProps) { if (props.deadLetterQueue && props.deadLetterQueueEnabled === false) { throw Error('deadLetterQueue defined but deadLetterQueueEnabled explicitly set to false'); @@ -266,5 +332,4 @@ export class Function extends FunctionRef { targetArn: deadLetterQueue.queueArn }; } - } diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index dd989ee5b0882..4b9bef096fef7 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", 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..396988864f19f --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.expected.json @@ -0,0 +1,428 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PublicSubnet1" + } + ] + } + }, + "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" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PublicSubnet1" + } + ] + } + }, + "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, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PublicSubnet2" + } + ] + } + }, + "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" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PublicSubnet2" + } + ] + } + }, + "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, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PrivateSubnet1" + } + ] + } + }, + "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, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PrivateSubnet2" + } + ] + } + }, + "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", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC" + } + ] + } + }, + "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 new file mode 100644 index 0000000000000..e60798028fcba --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts @@ -0,0 +1,181 @@ +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', { vpc: this.vpc }); + const somethingConnectable = new SomethingConnectable(new ec2.Connections({ securityGroup })); + + // WHEN + this.lambda.connections.allowTo(somethingConnectable, new ec2.TcpAllPorts(), 'Lambda can call connectable'); + + // THEN: SomeSecurityGroup accepts connections from Lambda + expect(this.stack).to(haveResource("AWS::EC2::SecurityGroupEgress", { + GroupId: {"Fn::GetAtt": ["LambdaSecurityGroupE74659A1", "GroupId"]}, + IpProtocol: "tcp", + Description: "Lambda can call connectable", + DestinationSecurityGroupId: {"Fn::GetAtt": [ "SomeSecurityGroupEF219AD6", "GroupId" ]}, + FromPort: 0, + ToPort: 65535 + })); + + // THEN: Lambda can connect to SomeSecurityGroup + expect(this.stack).to(haveResource("AWS::EC2::SecurityGroupIngress", { + IpProtocol: "tcp", + Description: "Lambda can call connectable", + FromPort: 0, + GroupId: { "Fn::GetAtt": ["SomeSecurityGroupEF219AD6", "GroupId"] }, + SourceSecurityGroupId: {"Fn::GetAtt": ["LambdaSecurityGroupE74659A1", "GroupId" ]}, + ToPort: 65535 + })); + + test.done(); + } + + 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(); + }, + + '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(); + } +}; + +/** + * 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) { + } +}