Skip to content

Commit 0d2b633

Browse files
author
Elad Ben-Israel
authored
feat(assets): enable local tooling scenarios such as lambda debugging (#1433)
Adds CloudFormation resource metadata which enables tools such as SAM CLI to find local assets used by resources in the template. The toolkit enables this feature by default, but it can be disabled using `--no-asset-metadata` or by setting `assetMetadata` to `false` in `cdk.json`. See design document under [design/code-asset-metadata.md](./design/code-asset-metadata.md) Fixes #1432
1 parent 36f69b6 commit 0d2b633

File tree

14 files changed

+254
-21
lines changed

14 files changed

+254
-21
lines changed

design/code-asset-metadata.md

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# RFC: AWS Lambda - Metadata about Code Assets
2+
3+
As described in [#1432](https://github.com/awslabs/aws-cdk/issues/1432), in order to support local debugging,
4+
debuggers like [SAM CLI](https://github.com/awslabs/aws-sam-cli) need to be able to find the code of a Lambda
5+
function locally.
6+
7+
The current implementation of assets uses CloudFormation Parameters which represent the S3 bucket and key of the
8+
uploaded asset, which makes it impossible for local tools to reason about (without traversing the cx protocol with
9+
many heuristics).
10+
11+
## Approach
12+
13+
We will automatically embed CloudFormation metadata on `AWS::Lambda::Function` (and any other) resources which use
14+
local assets for code. The metadata will allow tools like SAM CLI to find the code locally for local invocations.
15+
16+
Given a CDK app with an AWS Lambda function defined like so:
17+
18+
```ts
19+
new lambda.Function(this, 'MyHandler', {
20+
// ...
21+
code: lambda.Code.asset('/path/to/handler')
22+
});
23+
```
24+
25+
The synthesized `AWS::Lambda::Function` resource will include a "Metadata" entry as follows:
26+
27+
```js
28+
{
29+
"Type": "AWS::Lambda::Function",
30+
"Properties": {
31+
"Code": {
32+
// current asset magic
33+
}
34+
},
35+
"Metadata": {
36+
"aws:asset:property": "Code",
37+
"aws:asset:path": "/path/to/handler"
38+
}
39+
}
40+
```
41+
42+
Local debugging tools like SAM CLI will be able to traverse the template and look up the `aws:asset` metadata
43+
entries, and use them to process the template so it will be compatible with their inputs.
44+
45+
## Design
46+
47+
We will add a new method to the `Asset` class called `addResourceMetadata(resource, propName)`. This method will
48+
take in an L1 resource (`cdk.Resource`) and a property name (`string`) and will add template metadata as
49+
described above.
50+
51+
This feature will be enabled by a context key `aws:cdk:enable-asset-metadata`, which will be enabled by default in
52+
the CDK Toolkit. The switch `--asset-metadata` (or `--no-asset-metadata`) can be used to control this behavior, as
53+
well as through the key `assetMetadata` in `cdk.json`. Very similar design to how path metadata is controlled.
54+
55+
## Alternatives Considered
56+
57+
We considered alternatives that will "enforce" the embedding of metadata when an asset is referenced by a resource. Since
58+
a single asset can be referenced by multiple resources, it means that the _relationship_ is what should trigger the
59+
metadata addition. There currently isn't support in the framework for such hooks, but there is a possiblility that
60+
the changes in [#1436](https://github.com/awslabs/aws-cdk/pull/1436) might enable hooking into the relationnship, and then we might be able to use this mechanism to produce the metadata.
61+
62+
Having said that, the need to embed asset metadata on resources is mainly confined to authors of L2 constructs, and not applicable for the general user population, so the value of automation is not high.
63+
64+
## What about L1 resources?
65+
66+
If users directly use L1 resources such as `lambda.CfnFunction` or `serverless.CfnFunction` and wish to use local CDK assets with tooling, they will have to explicitly add metadata:
67+
68+
```ts
69+
const asset = new assets.ZipDirectoryAsset(this, 'Foo', {
70+
path: '/foo/boom'
71+
});
72+
73+
const resource = new serverless.CfnFunction(this, 'Func', {
74+
codeUri: {
75+
bucket: asset.s3BucketName,
76+
key: asset.s3ObjectKey
77+
},
78+
runtime: 'nodejs8.10',
79+
handler: 'index.handler'
80+
});
81+
82+
resource.addResourceMetadata(resource, 'CodeUri');
83+
```

packages/@aws-cdk/assets/README.md

+22
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,25 @@ the asset store, it is uploaded during deployment.
5959

6060
Now, when the toolkit deploys the stack, it will set the relevant CloudFormation
6161
Parameters to point to the actual bucket and key for each asset.
62+
63+
## CloudFormation Resource Metadata
64+
65+
> NOTE: This section is relevant for authors of AWS Resource Constructs.
66+
67+
In certain situations, it is desirable for tools to be able to know that a certain CloudFormation
68+
resource is using a local asset. For example, SAM CLI can be used to invoke AWS Lambda functions
69+
locally for debugging purposes.
70+
71+
To enable such use cases, external tools will consult a set of metadata entries on AWS CloudFormation
72+
resources:
73+
74+
- `aws:asset:path` points to the local path of the asset.
75+
- `aws:asset:property` is the name of the resource property where the asset is used
76+
77+
Using these two metadata entries, tools will be able to identify that assets are used
78+
by a certain resource, and enable advanced local experiences.
79+
80+
To add these metadata entries to a resource, use the
81+
`asset.addResourceMetadata(resource, property)` method.
82+
83+
See https://github.com/awslabs/aws-cdk/issues/1432 for more details

packages/@aws-cdk/assets/lib/asset.ts

+28
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,34 @@ export class Asset extends cdk.Construct {
140140
}
141141
}
142142

143+
/**
144+
* Adds CloudFormation template metadata to the specified resource with
145+
* information that indicates which resource property is mapped to this local
146+
* asset. This can be used by tools such as SAM CLI to provide local
147+
* experience such as local invocation and debugging of Lambda functions.
148+
*
149+
* Asset metadata will only be included if the stack is synthesized with the
150+
* "aws:cdk:enable-asset-metadata" context key defined, which is the default
151+
* behavior when synthesizing via the CDK Toolkit.
152+
*
153+
* @see https://github.com/awslabs/aws-cdk/issues/1432
154+
*
155+
* @param resource The CloudFormation resource which is using this asset.
156+
* @param resourceProperty The property name where this asset is referenced
157+
* (e.g. "Code" for AWS::Lambda::Function)
158+
*/
159+
public addResourceMetadata(resource: cdk.Resource, resourceProperty: string) {
160+
if (!this.getContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT)) {
161+
return; // not enabled
162+
}
163+
164+
// tell tools such as SAM CLI that the "Code" property of this resource
165+
// points to a local path in order to enable local invocation of this function.
166+
resource.options.metadata = resource.options.metadata || { };
167+
resource.options.metadata[cxapi.ASSET_RESOURCE_METADATA_PATH_KEY] = this.assetPath;
168+
resource.options.metadata[cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY] = resourceProperty;
169+
}
170+
143171
/**
144172
* Grants read permissions to the principal on the asset's S3 object.
145173
*/

packages/@aws-cdk/assets/test/test.asset.ts

+46-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { expect, haveResource } from '@aws-cdk/assert';
1+
import { expect, haveResource, ResourcePart } from '@aws-cdk/assert';
22
import iam = require('@aws-cdk/aws-iam');
33
import cdk = require('@aws-cdk/cdk');
4+
import cxapi = require('@aws-cdk/cx-api');
45
import { Test } from 'nodeunit';
56
import path = require('path');
67
import { FileAsset, ZipDirectoryAsset } from '../lib/asset';
@@ -139,6 +140,50 @@ export = {
139140
test.equal(zipDirectoryAsset.isZipArchive, true);
140141
test.equal(zipFileAsset.isZipArchive, true);
141142
test.equal(jarFileAsset.isZipArchive, true);
143+
test.done();
144+
},
145+
146+
'addResourceMetadata can be used to add CFN metadata to resources'(test: Test) {
147+
// GIVEN
148+
const stack = new cdk.Stack();
149+
stack.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true);
150+
151+
const location = path.join(__dirname, 'sample-asset-directory');
152+
const resource = new cdk.Resource(stack, 'MyResource', { type: 'My::Resource::Type' });
153+
const asset = new ZipDirectoryAsset(stack, 'MyAsset', { path: location });
154+
155+
// WHEN
156+
asset.addResourceMetadata(resource, 'PropName');
157+
158+
// THEN
159+
expect(stack).to(haveResource('My::Resource::Type', {
160+
Metadata: {
161+
"aws:asset:path": location,
162+
"aws:asset:property": "PropName"
163+
}
164+
}, ResourcePart.CompleteDefinition));
165+
test.done();
166+
},
167+
168+
'asset metadata is only emitted if ASSET_RESOURCE_METADATA_ENABLED_CONTEXT is defined'(test: Test) {
169+
// GIVEN
170+
const stack = new cdk.Stack();
171+
172+
const location = path.join(__dirname, 'sample-asset-directory');
173+
const resource = new cdk.Resource(stack, 'MyResource', { type: 'My::Resource::Type' });
174+
const asset = new ZipDirectoryAsset(stack, 'MyAsset', { path: location });
175+
176+
// WHEN
177+
asset.addResourceMetadata(resource, 'PropName');
178+
179+
// THEN
180+
expect(stack).notTo(haveResource('My::Resource::Type', {
181+
Metadata: {
182+
"aws:asset:path": location,
183+
"aws:asset:property": "PropName"
184+
}
185+
}, ResourcePart.CompleteDefinition));
186+
142187
test.done();
143188
}
144189
};

