From f825d39fa3a1e352755faded5dd1e97fad8c2fd9 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Fri, 15 Mar 2019 14:22:32 -0700 Subject: [PATCH] feat(lambda): introduce a new kind of `Code`, `CodePipelineCode`. --- packages/@aws-cdk/aws-lambda/README.md | 64 +++++++++++++- packages/@aws-cdk/aws-lambda/lib/code.ts | 66 ++++++++++++++- .../@aws-cdk/aws-lambda/test/test.code.ts | 83 ++++++++++++++++++- 3 files changed, 207 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index 2bf3c4a7bef4e..50f227add3ef0 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -72,7 +72,7 @@ fn.addEventSource(new S3EventSource(bucket, { See the documentation for the __@aws-cdk/aws-lambda-event-sources__ module for more details. -### Lambda in CodePipeline +### Lambda invoked in CodePipeline This module also contains an Action that allows you to invoke a Lambda function from CodePipeline: @@ -120,6 +120,68 @@ lambdaAction.outputArtifact('Out2'); // returns the named output Artifact, or th See [the AWS documentation](https://docs.aws.amazon.com/codepipeline/latest/userguide/actions-invoke-lambda-function.html) on how to write a Lambda function invoked from CodePipeline. +### Lambda deployed through CodePipeline + +If you want to deploy your Lambda through CodePipeline, +you can use the special `Code` convenience method, `lambda.Code.codePipeline`. +Note that your Lambda must be in a separate Stack than your Pipeline. +The Lambda itself will be deployed, alongside the entire Stack it belongs to, +using a CloudFormation CodePipeline Action. Example: + +```typescript +const lambdaCode = lambda.Code.codePipeline(); +const lambda = new lambda.Function(lambdaStack, 'Lambda', { + code: lambdaCode, + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810, +}); + +// other resources that your Lambda needs, added to the lambdaStack... + +const pipeline = new codepipeline.Pipeline(pipelineStack, 'Pipeline'); + +// add the source code repository containing this code to your Pipeline, +// and the source code of the Lambda Function, if they're separate +pipeline.addStage({ + name: 'Source', + actions: [ + // ... + ], +}); + +// add a build Action to your Pipeline, +// that calls `cdk synth` on the lambdaStack, +// and saves it to some file, +// and a separate build for your Lambda source code - if needed +pipeline.addStage({ + name: 'Build', + actions: [ + lambdaBuildAction, + cdkBuildAction, + ], +}); + +// finally, deploy your Lambda code +const parameterOverrides: { [name: string]: any } = {}; +parameterOverrides[lambdaCode.bucketNameParam.logicalId] = lambdaBuildAction.outputArtifact.bucketName; +parameterOverrides[lambdaCode.bucketKeyParam.logicalId] = lambdaBuildAction.outputArtifact.objectKey; +pipeline.addStage({ + name: 'Deploy', + actions: [ + new cloudformation.PipelineCreateUpdateStackAction({ + actionName: 'Lambda_CFN_Deploy', + templatePath: cdkBuildAction.outputArtifact.atPath('template.yaml'), + stackName: 'YourDeployStackHere', + adminPermissions: true, + parameterOverrides, + additionalInputArtifacts: [ + lambdaBuildAction, + ], + }), + ], +}); +``` + ### Lambda with DLQ ```ts diff --git a/packages/@aws-cdk/aws-lambda/lib/code.ts b/packages/@aws-cdk/aws-lambda/lib/code.ts index 3ebee37f33b62..5b5f954dccc69 100644 --- a/packages/@aws-cdk/aws-lambda/lib/code.ts +++ b/packages/@aws-cdk/aws-lambda/lib/code.ts @@ -50,6 +50,10 @@ export abstract class Code { return new AssetCode(filePath, assets.AssetPackaging.File); } + public static codePipeline(props?: CodePipelineCodeProps): CodePipelineCode { + return new CodePipelineCode(props); + } + /** * Determines whether this Code is inline code or not. */ @@ -150,8 +154,8 @@ export class AssetCode extends Code { this.packaging = packaging; } else { this.packaging = fs.lstatSync(path).isDirectory() - ? assets.AssetPackaging.ZipDirectory - : assets.AssetPackaging.File; + ? assets.AssetPackaging.ZipDirectory + : assets.AssetPackaging.File; } } @@ -175,9 +179,65 @@ export class AssetCode extends Code { this.asset!.addResourceMetadata(resource, 'Code'); } - return { + return { s3Bucket: this.asset!.s3BucketName, s3Key: this.asset!.s3ObjectKey }; } } + +export interface CodePipelineCodeProps { + bucketNameParam?: cdk.CfnParameter; + + bucketKeyParam?: cdk.CfnParameter; +} + +export class CodePipelineCode extends Code { + public readonly isInline = false; + private _bucketNameParam?: cdk.CfnParameter; + private _bucketKeyParam?: cdk.CfnParameter; + + constructor(props: CodePipelineCodeProps = {}) { + super(); + + this._bucketNameParam = props.bucketNameParam; + this._bucketKeyParam = props.bucketKeyParam; + } + + public bind(construct: cdk.Construct) { + if (!this._bucketNameParam) { + this._bucketNameParam = new cdk.CfnParameter(construct, 'LambdaSourceBucketNameParameter', { + type: 'String', + }); + } + + if (!this._bucketKeyParam) { + this._bucketKeyParam = new cdk.CfnParameter(construct, 'LambdaSourceBucketKeyParameter', { + type: 'String', + }); + } + } + + public _toJSON(_?: cdk.CfnResource): CfnFunction.CodeProperty { + return { + s3Bucket: this.bucketNameParam.stringValue, + s3Key: this.bucketKeyParam.stringValue, + }; + } + + public get bucketNameParam(): cdk.CfnParameter { + if (this._bucketNameParam) { + return this._bucketNameParam; + } else { + throw new Error('You have to use CodePipelineCode to create a Lambda Function before you can access the bucketNameParam property'); + } + } + + public get bucketKeyParam(): cdk.CfnParameter { + if (this._bucketKeyParam) { + return this._bucketKeyParam; + } else { + throw new Error('You have to use CodePipelineCode to create a Lambda Function before you can access the bucketKeyParam property'); + } + } +} diff --git a/packages/@aws-cdk/aws-lambda/test/test.code.ts b/packages/@aws-cdk/aws-lambda/test/test.code.ts index ea08dd02d83e3..cd67c696d0823 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.code.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.code.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert'; import assets = require('@aws-cdk/assets'); import cdk = require('@aws-cdk/cdk'); import cxapi = require('@aws-cdk/cx-api'); @@ -93,7 +93,86 @@ export = { }, ResourcePart.CompleteDefinition)); test.done(); } - } + }, + + 'lambda.Code.codepipeline': { + "automatically creates the Bucket and Key parameters when it's used in a Function"(test: Test) { + const stack = new cdk.Stack(); + const code = new lambda.CodePipelineCode(); + new lambda.Function(stack, 'Function', { + code, + runtime: lambda.Runtime.NodeJS810, + handler: 'index.handler', + }); + + expect(stack).to(haveResourceLike('AWS::Lambda::Function', { + Code: { + S3Bucket: { + Ref: "FunctionLambdaSourceBucketNameParameter9E9E108F", + }, + S3Key: { + Ref: "FunctionLambdaSourceBucketKeyParameter5BBE948B", + }, + }, + })); + + test.notEqual(code.bucketNameParam, undefined); + test.notEqual(code.bucketKeyParam, undefined); + + test.done(); + }, + + 'does not allow accessing the Parameter properties before being used in a Function'(test: Test) { + const code = new lambda.CodePipelineCode(); + + test.throws(() => { + test.notEqual(code.bucketNameParam, undefined); + }, /Function/); + + test.throws(() => { + test.notEqual(code.bucketKeyParam, undefined); + }, /Function/); + + test.done(); + }, + + 'allows passing custom Parameters when creating it'(test: Test) { + const stack = new cdk.Stack(); + const bucketNameParam = new cdk.CfnParameter(stack, 'BucketNameParam', { + type: 'String', + }); + const bucketKeyParam = new cdk.CfnParameter(stack, 'BucketKeyParam', { + type: 'String', + }); + + const code = lambda.Code.codePipeline({ + bucketNameParam, + bucketKeyParam, + }); + + new lambda.Function(stack, 'Function', { + code, + runtime: lambda.Runtime.NodeJS810, + handler: 'index.handler', + }); + + expect(stack).to(haveResourceLike('AWS::Lambda::Function', { + Code: { + S3Bucket: { + Ref: "BucketNameParam", + }, + S3Key: { + Ref: "BucketKeyParam", + }, + }, + })); + + test.notEqual(code.bucketNameParam, undefined); + test.notEqual(code.bucketKeyParam, undefined); + + test.done(); + }, + }, }; function defineFunction(code: lambda.Code, runtime: lambda.Runtime = lambda.Runtime.NodeJS810) {