Skip to content

Commit

Permalink
feat(aws-lambda): allow placing Lambda in VPC (#598)
Browse files Browse the repository at this point in the history
feat(aws-lambda): allow placing Lambda in VPC (#598)

It is now possible to deploy Lambda ENIs in a VPC, so
they can access private services.

Fixes #580.
  • Loading branch information
rix0rrr authored Sep 9, 2018
1 parent 27cbe2b commit 9f815f9
Show file tree
Hide file tree
Showing 10 changed files with 826 additions and 12 deletions.
10 changes: 9 additions & 1 deletion packages/@aws-cdk/aws-ec2/lib/connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-iam/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './managed-policy';
export * from './role';
export * from './policy';
export * from './user';
Expand Down
29 changes: 29 additions & 0 deletions packages/@aws-cdk/aws-iam/lib/managed-policy.ts
Original file line number Diff line number Diff line change
@@ -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/<policyName>
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
});
}
}
29 changes: 29 additions & 0 deletions packages/@aws-cdk/aws-iam/test/test.managed-policy.ts
Original file line number Diff line number Diff line change
@@ -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();
},
};
55 changes: 54 additions & 1 deletion packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -13,19 +14,30 @@ import { Permission } from './permission';
export interface FunctionRefProps {
/**
* The ARN of the Lambda function.
*
* Format: arn:<partition>:lambda:<region>:<account-id>:function:<function-name>
*/
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
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
};
}

Expand Down Expand Up @@ -322,6 +367,14 @@ class LambdaRefImport extends FunctionRef {
this.functionArn = props.functionArn;
this.functionName = new 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
})
});
}
}

/**
Expand Down
85 changes: 75 additions & 10 deletions packages/@aws-cdk/aws-lambda/lib/lambda.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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.
Expand All @@ -105,7 +134,6 @@ export interface FunctionProps {
* @default SQS queue with 14 day retention period if `deadLetterQueueEnabled` is `true`
*/
deadLetterQueue?: sqs.QueueRef;

}

/**
Expand Down Expand Up @@ -157,16 +185,19 @@ export class Function extends FunctionRef {

this.environment = props.environment || { };

const managedPolicyArns = new Array<cdk.Arn>();

// 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 || [])) {
Expand All @@ -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),
});

Expand Down Expand Up @@ -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');
Expand All @@ -266,5 +332,4 @@ export class Function extends FunctionRef {
targetArn: deadLetterQueue.queueArn
};
}

}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-lambda/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit 9f815f9

Please sign in to comment.