diff --git a/packages/@aws-cdk/aws-lambda-nodejs/README.md b/packages/@aws-cdk/aws-lambda-nodejs/README.md index bfaa99eb0b243..6e99c84835046 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/README.md +++ b/packages/@aws-cdk/aws-lambda-nodejs/README.md @@ -148,7 +148,7 @@ new lambda.NodejsFunction(this, 'my-handler', { }, logLevel: LogLevel.SILENT, // defaults to LogLevel.WARNING keepNames: true, // defaults to false - tsconfig: 'custom-tsconfig.json' // use custom-tsconfig.json instead of default, + tsconfig: 'custom-tsconfig.json' // use custom-tsconfig.json instead of default, metafile: true, // include meta file, defaults to false banner : '/* comments */', // by default no comments are passed footer : '/* comments */', // by default no comments are passed @@ -216,7 +216,7 @@ Use `bundling.dockerImage` to use a custom Docker bundling image: ```ts new lambda.NodejsFunction(this, 'my-handler', { bundling: { - dockerImage: cdk.BundlingDockerImage.fromAsset('/path/to/Dockerfile'), + dockerImage: cdk.DockerImage.fromBuild('/path/to/Dockerfile'), }, }); ``` diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts index bd69394ae757c..51da3b8575400 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts @@ -20,7 +20,7 @@ beforeEach(() => { getEsBuildVersionMock.mockReturnValue('0.8.8'); fromAssetMock.mockReturnValue({ image: 'built-image', - cp: () => {}, + cp: () => 'dest-path', run: () => {}, toJSON: () => 'built-image', }); diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index 98994962ec129..1f4ee7e5aaa46 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -36,6 +36,9 @@ runtime code. * `lambda.Code.fromAsset(path)` - specify a directory or a .zip file in the local filesystem which will be zipped and uploaded to S3 before deployment. See also [bundling asset code](#bundling-asset-code). + * `lambda.Code.fromDockerBuild(path, options)` - use the result of a Docker + build as code. The runtime code is expected to be located at `/asset` in the + image and will be zipped and uploaded to S3 as an asset. The following example shows how to define a Python function and deploy the code from the local directory `my-lambda-handler` to it: @@ -450,7 +453,7 @@ new lambda.Function(this, 'Function', { bundling: { image: lambda.Runtime.PYTHON_3_6.bundlingDockerImage, command: [ - 'bash', '-c', + 'bash', '-c', 'pip install -r requirements.txt -t /asset-output && cp -au . /asset-output' ], }, @@ -462,8 +465,8 @@ new lambda.Function(this, 'Function', { Runtimes expose a `bundlingDockerImage` property that points to the [AWS SAM](https://github.com/awslabs/aws-sam-cli) build image. -Use `cdk.BundlingDockerImage.fromRegistry(image)` to use an existing image or -`cdk.BundlingDockerImage.fromAsset(path)` to build a specific image: +Use `cdk.DockerImage.fromRegistry(image)` to use an existing image or +`cdk.DockerImage.fromBuild(path)` to build a specific image: ```ts import * as cdk from '@aws-cdk/core'; @@ -471,7 +474,7 @@ import * as cdk from '@aws-cdk/core'; new lambda.Function(this, 'Function', { code: lambda.Code.fromAsset('/path/to/handler', { bundling: { - image: cdk.BundlingDockerImage.fromAsset('/path/to/dir/with/DockerFile', { + image: cdk.DockerImage.fromBuild('/path/to/dir/with/DockerFile', { buildArgs: { ARG1: 'value1', }, diff --git a/packages/@aws-cdk/aws-lambda/lib/code.ts b/packages/@aws-cdk/aws-lambda/lib/code.ts index 29cd3d02ae4de..b4f41b2804257 100644 --- a/packages/@aws-cdk/aws-lambda/lib/code.ts +++ b/packages/@aws-cdk/aws-lambda/lib/code.ts @@ -57,6 +57,22 @@ export abstract class Code { return new AssetCode(path, options); } + /** + * Loads the function code from an asset created by a Docker build. + * + * By defaut, the asset is expected to be located at `/asset` in the + * image. + * + * @param path The path to the directory containing the Docker file + * @param options Docker build options + */ + public static fromDockerBuild(path: string, options: DockerBuildAssetOptions = {}): AssetCode { + const assetPath = cdk.DockerImage + .fromBuild(path, options) + .cp(options.imagePath ?? '/asset', options.outputPath); + return new AssetCode(assetPath); + } + /** * DEPRECATED * @deprecated use `fromAsset` @@ -488,3 +504,24 @@ export class AssetImageCode extends Code { }; } } + +/** + * Options when creating an asset from a Docker build. + */ +export interface DockerBuildAssetOptions extends cdk.DockerBuildOptions { + /** + * The path in the Docker image where the asset is located after the build + * operation. + * + * @default /asset + */ + readonly imagePath?: string; + + /** + * The path on the local filesystem where the asset will be copied + * using `docker cp`. + * + * @default - a unique temporary directory in the system temp directory + */ + readonly outputPath?: string; +} diff --git a/packages/@aws-cdk/aws-lambda/test/code.test.ts b/packages/@aws-cdk/aws-lambda/test/code.test.ts index 9b99c095c2467..7de7998b19c85 100644 --- a/packages/@aws-cdk/aws-lambda/test/code.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/code.test.ts @@ -327,6 +327,29 @@ describe('code', () => { }); }); }); + + describe('lambda.Code.fromDockerBuild', () => { + test('can use the result of a Docker build as an asset', () => { + // given + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); + + // when + new lambda.Function(stack, 'Fn', { + code: lambda.Code.fromDockerBuild(path.join(__dirname, 'docker-build-lambda')), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_12_X, + }); + + // then + expect(stack).toHaveResource('AWS::Lambda::Function', { + Metadata: { + [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.38cd320fa97b348accac88e48d9cede4923f7cab270ce794c95a665be83681a8', + [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code', + }, + }, ResourcePart.CompleteDefinition); + }); + }); }); function defineFunction(code: lambda.Code, runtime: lambda.Runtime = lambda.Runtime.NODEJS_10_X) { diff --git a/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/Dockerfile b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/Dockerfile new file mode 100644 index 0000000000000..4643fde141850 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/Dockerfile @@ -0,0 +1,3 @@ +FROM public.ecr.aws/amazonlinux/amazonlinux:latest + +COPY index.js /asset diff --git a/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/index.ts b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/index.ts new file mode 100644 index 0000000000000..cc867895b4efc --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/index.ts @@ -0,0 +1,5 @@ +/* eslint-disable no-console */ +export async function handler(event: any) { + console.log('Event: %j', event); + return event; +} diff --git a/packages/@aws-cdk/aws-s3-assets/README.md b/packages/@aws-cdk/aws-s3-assets/README.md index f2583b7c10a24..7a751410a2b22 100644 --- a/packages/@aws-cdk/aws-s3-assets/README.md +++ b/packages/@aws-cdk/aws-s3-assets/README.md @@ -88,8 +88,8 @@ The following example uses custom asset bundling to convert a markdown file to h [Example of using asset bundling](./test/integ.assets.bundling.lit.ts). -The bundling docker image (`image`) can either come from a registry (`BundlingDockerImage.fromRegistry`) -or it can be built from a `Dockerfile` located inside your project (`BundlingDockerImage.fromAsset`). +The bundling docker image (`image`) can either come from a registry (`DockerImage.fromRegistry`) +or it can be built from a `Dockerfile` located inside your project (`DockerImage.fromBuild`). You can set the `CDK_DOCKER` environment variable in order to provide a custom docker program to execute. This may sometime be needed when building in @@ -114,7 +114,7 @@ new assets.Asset(this, 'BundledAsset', { }, }, // Docker bundling fallback - image: BundlingDockerImage.fromRegistry('alpine'), + image: DockerImage.fromRegistry('alpine'), entrypoint: ['/bin/sh', '-c'], command: ['bundle'], }, @@ -135,7 +135,7 @@ Use `BundlingOutput.NOT_ARCHIVED` if the bundling output must always be zipped: const asset = new assets.Asset(this, 'BundledAsset', { path: '/path/to/asset', bundling: { - image: BundlingDockerImage.fromRegistry('alpine'), + image: DockerImage.fromRegistry('alpine'), command: ['command-that-produces-an-archive.sh'], outputType: BundlingOutput.NOT_ARCHIVED, // Bundling output will be zipped even though it produces a single archive file. }, diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts index 57885ceeca05c..059f2260c967b 100644 --- a/packages/@aws-cdk/core/lib/bundling.ts +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -135,6 +135,8 @@ export interface ILocalBundling { /** * A Docker image used for asset bundling + * + * @deprecated use DockerImage */ export class BundlingDockerImage { /** @@ -151,6 +153,8 @@ export class BundlingDockerImage { * * @param path The path to the directory containing the Docker file * @param options Docker build options + * + * @deprecated use DockerImage.fromBuild() */ public static fromAsset(path: string, options: DockerBuildOptions = {}) { const buildArgs = options.buildArgs || {}; @@ -181,7 +185,7 @@ export class BundlingDockerImage { } /** @param image The Docker image */ - private constructor(public readonly image: string, private readonly _imageHash?: string) {} + protected constructor(public readonly image: string, private readonly _imageHash?: string) {} /** * Provides a stable representation of this image for JSON serialization. @@ -229,10 +233,16 @@ export class BundlingDockerImage { } /** - * Copies a file or directory out of the Docker image to the local filesystem + * Copies a file or directory out of the Docker image to the local filesystem. + * + * If `outputPath` is omitted the destination path is a temporary directory. + * + * @param imagePath the path in the Docker image + * @param outputPath the destination path for the copy operation + * @returns the destination path */ - public cp(imagePath: string, outputPath: string) { - const { stdout } = dockerExec(['create', this.image]); + public cp(imagePath: string, outputPath?: string): string { + const { stdout } = dockerExec(['create', this.image], {}); // Empty options to avoid stdout redirect here const match = stdout.toString().match(/([0-9a-f]{16,})/); if (!match) { throw new Error('Failed to extract container ID from Docker create output'); @@ -240,16 +250,33 @@ export class BundlingDockerImage { const containerId = match[1]; const containerPath = `${containerId}:${imagePath}`; + const destPath = outputPath ?? FileSystem.mkdtemp('cdk-docker-cp-'); try { - dockerExec(['cp', containerPath, outputPath]); + dockerExec(['cp', containerPath, destPath]); + return destPath; } catch (err) { - throw new Error(`Failed to copy files from ${containerPath} to ${outputPath}: ${err}`); + throw new Error(`Failed to copy files from ${containerPath} to ${destPath}: ${err}`); } finally { dockerExec(['rm', '-v', containerId]); } } } +/** + * A Docker image + */ +export class DockerImage extends BundlingDockerImage { + /** + * Builds a Docker image + * + * @param path The path to the directory containing the Docker file + * @param options Docker build options + */ + public static fromBuild(path: string, options: DockerBuildOptions = {}) { + return BundlingDockerImage.fromAsset(path, options); + } +} + /** * A Docker volume */ diff --git a/packages/@aws-cdk/core/test/bundling.test.ts b/packages/@aws-cdk/core/test/bundling.test.ts index 258860d65585c..8b03dce3da0d3 100644 --- a/packages/@aws-cdk/core/test/bundling.test.ts +++ b/packages/@aws-cdk/core/test/bundling.test.ts @@ -3,7 +3,7 @@ import * as crypto from 'crypto'; import * as path from 'path'; import { nodeunitShim, Test } from 'nodeunit-shim'; import * as sinon from 'sinon'; -import { BundlingDockerImage, FileSystem } from '../lib'; +import { BundlingDockerImage, DockerImage, FileSystem } from '../lib'; nodeunitShim({ 'tearDown'(callback: any) { @@ -263,4 +263,25 @@ nodeunitShim({ test.ok(spawnSyncStub.calledWith(sinon.match.any, ['rm', '-v', containerId])); test.done(); }, + + 'cp utility copies to a temp dir of outputPath is omitted'(test: Test) { + // GIVEN + const containerId = '1234567890abcdef1234567890abcdef'; + sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from(`${containerId}\n`), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + // WHEN + const tempPath = DockerImage.fromRegistry('alpine').cp('/foo/bar'); + + // THEN + test.ok(/cdk-docker-cp-/.test(tempPath)); + + test.done(); + }, });