From d50344abb643f6f2c200bba15cfce4d469485dd9 Mon Sep 17 00:00:00 2001 From: Nas Taibi <36194441+nataibi@users.noreply.github.com> Date: Tue, 17 Dec 2019 12:19:53 +0000 Subject: [PATCH] feat(lambda): provisioned concurrency (#5308) fixes #5298 --- packages/@aws-cdk/aws-lambda/lib/alias.ts | 29 ++- packages/@aws-cdk/aws-lambda/lib/function.ts | 4 +- .../@aws-cdk/aws-lambda/lib/lambda-version.ts | 29 ++- ...integ.lambda.prov.concurrent.expected.json | 220 ++++++++++++++++++ .../test/integ.lambda.prov.concurrent.ts | 61 +++++ .../@aws-cdk/aws-lambda/test/test.alias.ts | 80 +++++++ 6 files changed, 418 insertions(+), 5 deletions(-) create mode 100644 packages/@aws-cdk/aws-lambda/test/integ.lambda.prov.concurrent.expected.json create mode 100644 packages/@aws-cdk/aws-lambda/test/integ.lambda.prov.concurrent.ts diff --git a/packages/@aws-cdk/aws-lambda/lib/alias.ts b/packages/@aws-cdk/aws-lambda/lib/alias.ts index 8d5fd35f6ae9a..faa7914df3b36 100644 --- a/packages/@aws-cdk/aws-lambda/lib/alias.ts +++ b/packages/@aws-cdk/aws-lambda/lib/alias.ts @@ -59,6 +59,13 @@ export interface AliasProps { * @default No additional versions */ readonly additionalVersions?: VersionWeight[]; + + /** + * Specifies a provisioned concurrency configuration for a function's alias. + * + * @default No provisioned concurrency + */ + readonly provisionedConcurrentExecutions?: number; } export interface AliasAttributes { @@ -127,7 +134,8 @@ export class Alias extends QualifiedFunctionBase implements IAlias { description: props.description, functionName: this.version.lambda.functionName, functionVersion: props.version.version, - routingConfig: this.determineRoutingConfig(props) + routingConfig: this.determineRoutingConfig(props), + provisionedConcurrencyConfig: this.determineProvisionedConcurrency(props) }); this.functionArn = this.getResourceArnAttribute(alias.ref, { @@ -200,6 +208,23 @@ export class Alias extends QualifiedFunctionBase implements IAlias { throw new Error(`Sum of additional version weights must not exceed 1, got: ${total}`); } } + + /** + * Validate that the provisionedConcurrentExecutions makes sense + * + * Member must have value greater than or equal to 1 + */ + private determineProvisionedConcurrency(props: AliasProps): CfnAlias.ProvisionedConcurrencyConfigurationProperty | undefined { + if (!props.provisionedConcurrentExecutions) { + return undefined; + } + + if (props.provisionedConcurrentExecutions <= 0) { + throw new Error('provisionedConcurrentExecutions must have value greater than or equal to 1'); + } + + return {provisionedConcurrentExecutions: props.provisionedConcurrentExecutions}; + } } /** @@ -215,4 +240,4 @@ export interface VersionWeight { * How much weight to assign to this version (0..1) */ readonly weight: number; -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index af04da09f15f4..08b81d0a4a1a2 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -541,13 +541,15 @@ export class Function extends FunctionBase { * @param codeSha256 The SHA-256 hash of the most recently deployed Lambda source code, or * omit to skip validation. * @param description A description for this version. + * @param provisionedExecutions A provisioned concurrency configuration for a function's version. * @returns A new Version object. */ - public addVersion(name: string, codeSha256?: string, description?: string): Version { + public addVersion(name: string, codeSha256?: string, description?: string, provisionedExecutions?: number): Version { return new Version(this, 'Version' + name, { lambda: this, codeSha256, description, + provisionedConcurrentExecutions: provisionedExecutions, }); } diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts index f0aee24953e5f..99ace44feac59 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts @@ -41,6 +41,13 @@ export interface VersionProps { * Function to get the value of */ readonly lambda: IFunction; + + /** + * Specifies a provisioned concurrency configuration for a function's version. + * + * @default No provisioned concurrency + */ + readonly provisionedConcurrentExecutions?: number; } export interface VersionAttributes { @@ -126,7 +133,8 @@ export class Version extends QualifiedFunctionBase implements IVersion { const version = new CfnVersion(this, 'Resource', { codeSha256: props.codeSha256, description: props.description, - functionName: props.lambda.functionName + functionName: props.lambda.functionName, + provisionedConcurrencyConfig: this.determineProvisionedConcurrency(props) }); this.version = version.attrVersion; @@ -155,6 +163,23 @@ export class Version extends QualifiedFunctionBase implements IVersion { ...props }); } + + /** + * Validate that the provisionedConcurrentExecutions makes sense + * + * Member must have value greater than or equal to 1 + */ + private determineProvisionedConcurrency(props: VersionProps): CfnVersion.ProvisionedConcurrencyConfigurationProperty | undefined { + if (!props.provisionedConcurrentExecutions) { + return undefined; + } + + if (props.provisionedConcurrentExecutions <= 0) { + throw new Error('provisionedConcurrentExecutions must have value greater than or equal to 1'); + } + + return {provisionedConcurrentExecutions: props.provisionedConcurrentExecutions}; + } } /** @@ -172,4 +197,4 @@ export class Version extends QualifiedFunctionBase implements IVersion { */ function extractVersionFromArn(arn: string) { return Fn.select(7, Fn.split(':', arn)); -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.lambda.prov.concurrent.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.lambda.prov.concurrent.expected.json new file mode 100644 index 0000000000000..ad788ff4425b6 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.lambda.prov.concurrent.expected.json @@ -0,0 +1,220 @@ +{ + "Resources": { + "MyLambdaAliasPCEServiceRoleF7C9F212": { + "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" + ] + ] + } + ] + } + }, + "MyLambdaAliasPCEServiceRoleDefaultPolicyE7418D56": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "*", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyLambdaAliasPCEServiceRoleDefaultPolicyE7418D56", + "Roles": [ + { + "Ref": "MyLambdaAliasPCEServiceRoleF7C9F212" + } + ] + } + }, + "MyLambdaAliasPCED0B8D751": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(\"Hello from CDK! with Alias Provisioned Concurrent Exec!\");}" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyLambdaAliasPCEServiceRoleF7C9F212", + "Arn" + ] + }, + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "MyLambdaAliasPCEServiceRoleDefaultPolicyE7418D56", + "MyLambdaAliasPCEServiceRoleF7C9F212" + ] + }, + "MyLambdaAliasPCEVersion15F479C08": { + "Type": "AWS::Lambda::Version", + "Properties": { + "FunctionName": { + "Ref": "MyLambdaAliasPCED0B8D751" + } + } + }, + "Alias325C5727": { + "Type": "AWS::Lambda::Alias", + "Properties": { + "FunctionName": { + "Ref": "MyLambdaAliasPCED0B8D751" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "MyLambdaAliasPCEVersion15F479C08", + "Version" + ] + }, + "Name": "prod", + "ProvisionedConcurrencyConfig": { + "ProvisionedConcurrentExecutions": 5 + } + } + }, + "AliasAliasPermissionAF30F9E8": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Alias325C5727" + }, + "Principal": "cloudformation.amazonaws.com" + } + }, + "MyLambdaVersionPCEServiceRole2ACFB73E": { + "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" + ] + ] + } + ] + } + }, + "MyLambdaVersionPCEServiceRoleDefaultPolicy229A1552": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "*", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyLambdaVersionPCEServiceRoleDefaultPolicy229A1552", + "Roles": [ + { + "Ref": "MyLambdaVersionPCEServiceRole2ACFB73E" + } + ] + } + }, + "MyLambdaVersionPCEA3A0D86B": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(\"Hello from CDK! with Version Provisioned Concurrent Exec!\");}" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyLambdaVersionPCEServiceRole2ACFB73E", + "Arn" + ] + }, + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "MyLambdaVersionPCEServiceRoleDefaultPolicy229A1552", + "MyLambdaVersionPCEServiceRole2ACFB73E" + ] + }, + "MyLambdaVersionPCEVersion2C704112A": { + "Type": "AWS::Lambda::Version", + "Properties": { + "FunctionName": { + "Ref": "MyLambdaVersionPCEA3A0D86B" + }, + "ProvisionedConcurrencyConfig": { + "ProvisionedConcurrentExecutions": 5 + } + } + }, + "Alias29455D932": { + "Type": "AWS::Lambda::Alias", + "Properties": { + "FunctionName": { + "Ref": "MyLambdaVersionPCEA3A0D86B" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "MyLambdaVersionPCEVersion2C704112A", + "Version" + ] + }, + "Name": "prod" + } + }, + "Alias2AliasPermission2448514B6": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Alias29455D932" + }, + "Principal": "cloudformation.amazonaws.com" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.lambda.prov.concurrent.ts b/packages/@aws-cdk/aws-lambda/test/integ.lambda.prov.concurrent.ts new file mode 100644 index 0000000000000..a7c74bbb8b551 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.lambda.prov.concurrent.ts @@ -0,0 +1,61 @@ +import iam = require('@aws-cdk/aws-iam'); +import cdk = require('@aws-cdk/core'); +import lambda = require('../lib'); + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-lambda-pce-1'); + +const lambdaCode = 'exports.handler = async function(event, context) { ' + + 'console.log("Hello from CDK! with #type# Provisioned Concurrent Exec!");}'; + +const pce = 5; + +// Integration test for provisioned concurrent execution via Alias +const fn = new lambda.Function(stack, 'MyLambdaAliasPCE', { + code: new lambda.InlineCode(lambdaCode.replace('#type#', 'Alias')), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, +}); + +fn.addToRolePolicy(new iam.PolicyStatement({ + resources: ['*'], + actions: ['*'] +})); + +const version = fn.addVersion('1'); + +const alias = new lambda.Alias(stack, 'Alias', { + aliasName: 'prod', + version, + provisionedConcurrentExecutions: pce +}); + +alias.addPermission('AliasPermission', { + principal: new iam.ServicePrincipal('cloudformation.amazonaws.com') +}); + +// Integration test for provisioned concurrent execution via Version +const fnVersionPCE = new lambda.Function(stack, 'MyLambdaVersionPCE', { + code: new lambda.InlineCode(lambdaCode.replace('#type#', 'Version')), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, +}); + +fnVersionPCE.addToRolePolicy(new iam.PolicyStatement({ + resources: ['*'], + actions: ['*'] +})); + +const version2 = fnVersionPCE.addVersion('2', undefined, undefined, pce); + +const alias2 = new lambda.Alias(stack, 'Alias2', { + aliasName: 'prod', + version: version2 +}); + +alias2.addPermission('AliasPermission2', { + principal: new iam.ServicePrincipal('cloudformation.amazonaws.com') +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda/test/test.alias.ts b/packages/@aws-cdk/aws-lambda/test/test.alias.ts index 52316542a849f..54c4ee1caa399 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.alias.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.alias.ts @@ -122,7 +122,50 @@ export = { test.done(); }, + 'version and aliases with provisioned execution'(test: Test): void { + const stack = new Stack(); + const fn = new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('hello()'), + handler: 'index.hello', + runtime: lambda.Runtime.NODEJS_10_X, + }); + + const pce = 5; + const version = fn.addVersion('1', undefined, "testing", pce); + + new lambda.Alias(stack, 'Alias', { + aliasName: 'prod', + version, + provisionedConcurrentExecutions: pce + }); + + expect(stack).to(beASupersetOfTemplate({ + MyLambdaVersion16CDE3C40: { + Type: "AWS::Lambda::Version", + Properties: { + FunctionName: { + Ref: "MyLambdaCCE802FB" + }, + ProvisionedConcurrencyConfig: { + ProvisionedConcurrentExecutions: 5 + } + } + }, + Alias325C5727: { + Type: "AWS::Lambda::Alias", + Properties: { + FunctionName: { Ref: "MyLambdaCCE802FB" }, + FunctionVersion: stack.resolve(version.version), + Name: "prod", + ProvisionedConcurrencyConfig: { + ProvisionedConcurrentExecutions: 5 + } + } + } + })); + test.done(); + }, 'sanity checks on version weights'(test: Test) { const stack = new Stack(); @@ -198,6 +241,43 @@ export = { test.done(); }, + 'sanity checks provisionedConcurrentExecutions'(test: Test) { + const stack = new Stack(); + const pce = -1; + + const fn = new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('hello()'), + handler: 'index.hello', + runtime: lambda.Runtime.NODEJS_10_X, + }); + + // WHEN: Alias provisionedConcurrencyConfig less than 0 + test.throws(() => { + new lambda.Alias(stack, 'Alias1', { + aliasName: 'prod', + version: fn.addVersion('1'), + provisionedConcurrentExecutions: pce + }); + }); + + // WHEN: Version provisionedConcurrencyConfig less than 0 + test.throws(() => { + new lambda.Version(stack, 'Version 1', { + lambda: fn, + codeSha256: undefined, + description: undefined, + provisionedConcurrentExecutions: pce + }); + }); + + // WHEN: Adding a version provisionedConcurrencyConfig less than 0 + test.throws(() => { + fn.addVersion('1', undefined, undefined, pce); + }); + + test.done(); + }, + 'alias exposes real Lambdas role'(test: Test) { const stack = new Stack();