Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(assets): surface the CFN Parameters that Assets create #2057

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions packages/@aws-cdk/assets/lib/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions packages/@aws-cdk/assets/test/test.asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
},

Expand Down
91 changes: 90 additions & 1 deletion packages/@aws-cdk/aws-codepipeline-actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section makes me feel that maybe we should merge the README files of aws-codepipeline and aws-codepipeline-actions. It feels artificial to have to jump between the two... What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have any philosophical objections to that, I do have one practical concern though: nesting. As you can see, we've reached H5 in the ReadMe for codepipeline-actions, and I assume we'll need to increase the nesting of the headings by at least one level if we move them to the codepipeline ReadMe.


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the comment to describe what this does

});
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: new line before each "//" line

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');
Expand All @@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds weird and scary?... We can't have "cdk synth" produce a different result each time. Is this just temporary until we have #1400?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's meant to be used in a Pipeline, so I believe that's fine.

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:
Expand Down
8 changes: 8 additions & 0 deletions packages/@aws-cdk/aws-codepipeline/lib/artifact.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assets = require('@aws-cdk/assets');
import { Token } from "@aws-cdk/cdk";

/**
Expand Down Expand Up @@ -51,6 +52,13 @@ export class Artifact {
public toString() {
return this.artifactName;
}

public overrideAsset(asset: assets.Asset): { [name: string]: string } {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing documentation. I am not sure about this name... Reads weird: artifact.overrideAsset(asset). What does this mean? Maybe something like artifact.wireAsset(asset) or artifact.useAsset(asset)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The word used is override because you're changing what the asset points to. Before, it was referencing something on the file system - now, it references something in the Pipeline. So I think it makes a lot of sense.

But if you prefer useAsset (wireAssetI don't think is great...), I won't fight you (too strongly ;p) on it.

const ret: { [name: string]: string } = {};
ret[asset.bucketNameParam] = this.bucketName;
ret[asset.objectKeyParam] = this.objectKey;
return ret;
}
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-codepipeline/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
32 changes: 32 additions & 0 deletions packages/@aws-cdk/aws-codepipeline/test/test.artifact.ts
Original file line number Diff line number Diff line change
@@ -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();
},
},
};
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 @@ -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.
Expand All @@ -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
};
}
}
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