diff --git a/packages/@aws-cdk/assets/lib/asset.ts b/packages/@aws-cdk/assets/lib/asset.ts index e7d230aa60be4..06b63935828ff 100644 --- a/packages/@aws-cdk/assets/lib/asset.ts +++ b/packages/@aws-cdk/assets/lib/asset.ts @@ -1,3 +1,4 @@ +import cpapi = require('@aws-cdk/aws-codepipeline-api'); import iam = require('@aws-cdk/aws-iam'); import s3 = require('@aws-cdk/aws-s3'); import cdk = require('@aws-cdk/cdk'); @@ -76,6 +77,20 @@ export class Asset extends cdk.Construct { */ public readonly isZipArchive: boolean; + /** + * The name of the CloudFormation Parameter that represents the name of the S3 Bucket + * this asset will actually be stored in when deploying the Stack containing this asset. + * Can be used to override this location in CodePipeline. + */ + public readonly bucketNameParam: string; + + /** + * The name of the CloudFormation Parameter that represents the path inside the S3 Bucket + * this asset will actually be stored at when deploying the Stack containing this asset. + * Can be used to override this location in CodePipeline. + */ + public readonly objectKeyParam: string; + /** * The S3 prefix where all different versions of this asset are stored */ @@ -121,6 +136,9 @@ export class Asset extends cdk.Construct { // form the s3 URL of the object key this.s3Url = this.bucket.urlForObject(this.s3ObjectKey); + this.bucketNameParam = bucketParam.logicalId; + this.objectKeyParam = keyParam.logicalId; + // attach metadata to the lambda function which includes information // for tooling to be able to package and upload a directory to the // s3 bucket and plug in the bucket name and key in the correct @@ -129,8 +147,8 @@ export class Asset extends cdk.Construct { path: this.assetPath, id: this.node.uniqueId, packaging: props.packaging, - s3BucketParameter: bucketParam.logicalId, - s3KeyParameter: keyParam.logicalId, + s3BucketParameter: this.bucketNameParam, + s3KeyParameter: this.objectKeyParam, }; this.node.addMetadata(cxapi.ASSET_METADATA, asset); @@ -178,6 +196,13 @@ export class Asset extends cdk.Construct { // when deploying a new version. this.bucket.grantRead(principal, `${this.s3Prefix}*`); } + + public overrideWith(pipelineArtifact: cpapi.Artifact): { [name: string]: string } { + const ret: { [name: string]: string } = {}; + ret[this.bucketNameParam] = pipelineArtifact.bucketName; + ret[this.objectKeyParam] = pipelineArtifact.objectKey; + return ret; + } } export interface FileAssetProps { diff --git a/packages/@aws-cdk/assets/package.json b/packages/@aws-cdk/assets/package.json index 0361f863fbae4..6a058a2036faa 100644 --- a/packages/@aws-cdk/assets/package.json +++ b/packages/@aws-cdk/assets/package.json @@ -57,6 +57,7 @@ "pkglint": "^0.26.0" }, "dependencies": { + "@aws-cdk/aws-codepipeline-api": "^0.26.0", "@aws-cdk/aws-iam": "^0.26.0", "@aws-cdk/aws-s3": "^0.26.0", "@aws-cdk/cdk": "^0.26.0", @@ -64,6 +65,7 @@ }, "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-codepipeline-api": "^0.26.0", "@aws-cdk/aws-iam": "^0.26.0", "@aws-cdk/aws-s3": "^0.26.0", "@aws-cdk/cdk": "^0.26.0" @@ -71,4 +73,4 @@ "engines": { "node": ">= 8.10.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/assets/test/test.asset.ts b/packages/@aws-cdk/assets/test/test.asset.ts index 1c28ec9ef4d04..affc7c0324f8b 100644 --- a/packages/@aws-cdk/assets/test/test.asset.ts +++ b/packages/@aws-cdk/assets/test/test.asset.ts @@ -1,4 +1,5 @@ import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import cpapi = require('@aws-cdk/aws-codepipeline-api'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import cxapi = require('@aws-cdk/cx-api'); @@ -34,6 +35,31 @@ export = { test.equal(template.Parameters.MyAssetS3Bucket68C9B344.Type, 'String'); test.equal(template.Parameters.MyAssetS3VersionKey68E1A45D.Type, 'String'); + test.equal(stack.node.resolve(asset.bucketNameParam), 'MyAssetS3Bucket68C9B344'); + test.equal(stack.node.resolve(asset.objectKeyParam), 'MyAssetS3VersionKey68E1A45D'); + + test.done(); + }, + + 'can be overridden using CodePipeline Artifacts'(test: Test) { + const stack = new cdk.Stack(); + const dirPath = path.join(__dirname, 'sample-asset-directory'); + const asset = new ZipDirectoryAsset(stack, 'MyAsset', { + path: dirPath + }); + + const artifact = new cpapi.Artifact('MyArtifact'); + + const overrides = stack.node.resolve(asset.overrideWith(artifact)); + + test.deepEqual(overrides.MyAssetS3Bucket68C9B344, { + 'Fn::GetArtifactAtt': ['MyArtifact', 'BucketName'] + }); + + test.deepEqual(overrides.MyAssetS3VersionKey68E1A45D, { + 'Fn::GetArtifactAtt': ['MyArtifact', 'ObjectKey'] + }); + test.done(); }, diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index 2bf3c4a7bef4e..3a3556f755a3e 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,61 @@ 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 need to override the Parameters that are present in the Asset of the Lambda Code. +Note that your Lambda must be in a different 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.asset('path/to/directory/or/zip/file'); +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 +pipeline.addStage({ + name: 'Deploy', + actions: [ + new cloudformation.PipelineCreateUpdateStackAction({ + actionName: 'Lambda_CFN_Deploy', + templatePath: cdkBuildAction.outputArtifact.atPath('template.yaml'), + stackName: 'YourDeployStackHere', + adminPermissions: true, + parameterOverrides: { + ...lambdaCode.asset.overrideWith(lambdaBuildAction.outputArtifact), + }, + additionalInputArtifacts: [ + lambdaBuildAction.outputArtifact, + ], + }), + ], +}); +``` + ### 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..cc650e41f1a91 100644 --- a/packages/@aws-cdk/aws-lambda/lib/code.ts +++ b/packages/@aws-cdk/aws-lambda/lib/code.ts @@ -11,7 +11,7 @@ export abstract class Code { * @param key The object key * @param objectVersion Optional S3 object version */ - public static bucket(bucket: s3.IBucket, key: string, objectVersion?: string) { + public static bucket(bucket: s3.IBucket, key: string, objectVersion?: string): S3Code { return new S3Code(bucket, key, objectVersion); } @@ -19,7 +19,7 @@ export abstract class Code { * @returns `LambdaInlineCode` with inline code. * @param code The actual handler code (limited to 4KiB) */ - public static inline(code: string) { + public static inline(code: string): InlineCode { return new InlineCode(code); } @@ -27,7 +27,7 @@ export abstract class Code { * Loads the function code from a local disk asset. * @param path Either a directory with the Lambda code bundle or a .zip file */ - public static asset(path: string) { + public static asset(path: string): AssetCode { return new AssetCode(path); } @@ -37,7 +37,7 @@ export abstract class Code { * @param directoryToZip The directory to zip * @deprecated use `lambda.Code.asset(path)` (no need to specify if it's a file or a directory) */ - public static directory(directoryToZip: string) { + public static directory(directoryToZip: string): AssetCode { return new AssetCode(directoryToZip, assets.AssetPackaging.ZipDirectory); } @@ -46,7 +46,7 @@ export abstract class Code { * @param filePath The file path * @deprecated use `lambda.Code.asset(path)` (no need to specify if it's a file or a directory) */ - public static file(filePath: string) { + public static file(filePath: string): AssetCode { return new AssetCode(filePath, assets.AssetPackaging.File); } @@ -137,7 +137,7 @@ export class AssetCode extends Code { */ public readonly packaging: assets.AssetPackaging; - private asset?: assets.Asset; + private _asset?: assets.Asset; /** * @param path The path to the asset file or directory. @@ -157,27 +157,35 @@ export class AssetCode extends Code { public bind(construct: cdk.Construct) { // If the same AssetCode is used multiple times, retain only the first instantiation. - if (!this.asset) { - this.asset = new assets.Asset(construct, 'Code', { + if (!this._asset) { + this._asset = new assets.Asset(construct, 'Code', { path: this.path, packaging: this.packaging }); } - if (!this.asset.isZipArchive) { + if (!this._asset.isZipArchive) { throw new Error(`Asset must be a .zip file or a directory (${this.path})`); } } + public get asset(): assets.Asset { + if (this._asset) { + return this._asset; + } else { + throw new Error(`In AssetCode('${this.path}'): you must provide this code to a Function constructor before accessing its 'asset' property!`); + } + } + public _toJSON(resource?: cdk.CfnResource): CfnFunction.CodeProperty { if (resource) { // https://github.com/awslabs/aws-cdk/issues/1432 - this.asset!.addResourceMetadata(resource, 'Code'); + this.asset.addResourceMetadata(resource, 'Code'); } return { - s3Bucket: this.asset!.s3BucketName, - s3Key: this.asset!.s3ObjectKey + s3Bucket: this.asset.s3BucketName, + s3Key: this.asset.s3ObjectKey }; } } diff --git a/packages/@aws-cdk/aws-lambda/test/test.code.ts b/packages/@aws-cdk/aws-lambda/test/test.code.ts index ea08dd02d83e3..88168c91815e8 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.code.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.code.ts @@ -92,8 +92,38 @@ export = { } }, ResourcePart.CompleteDefinition)); test.done(); - } - } + }, + + "allows access to the underlying Asset once it's been used to create a Function"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const code = lambda.Code.asset(path.join(__dirname, 'my-lambda-handler')); + new lambda.Function(stack, 'Func', { + code, + runtime: lambda.Runtime.Python37, + handler: 'index.main', + }); + + // THEN + test.notEqual(code.asset, undefined); + + test.done(); + }, + + "does not allow accessing the Asset before being used to construct a Function"(test: Test) { + // WHEN + const code = lambda.Code.asset(path.join(__dirname, 'my-lambda-handler')); + + // THEN + test.throws(() => { + test.notEqual(code.asset, undefined); + }, /my-lambda-handler/); + + test.done(); + }, + }, }; function defineFunction(code: lambda.Code, runtime: lambda.Runtime = lambda.Runtime.NodeJS810) {