Skip to content

Commit ad01099

Browse files
authored
feat(lambda): Code.fromDockerBuild (#13318)
Use the result of a Docker build as code. The runtime code is expected to be located at `/asset` in the image. Also deprecate `BundlingDockerImage` in favor of `DockerImage`. Closes #13273 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent fe4f056 commit ad01099

File tree

10 files changed

+137
-18
lines changed

10 files changed

+137
-18
lines changed

packages/@aws-cdk/aws-lambda-nodejs/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ new lambda.NodejsFunction(this, 'my-handler', {
148148
},
149149
logLevel: LogLevel.SILENT, // defaults to LogLevel.WARNING
150150
keepNames: true, // defaults to false
151-
tsconfig: 'custom-tsconfig.json' // use custom-tsconfig.json instead of default,
151+
tsconfig: 'custom-tsconfig.json' // use custom-tsconfig.json instead of default,
152152
metafile: true, // include meta file, defaults to false
153153
banner : '/* comments */', // by default no comments are passed
154154
footer : '/* comments */', // by default no comments are passed
@@ -220,7 +220,7 @@ Use `bundling.dockerImage` to use a custom Docker bundling image:
220220
```ts
221221
new lambda.NodejsFunction(this, 'my-handler', {
222222
bundling: {
223-
dockerImage: cdk.BundlingDockerImage.fromAsset('/path/to/Dockerfile'),
223+
dockerImage: cdk.DockerImage.fromBuild('/path/to/Dockerfile'),
224224
},
225225
});
226226
```

packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ beforeEach(() => {
2020
getEsBuildVersionMock.mockReturnValue('0.8.8');
2121
fromAssetMock.mockReturnValue({
2222
image: 'built-image',
23-
cp: () => {},
23+
cp: () => 'dest-path',
2424
run: () => {},
2525
toJSON: () => 'built-image',
2626
});

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

+7-4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ runtime code.
3636
* `lambda.Code.fromAsset(path)` - specify a directory or a .zip file in the local
3737
filesystem which will be zipped and uploaded to S3 before deployment. See also
3838
[bundling asset code](#bundling-asset-code).
39+
* `lambda.Code.fromDockerBuild(path, options)` - use the result of a Docker
40+
build as code. The runtime code is expected to be located at `/asset` in the
41+
image and will be zipped and uploaded to S3 as an asset.
3942

4043
The following example shows how to define a Python function and deploy the code
4144
from the local directory `my-lambda-handler` to it:
@@ -450,7 +453,7 @@ new lambda.Function(this, 'Function', {
450453
bundling: {
451454
image: lambda.Runtime.PYTHON_3_6.bundlingDockerImage,
452455
command: [
453-
'bash', '-c',
456+
'bash', '-c',
454457
'pip install -r requirements.txt -t /asset-output && cp -au . /asset-output'
455458
],
456459
},
@@ -462,16 +465,16 @@ new lambda.Function(this, 'Function', {
462465

463466
Runtimes expose a `bundlingDockerImage` property that points to the [AWS SAM](https://github.com/awslabs/aws-sam-cli) build image.
464467

465-
Use `cdk.BundlingDockerImage.fromRegistry(image)` to use an existing image or
466-
`cdk.BundlingDockerImage.fromAsset(path)` to build a specific image:
468+
Use `cdk.DockerImage.fromRegistry(image)` to use an existing image or
469+
`cdk.DockerImage.fromBuild(path)` to build a specific image:
467470

468471
```ts
469472
import * as cdk from '@aws-cdk/core';
470473

471474
new lambda.Function(this, 'Function', {
472475
code: lambda.Code.fromAsset('/path/to/handler', {
473476
bundling: {
474-
image: cdk.BundlingDockerImage.fromAsset('/path/to/dir/with/DockerFile', {
477+
image: cdk.DockerImage.fromBuild('/path/to/dir/with/DockerFile', {
475478
buildArgs: {
476479
ARG1: 'value1',
477480
},

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

+37
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@ export abstract class Code {
5757
return new AssetCode(path, options);
5858
}
5959

60+
/**
61+
* Loads the function code from an asset created by a Docker build.
62+
*
63+
* By defaut, the asset is expected to be located at `/asset` in the
64+
* image.
65+
*
66+
* @param path The path to the directory containing the Docker file
67+
* @param options Docker build options
68+
*/
69+
public static fromDockerBuild(path: string, options: DockerBuildAssetOptions = {}): AssetCode {
70+
const assetPath = cdk.DockerImage
71+
.fromBuild(path, options)
72+
.cp(options.imagePath ?? '/asset', options.outputPath);
73+
return new AssetCode(assetPath);
74+
}
75+
6076
/**
6177
* DEPRECATED
6278
* @deprecated use `fromAsset`
@@ -488,3 +504,24 @@ export class AssetImageCode extends Code {
488504
};
489505
}
490506
}
507+
508+
/**
509+
* Options when creating an asset from a Docker build.
510+
*/
511+
export interface DockerBuildAssetOptions extends cdk.DockerBuildOptions {
512+
/**
513+
* The path in the Docker image where the asset is located after the build
514+
* operation.
515+
*
516+
* @default /asset
517+
*/
518+
readonly imagePath?: string;
519+
520+
/**
521+
* The path on the local filesystem where the asset will be copied
522+
* using `docker cp`.
523+
*
524+
* @default - a unique temporary directory in the system temp directory
525+
*/
526+
readonly outputPath?: string;
527+
}

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

+23
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,29 @@ describe('code', () => {
329329
});
330330
});
331331
});
332+
333+
describe('lambda.Code.fromDockerBuild', () => {
334+
test('can use the result of a Docker build as an asset', () => {
335+
// given
336+
const stack = new cdk.Stack();
337+
stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true);
338+
339+
// when
340+
new lambda.Function(stack, 'Fn', {
341+
code: lambda.Code.fromDockerBuild(path.join(__dirname, 'docker-build-lambda')),
342+
handler: 'index.handler',
343+
runtime: lambda.Runtime.NODEJS_12_X,
344+
});
345+
346+
// then
347+
expect(stack).toHaveResource('AWS::Lambda::Function', {
348+
Metadata: {
349+
[cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.38cd320fa97b348accac88e48d9cede4923f7cab270ce794c95a665be83681a8',
350+
[cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code',
351+
},
352+
}, ResourcePart.CompleteDefinition);
353+
});
354+
});
332355
});
333356

334357
function defineFunction(code: lambda.Code, runtime: lambda.Runtime = lambda.Runtime.NODEJS_10_X) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM public.ecr.aws/amazonlinux/amazonlinux:latest
2+
3+
COPY index.js /asset
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/* eslint-disable no-console */
2+
export async function handler(event: any) {
3+
console.log('Event: %j', event);
4+
return event;
5+
}

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ The following example uses custom asset bundling to convert a markdown file to h
8888

8989
[Example of using asset bundling](./test/integ.assets.bundling.lit.ts).
9090

91-
The bundling docker image (`image`) can either come from a registry (`BundlingDockerImage.fromRegistry`)
92-
or it can be built from a `Dockerfile` located inside your project (`BundlingDockerImage.fromAsset`).
91+
The bundling docker image (`image`) can either come from a registry (`DockerImage.fromRegistry`)
92+
or it can be built from a `Dockerfile` located inside your project (`DockerImage.fromBuild`).
9393

9494
You can set the `CDK_DOCKER` environment variable in order to provide a custom
9595
docker program to execute. This may sometime be needed when building in
@@ -114,7 +114,7 @@ new assets.Asset(this, 'BundledAsset', {
114114
},
115115
},
116116
// Docker bundling fallback
117-
image: BundlingDockerImage.fromRegistry('alpine'),
117+
image: DockerImage.fromRegistry('alpine'),
118118
entrypoint: ['/bin/sh', '-c'],
119119
command: ['bundle'],
120120
},
@@ -135,7 +135,7 @@ Use `BundlingOutput.NOT_ARCHIVED` if the bundling output must always be zipped:
135135
const asset = new assets.Asset(this, 'BundledAsset', {
136136
path: '/path/to/asset',
137137
bundling: {
138-
image: BundlingDockerImage.fromRegistry('alpine'),
138+
image: DockerImage.fromRegistry('alpine'),
139139
command: ['command-that-produces-an-archive.sh'],
140140
outputType: BundlingOutput.NOT_ARCHIVED, // Bundling output will be zipped even though it produces a single archive file.
141141
},

packages/@aws-cdk/core/lib/bundling.ts

+33-6
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ export interface ILocalBundling {
136136

137137
/**
138138
* A Docker image used for asset bundling
139+
*
140+
* @deprecated use DockerImage
139141
*/
140142
export class BundlingDockerImage {
141143
/**
@@ -152,6 +154,8 @@ export class BundlingDockerImage {
152154
*
153155
* @param path The path to the directory containing the Docker file
154156
* @param options Docker build options
157+
*
158+
* @deprecated use DockerImage.fromBuild()
155159
*/
156160
public static fromAsset(path: string, options: DockerBuildOptions = {}) {
157161
const buildArgs = options.buildArgs || {};
@@ -184,7 +188,7 @@ export class BundlingDockerImage {
184188
}
185189

186190
/** @param image The Docker image */
187-
private constructor(public readonly image: string, private readonly _imageHash?: string) {}
191+
protected constructor(public readonly image: string, private readonly _imageHash?: string) {}
188192

189193
/**
190194
* Provides a stable representation of this image for JSON serialization.
@@ -232,27 +236,50 @@ export class BundlingDockerImage {
232236
}
233237

234238
/**
235-
* Copies a file or directory out of the Docker image to the local filesystem
239+
* Copies a file or directory out of the Docker image to the local filesystem.
240+
*
241+
* If `outputPath` is omitted the destination path is a temporary directory.
242+
*
243+
* @param imagePath the path in the Docker image
244+
* @param outputPath the destination path for the copy operation
245+
* @returns the destination path
236246
*/
237-
public cp(imagePath: string, outputPath: string) {
238-
const { stdout } = dockerExec(['create', this.image]);
247+
public cp(imagePath: string, outputPath?: string): string {
248+
const { stdout } = dockerExec(['create', this.image], {}); // Empty options to avoid stdout redirect here
239249
const match = stdout.toString().match(/([0-9a-f]{16,})/);
240250
if (!match) {
241251
throw new Error('Failed to extract container ID from Docker create output');
242252
}
243253

244254
const containerId = match[1];
245255
const containerPath = `${containerId}:${imagePath}`;
256+
const destPath = outputPath ?? FileSystem.mkdtemp('cdk-docker-cp-');
246257
try {
247-
dockerExec(['cp', containerPath, outputPath]);
258+
dockerExec(['cp', containerPath, destPath]);
259+
return destPath;
248260
} catch (err) {
249-
throw new Error(`Failed to copy files from ${containerPath} to ${outputPath}: ${err}`);
261+
throw new Error(`Failed to copy files from ${containerPath} to ${destPath}: ${err}`);
250262
} finally {
251263
dockerExec(['rm', '-v', containerId]);
252264
}
253265
}
254266
}
255267

268+
/**
269+
* A Docker image
270+
*/
271+
export class DockerImage extends BundlingDockerImage {
272+
/**
273+
* Builds a Docker image
274+
*
275+
* @param path The path to the directory containing the Docker file
276+
* @param options Docker build options
277+
*/
278+
public static fromBuild(path: string, options: DockerBuildOptions = {}) {
279+
return BundlingDockerImage.fromAsset(path, options);
280+
}
281+
}
282+
256283
/**
257284
* A Docker volume
258285
*/

packages/@aws-cdk/core/test/bundling.test.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as crypto from 'crypto';
33
import * as path from 'path';
44
import { nodeunitShim, Test } from 'nodeunit-shim';
55
import * as sinon from 'sinon';
6-
import { BundlingDockerImage, FileSystem } from '../lib';
6+
import { BundlingDockerImage, DockerImage, FileSystem } from '../lib';
77

88
nodeunitShim({
99
'tearDown'(callback: any) {
@@ -265,4 +265,25 @@ nodeunitShim({
265265
test.ok(spawnSyncStub.calledWith(sinon.match.any, ['rm', '-v', containerId]));
266266
test.done();
267267
},
268+
269+
'cp utility copies to a temp dir of outputPath is omitted'(test: Test) {
270+
// GIVEN
271+
const containerId = '1234567890abcdef1234567890abcdef';
272+
sinon.stub(child_process, 'spawnSync').returns({
273+
status: 0,
274+
stderr: Buffer.from('stderr'),
275+
stdout: Buffer.from(`${containerId}\n`),
276+
pid: 123,
277+
output: ['stdout', 'stderr'],
278+
signal: null,
279+
});
280+
281+
// WHEN
282+
const tempPath = DockerImage.fromRegistry('alpine').cp('/foo/bar');
283+
284+
// THEN
285+
test.ok(/cdk-docker-cp-/.test(tempPath));
286+
287+
test.done();
288+
},
268289
});

0 commit comments

Comments
 (0)