Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aws-lambda): allow placing Lambda in VPC #598

Merged
merged 15 commits into from
Sep 9, 2018
Merged
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();
},
};
57 changes: 55 additions & 2 deletions 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 = 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 All @@ -341,4 +394,4 @@ class LambdaRefImport extends FunctionRef {
return new cdk.FnSelect(6, new cdk.FnSplit(':', arn));

}
}
}
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe role.addManagedPolicy('service-role/xxx') would be nicer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, but you can have your own managed policies that you can attach, and they'll have a different account ('12345' vs 'aws'). Let's use ARNs to be safe.

Ultimately, this should be just "the object", not "the ARN", but I just wanted to make this copy/pasted code slighly better, not introduce a new modeling of ManagedPolicies as part of this CR.

});

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');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mention what are the legal options (private/internal) and also why is this not possible

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't mention Internal because in this PR support for selecting internal subnets hasn't landed yet.

Also I'm told that my understanding of this is incomplete (by someone from the Lambda team), AND they're changing it early next year, AND we never explicate the reason on any other of our public documentation pages, we just say "no public for you". I don't want us to be the single source of truth for a factoid that's going to become wrong in a short while.

}
}

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