packages/@aws-cdk/aws-lambda/lib/code.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export abstract class Code {
5454
* Called during stack synthesis to render the CodePropery for the
5555
* Lambda function.
5656
*/
57-
public abstract toJSON(): CfnFunction.CodeProperty;
57+
public abstract toJSON(resource: CfnFunction): CfnFunction.CodeProperty;
5858

5959
/**
6060
* Called when the lambda is initialized to allow this object to
@@ -81,7 +81,7 @@ export class S3Code extends Code {
8181
this.bucketName = bucket.bucketName;
8282
}
8383

84-
public toJSON(): CfnFunction.CodeProperty {
84+
public toJSON(_: CfnFunction): CfnFunction.CodeProperty {
8585
return {
8686
s3Bucket: this.bucketName,
8787
s3Key: this.key,
@@ -108,7 +108,7 @@ export class InlineCode extends Code {
108108
}
109109
}
110110

111-
public toJSON(): CfnFunction.CodeProperty {
111+
public toJSON(_: CfnFunction): CfnFunction.CodeProperty {
112112
return {
113113
zipFile: this.code
114114
};
@@ -156,7 +156,10 @@ export class AssetCode extends Code {
156156
}
157157
}
158158

159-
public toJSON(): CfnFunction.CodeProperty {
159+
public toJSON(resource: CfnFunction): CfnFunction.CodeProperty {
160+
// https://github.com/awslabs/aws-cdk/issues/1432
161+
this.asset!.addResourceMetadata(resource, 'Code');
162+
160163
return {
161164
s3Bucket: this.asset!.s3BucketName,
162165
s3Key: this.asset!.s3ObjectKey

packages/@aws-cdk/aws-lambda/lib/lambda.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ export class Function extends FunctionRef {
244244
const resource = new CfnFunction(this, 'Resource', {
245245
functionName: props.functionName,
246246
description: props.description,
247-
code: new cdk.Token(() => props.code.toJSON()),
247+
code: new cdk.Token(() => props.code.toJSON(resource)),
248248
handler: props.handler,
249249
timeout: props.timeout,
250250
runtime: props.runtime.name,

packages/@aws-cdk/aws-lambda/test/test.code.ts

+26
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { expect, haveResource, ResourcePart } from '@aws-cdk/assert';
12
import assets = require('@aws-cdk/assets');
23
import cdk = require('@aws-cdk/cdk');
4+
import cxapi = require('@aws-cdk/cx-api');
35
import { Test } from 'nodeunit';
46
import path = require('path');
57
import lambda = require('../lib');
@@ -65,6 +67,30 @@ export = {
6567
test.deepEqual(synthesized.metadata['/MyStack/Func1/Code'][0].type, 'aws:cdk:asset');
6668
test.deepEqual(synthesized.metadata['/MyStack/Func2/Code'], undefined);
6769

70+
test.done();
71+
},
72+
73+
'adds code asset metadata'(test: Test) {
74+
// GIVEN
75+
const stack = new cdk.Stack();
76+
stack.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true);
77+
78+
const location = path.join(__dirname, 'my-lambda-handler');
79+
80+
// WHEN
81+
new lambda.Function(stack, 'Func1', {
82+
code: lambda.Code.asset(location),
83+
runtime: lambda.Runtime.NodeJS810,
84+
handler: 'foom',
85+
});
86+
87+
// THEN
88+
expect(stack).to(haveResource('AWS::Lambda::Function', {
89+
Metadata: {
90+
[cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: location,
91+
[cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code'
92+
}
93+
}, ResourcePart.CompleteDefinition));
6894
test.done();
6995
}
7096
}

packages/@aws-cdk/cx-api/lib/cxapi.ts

-12
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,3 @@ export const PATH_METADATA_KEY = 'aws:cdk:path';
128128
* Enables the embedding of the "aws:cdk:path" in CloudFormation template metadata.
129129
*/
130130
export const PATH_METADATA_ENABLE_CONTEXT = 'aws:cdk:enable-path-metadata';
131-
132-
/**
133-
* Separator string that separates the prefix separator from the object key separator.
134-
*
135-
* Asset keys will look like:
136-
*
137-
* /assets/MyConstruct12345678/||abcdef12345.zip
138-
*
139-
* This allows us to encode both the prefix and the full location in a single
140-
* CloudFormation Template Parameter.
141-
*/
142-
export const ASSET_PREFIX_SEPARATOR = '||';

packages/@aws-cdk/cx-api/lib/metadata/assets.ts

+26
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
export const ASSET_METADATA = 'aws:cdk:asset';
22

3+
/**
4+
* If this is set in the context, the aws:asset:xxx metadata entries will not be
5+
* added to the template. This is used, for example, when we run integrationt
6+
* tests.
7+
*/
8+
export const ASSET_RESOURCE_METADATA_ENABLED_CONTEXT = 'aws:cdk:enable-asset-metadata';
9+
10+
/**
11+
* Metadata added to the CloudFormation template entries that map local assets
12+
* to resources.
13+
*/
14+
export const ASSET_RESOURCE_METADATA_PATH_KEY = 'aws:asset:path';
15+
export const ASSET_RESOURCE_METADATA_PROPERTY_KEY = 'aws:asset:property';
16+
17+
/**
18+
* Separator string that separates the prefix separator from the object key separator.
19+
*
20+
* Asset keys will look like:
21+
*
22+
* /assets/MyConstruct12345678/||abcdef12345.zip
23+
*
24+
* This allows us to encode both the prefix and the full location in a single
25+
* CloudFormation Template Parameter.
26+
*/
27+
export const ASSET_PREFIX_SEPARATOR = '||';
28+
329
export interface FileAssetMetadataEntry {
430
/**
531
* Requested packaging style

packages/aws-cdk/bin/cdk.ts

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ async function parseCommandLineArguments() {
4545
.option('ec2creds', { type: 'boolean', alias: 'i', default: undefined, desc: 'Force trying to fetch EC2 instance credentials. Default: guess EC2 instance status.' })
4646
.option('version-reporting', { type: 'boolean', desc: 'Include the "AWS::CDK::Metadata" resource in synthesized templates (enabled by default)', default: undefined })
4747
.option('path-metadata', { type: 'boolean', desc: 'Include "aws:cdk:path" CloudFormation metadata for each resource (enabled by default)', default: true })
48+
.option('asset-metadata', { type: 'boolean', desc: 'Include "aws:asset:*" CloudFormation metadata for resources that user assets (enabled by default)', default: true })
4849
.option('role-arn', { type: 'string', alias: 'r', desc: 'ARN of Role to use when invoking CloudFormation', default: undefined })
4950
.option('toolkit-stack-name', { type: 'string', desc: 'The name of the CDK toolkit stack' })
5051
.command([ 'list', 'ls' ], 'Lists all stacks in the app', yargs => yargs

packages/aws-cdk/lib/api/cxapp/exec.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,22 @@ export async function execProgram(aws: SDK, config: Settings): Promise<cxapi.Syn
1717

1818
let pathMetadata: boolean = config.get(['pathMetadata']);
1919
if (pathMetadata === undefined) {
20-
pathMetadata = true; // default to true
20+
pathMetadata = true; // defaults to true
2121
}
2222

2323
if (pathMetadata) {
2424
context[cxapi.PATH_METADATA_ENABLE_CONTEXT] = true;
2525
}
2626

27+
let assetMetadata: boolean = config.get(['assetMetadata']);
28+
if (assetMetadata === undefined) {
29+
assetMetadata = true; // defaults to true
30+
}
31+
32+
if (assetMetadata) {
33+
context[cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT] = true;
34+
}
35+
2736
debug('context:', context);
2837

2938
env[cxapi.CONTEXT_ENV] = JSON.stringify(context);

packages/aws-cdk/lib/settings.ts

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export class Settings {
9595
context,
9696
language: argv.language,
9797
pathMetadata: argv.pathMetadata,
98+
assetMetadata: argv.assetMetadata,
9899
plugin: argv.plugin,
99100
requireApproval: argv.requireApproval,
100101
toolkitStackName: argv.toolkitStackName,

tools/cdk-integ-tools/bin/cdk-integ-assert.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ async function main() {
1717
}
1818

1919
const expected = await test.readExpected();
20-
const actual = await test.invoke(['--json', '--no-path-metadata', 'synth'], { json: true, context: STATIC_TEST_CONTEXT });
20+
const actual = await test.invoke(['--json', '--no-path-metadata', '--no-asset-metadata', 'synth'], { json: true, context: STATIC_TEST_CONTEXT });
2121

2222
const diff = diffTemplate(expected, actual);
2323

tools/cdk-integ-tools/bin/cdk-integ.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ async function main() {
2525

2626
const args = new Array<string>();
2727

28-
// inject "--no-path-metadata" so aws:cdk:path entries are not added to CFN metadata
28+
// don't inject cloudformation metadata into template
2929
args.push('--no-path-metadata');
30+
args.push('--no-asset-metadata');
3031

3132
// inject "--verbose" to the command line of "cdk" if we are in verbose mode
3233
if (argv.verbose) {

0 commit comments

Comments
 (0)