Skip to content

Commit

Permalink
feat(assets): surface the CFN Parameters that Assets create.
Browse files Browse the repository at this point in the history
This is needed in order to override them when deploying the Stack through CodePipeline.
  • Loading branch information
skinny85 committed Mar 20, 2019
1 parent abacc66 commit 4db81c5
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 18 deletions.
29 changes: 27 additions & 2 deletions packages/@aws-cdk/assets/lib/asset.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion packages/@aws-cdk/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,20 @@
"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",
"@aws-cdk/cx-api": "^0.26.0"
},
"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"
},
"engines": {
"node": ">= 8.10.0"
}
}
}
26 changes: 26 additions & 0 deletions packages/@aws-cdk/assets/test/test.asset.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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();
},

Expand Down
57 changes: 56 additions & 1 deletion packages/@aws-cdk/aws-lambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down
32 changes: 20 additions & 12 deletions packages/@aws-cdk/aws-lambda/lib/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,23 @@ 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);
}

/**
* @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);
}

/**
* 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);
}

Expand All @@ -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);
}

Expand All @@ -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);
}

Expand Down Expand Up @@ -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.
Expand All @@ -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
};
}
}
34 changes: 32 additions & 2 deletions packages/@aws-cdk/aws-lambda/test/test.code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit 4db81c5

Please sign in to comment.