From 6bb190016aadc3e64515e8d944ea84883325c5e3 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Tue, 19 Mar 2019 15:33:52 -0700 Subject: [PATCH] feat(assets): surface the CFN Parameters that Assets create. This is needed in order to override them when deploying the Stack through CodePipeline. --- packages/@aws-cdk/assets/lib/asset.ts | 21 ++++- packages/@aws-cdk/assets/test/test.asset.ts | 3 + .../aws-codepipeline-actions/README.md | 91 ++++++++++++++++++- .../@aws-cdk/aws-codepipeline/lib/artifact.ts | 8 ++ .../@aws-cdk/aws-codepipeline/package.json | 2 + .../aws-codepipeline/test/test.artifact.ts | 32 +++++++ packages/@aws-cdk/aws-lambda/lib/code.ts | 32 ++++--- .../@aws-cdk/aws-lambda/test/test.code.ts | 34 ++++++- 8 files changed, 206 insertions(+), 17 deletions(-) create mode 100644 packages/@aws-cdk/aws-codepipeline/test/test.artifact.ts diff --git a/packages/@aws-cdk/assets/lib/asset.ts b/packages/@aws-cdk/assets/lib/asset.ts index 378c73a2da87d..b3834d2154886 100644 --- a/packages/@aws-cdk/assets/lib/asset.ts +++ b/packages/@aws-cdk/assets/lib/asset.ts @@ -76,6 +76,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 +135,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 +146,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); diff --git a/packages/@aws-cdk/assets/test/test.asset.ts b/packages/@aws-cdk/assets/test/test.asset.ts index 82147d630f893..4ccfde3233be8 100644 --- a/packages/@aws-cdk/assets/test/test.asset.ts +++ b/packages/@aws-cdk/assets/test/test.asset.ts @@ -34,6 +34,9 @@ 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(); }, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index 1eaa460a66531..9dd8b7d89673b 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -324,9 +324,69 @@ This package defines the following actions: changes from the people (or system) applying the changes. * **CloudFormationExecuteChangeSetAction** - Execute a change set prepared previously. +##### 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 lambdaStack = new cdk.Stack(app, 'LambdaStack', { + autoDeploy: false, // to make working with the 2 Stacks easier in the toolkit +}); +const lambdaCode = lambda.Code.asset('path/to/directory/or/zip/file'); +const func = 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 Stack +pipeline.addStage({ + name: 'Deploy', + actions: [ + new codepipeline_actions.CloudFormationCreateUpdateStackAction({ + actionName: 'Lambda_CFN_Deploy', + templatePath: cdkBuildAction.outputArtifact.atPath('template.yaml'), + stackName: 'YourDeployStackHere', + adminPermissions: true, + parameterOverrides: { + ...lambdaBuildAction.outputArtifact.overrideAsset(lambdaCode.asset), + }, + additionalInputArtifacts: [ + lambdaBuildAction.outputArtifact, + ], + }), + ], +}); +``` + #### AWS CodeDeploy -To use CodeDeploy in a Pipeline: +##### Server deployments + +To use CodeDeploy for EC2/on-premise deployments in a Pipeline: ```ts import codedeploy = require('@aws-cdk/aws-codedeploy'); @@ -348,6 +408,35 @@ pipeline.addStage({ }); ``` +##### Lambda deployments + +To use CodeDeploy for blue-green Lambda deployments in a Pipeline: + +```typescript +const lambdaCode = lambda.Code.asset('path/to/directory/or/zip/file'); +const func = new lambda.Function(lambdaStack, 'Lambda', { + code: lambdaCode, + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810, +}); +// used to make sure each CDK synthesis produces a different Version +const version = func.newVersion(); +const alias = new lambda.Alias(lambdaStack, 'LambdaAlias', { + aliasName: 'Prod', + version, +}); + +new codedeploy.LambdaDeploymentGroup(lambdaStack, 'DeploymentGroup', { + alias, + deploymentConfig: codedeploy.LambdaDeploymentConfig.Linear10PercentEvery1Minute, +}); +``` + +Then, you need to create your Pipeline Stack, +where you will define your Pipeline, +and deploy the `lambdaStack` using a CloudFormation CodePipeline Action +(see above for a complete example). + #### AWS S3 To use an S3 Bucket as a deployment target in CodePipeline: diff --git a/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts b/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts index 0e4f03a1e0c6e..cf2a6859e03bf 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts @@ -1,3 +1,4 @@ +import assets = require('@aws-cdk/assets'); import { Token } from "@aws-cdk/cdk"; /** @@ -51,6 +52,13 @@ export class Artifact { public toString() { return this.artifactName; } + + public overrideAsset(asset: assets.Asset): { [name: string]: string } { + const ret: { [name: string]: string } = {}; + ret[asset.bucketNameParam] = this.bucketName; + ret[asset.objectKeyParam] = this.objectKey; + return ret; + } } /** diff --git a/packages/@aws-cdk/aws-codepipeline/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index 2bce47d4eaf2f..a6399803d191d 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -72,6 +72,7 @@ "pkglint": "^0.27.0" }, "dependencies": { + "@aws-cdk/assets": "^0.27.0", "@aws-cdk/aws-events": "^0.27.0", "@aws-cdk/aws-iam": "^0.27.0", "@aws-cdk/aws-s3": "^0.27.0", @@ -80,6 +81,7 @@ }, "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { + "@aws-cdk/assets": "^0.27.0", "@aws-cdk/aws-events": "^0.27.0", "@aws-cdk/aws-iam": "^0.27.0", "@aws-cdk/aws-s3": "^0.27.0", diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.artifact.ts b/packages/@aws-cdk/aws-codepipeline/test/test.artifact.ts new file mode 100644 index 0000000000000..8af220c6b3c42 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline/test/test.artifact.ts @@ -0,0 +1,32 @@ +import assets = require('@aws-cdk/assets'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import codepipeline = require('../lib'); + +export = { + 'CodePipeline Artifacts': { + 'can override Assets'(test: Test) { + // given + const stack = new cdk.Stack(); + const asset = new assets.ZipDirectoryAsset(stack, 'MyAsset', { + path: __dirname + }); + + const artifact = new codepipeline.Artifact('MyArtifact'); + + // when + const overrides = stack.node.resolve(artifact.overrideAsset(asset)); + + // then + 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/lib/code.ts b/packages/@aws-cdk/aws-lambda/lib/code.ts index 4eaf5ef069bc6..8d0b0a298c050 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); } @@ -145,7 +145,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. @@ -165,30 +165,38 @@ 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!`); + } + } + /** * @internal */ 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) {