Skip to content

Commit

Permalink
PR feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
jogold committed May 18, 2020
1 parent 2d780e6 commit 3d3e7c2
Show file tree
Hide file tree
Showing 8 changed files with 408 additions and 304 deletions.
48 changes: 43 additions & 5 deletions packages/@aws-cdk/aws-lambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,8 @@ runtime code.
* `lambda.Code.fromInline(code)` - inline the handle code as a string. This is
limited to supported runtimes and the code cannot exceed 4KiB.
* `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.
* `lambda.Code.fromDockerImage(options)` - code from a command run in an existing
Docker image.
* `lambda.Code.fromDockerAsset(options)` - code from a command run in a Docker image
built from a Dockerfile.
filesystem which will be zipped and uploaded to S3 before deployment. See also
[using Docker with asset code](#Using-Docker-With-Asset-Code).

The following example shows how to define a Python function and deploy the code
from the local directory `my-lambda-handler` to it:
Expand Down Expand Up @@ -258,6 +255,47 @@ number of times and with different properties. Using `SingletonFunction` here wi
For example, the `LogRetention` construct requires only one single lambda function for all different log groups whose
retention it seeks to manage.

### Using Docker with Asset Code
When using `lambda.Code.fromAsset(path)` it is possible to "act" on the code by running a
command in a Docker container. By default, the asset path is mounted in the container
at `/asset` and is set as the working directory.

Example with Python:
```ts
new lambda.Function(this, 'Function', {
code: lambda.Code.fromAsset(path.join(__dirname, 'my-python-handler'), {
bundle: {
image: lambda.DockerImage.fromImage('python:3.6'), // Use an existing image
command: [
'pip', 'install',
'-r', 'requirements.txt',
'-t', '.',
],
},
}),
runtime: lambda.Runtime.PYTHON_3_6,
handler: 'index.handler',
});
```

Use `lambda.DockerImage.fromBuild(path)` to build a specific image:

```ts
new lambda.Function(this, 'Function', {
code: lambda.Code.fromAsset('/path/to/handler'), {
bundle: {
image: lambda.DockerImage.fromBuild('/path/to/dir/with/DockerFile', {
buildArgs: {
ARG1: 'value1',
},
}),
command: ['my', 'cool', 'command'],
},
}),
// ...
});
```

### Language-specific APIs
Language-specific higher level constructs are provided in separate modules:

Expand Down
242 changes: 71 additions & 171 deletions packages/@aws-cdk/aws-lambda/lib/code.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as s3 from '@aws-cdk/aws-s3';
import * as s3_assets from '@aws-cdk/aws-s3-assets';
import * as cdk from '@aws-cdk/core';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import { DockerImage, DockerVolume } from './docker';

export abstract class Code {
/**
Expand Down Expand Up @@ -42,24 +42,10 @@ export abstract class Code {
*
* @param path Either a directory with the Lambda code bundle or a .zip file
*/
public static fromAsset(path: string, options?: s3_assets.AssetOptions): AssetCode {
public static fromAsset(path: string, options?: AssetCodeOptions): AssetCode {
return new AssetCode(path, options);
}

/**
* Lambda code from a command run in an existing Docker image.
*/
public static fromDockerImage(options: DockerImageCodeOptions): AssetCode {
return new DockerImageCode(options);
}

/**
* Lambda code from a command run in a Docker image built from a Dockerfile.
*/
public static fromDockerAsset(options: DockerAssetCodeOptions): AssetCode {
return new DockerAssetCode(options);
}

/**
* @deprecated use `fromAsset`
*/
Expand Down Expand Up @@ -177,200 +163,118 @@ export class InlineCode extends Code {
}

/**
* Lambda code from a local directory.
* Bundle options
*/
export class AssetCode extends Code {
public readonly isInline = false;
private asset?: s3_assets.Asset;

export interface BundleOptions {
/**
* @param path The path to the asset file or directory.
*/
constructor(public readonly path: string, private readonly options: s3_assets.AssetOptions = { }) {
super();
}

public bind(scope: cdk.Construct): CodeConfig {
// If the same AssetCode is used multiple times, retain only the first instantiation.
if (!this.asset) {
this.asset = new s3_assets.Asset(scope, 'Code', {
path: this.path,
...this.options,
});
}

if (!this.asset.isZipArchive) {
throw new Error(`Asset must be a .zip file or a directory (${this.path})`);
}

return {
s3Location: {
bucketName: this.asset.s3BucketName,
objectKey: this.asset.s3ObjectKey,
},
};
}

public bindToResource(resource: cdk.CfnResource, options: ResourceBindOptions = { }) {
if (!this.asset) {
throw new Error('bindToResource() must be called after bind()');
}

const resourceProperty = options.resourceProperty || 'Code';

// https://github.com/aws/aws-cdk/issues/1432
this.asset.addResourceMetadata(resource, resourceProperty);
}
}

/**
* A Docker volume
*/
export interface DockerVolume {
/**
* The path to the file or directory on the host machine
* The Docker image where the command will run.
*/
readonly hostPath: string;
readonly image: DockerImage;

/**
* The path where the file or directory is mounted in the container
*/
readonly containerPath: string;
}

/**
* Docker run options
*/
export interface DockerRunOptions extends s3_assets.AssetOptions {
/**
* The command to run in the container.
*/
readonly command: string[];

/**
* The path of the asset directory that will contain the build output of the Docker
* container. This path is mounted at `/asset` in the container. It is created
* if it doesn't exist.
*/
readonly assetPath: string;

/**
* Additional Docker volumes to mount.
* Docker volumes to mount.
*
* @default - no additional volumes are mounted
* @default - The path to the asset file or directory is mounted at /asset
*/
readonly volumes?: DockerVolume[];

/**
* The environment variables to pass to the container.
*
* @default - No environment variables.
* @default - no environment variables.
*/
readonly environment?: { [key: string]: string; };
}

/**
* Options for DockerImageCode
*/
export interface DockerImageCodeOptions extends DockerRunOptions {
/**
* The Docker image where the command will run.
* Working directory inside the container.
*
* @default - the `containerPath` of the first mounted volume.
*/
readonly image: string;
readonly workingDirectory?: string;
}

/**
* Lambda code from a command run in an existing Docker image
* Asset code options
*/
export class DockerImageCode extends AssetCode {
constructor(options: DockerImageCodeOptions) {
if (!fs.existsSync(options.assetPath)) {
fs.mkdirSync(options.assetPath);
}

const volumes = options.volumes || [];
const environment = options.environment || {};

const dockerArgs: string[] = [
'run', '--rm',
'-v', `${options.assetPath}:/asset`,
...flatten(volumes.map(v => ['-v', `${v.hostPath}:${v.containerPath}`])),
...flatten(Object.entries(environment).map(([k, v]) => ['--env', `${k}=${v}`])),
options.image,
...options.command,
];

const docker = spawnSync('docker', dockerArgs);

if (docker.error) {
throw docker.error;
}

if (docker.status !== 0) {
throw new Error(`[Status ${docker.status}] stdout: ${docker.stdout?.toString().trim()}\n\n\nstderr: ${docker.stderr?.toString().trim()}`);
}

super(options.assetPath, {
exclude: options.exclude,
follow: options.follow,
readers: options.readers,
sourceHash: options.sourceHash,
});
}
}

/**
* Options for DockerAssetCode
*/
export interface DockerAssetCodeOptions extends DockerRunOptions {
export interface AssetCodeOptions extends s3_assets.AssetOptions {
/**
* The path to the directory containing the Docker file.
*/
readonly dockerPath: string;

/**
* Build args
* Bundle options
*
* @default - no build args
* @default - no bundling
*/
readonly buildArgs?: { [key: string]: string };
readonly bundle?: BundleOptions;
}

/**
* Lambda code from a command run in a Docker image built from a Dockerfile.
* Lambda code from a local directory.
*/
export class DockerAssetCode extends DockerImageCode {
constructor(options: DockerAssetCodeOptions) {
const buildArgs = options.buildArgs || {};
export class AssetCode extends Code {
public readonly isInline = false;
private asset?: s3_assets.Asset;

const dockerArgs: string[] = [
'build',
...flatten(Object.entries(buildArgs).map(([k, v]) => ['--build-arg', `${k}=${v}`])),
options.dockerPath,
];
/**
* @param path The path to the asset file or directory.
*/
constructor(public readonly path: string, private readonly options: AssetCodeOptions = {}) {
super();
}

const docker = spawnSync('docker', dockerArgs);
public bind(scope: cdk.Construct): CodeConfig {
if (this.options.bundle) {
// We are going to mount it, so ensure it exists
if (!fs.existsSync(this.path)) {
fs.mkdirSync(this.path);
}

const volumes = this.options.bundle.volumes ?? [
{
hostPath: this.path,
containerPath: '/asset',
},
];

this.options.bundle.image.run({
command: this.options.bundle.command,
volumes,
environment: this.options.bundle.environment,
workingDirectory: this.options.bundle.workingDirectory ?? volumes[0].containerPath,
});
}

if (docker.error) {
throw docker.error;
// If the same AssetCode is used multiple times, retain only the first instantiation.
if (!this.asset) {
this.asset = new s3_assets.Asset(scope, 'Code', {
path: this.path,
...this.options,
});
}

if (docker.status !== 0) {
throw new Error(`[Status ${docker.status}] stdout: ${docker.stdout?.toString().trim()}\n\n\nstderr: ${docker.stderr?.toString().trim()}`);
if (!this.asset.isZipArchive) {
throw new Error(`Asset must be a .zip file or a directory (${this.path})`);
}

const match = docker.stdout.toString().match(/Successfully built ([a-z0-9]+)/);
return {
s3Location: {
bucketName: this.asset.s3BucketName,
objectKey: this.asset.s3ObjectKey,
},
};
}

if (!match) {
throw new Error('Failed to extract image ID from Docker build output');
public bindToResource(resource: cdk.CfnResource, options: ResourceBindOptions = { }) {
if (!this.asset) {
throw new Error('bindToResource() must be called after bind()');
}

super({
assetPath: options.assetPath,
command: options.command,
image: match[1],
volumes: options.volumes,
});
const resourceProperty = options.resourceProperty || 'Code';

// https://github.com/aws/aws-cdk/issues/1432
this.asset.addResourceMetadata(resource, resourceProperty);
}
}

Expand Down Expand Up @@ -480,7 +384,3 @@ export class CfnParametersCode extends Code {
}
}
}

function flatten(x: string[][]) {
return Array.prototype.concat([], ...x);
}
Loading

0 comments on commit 3d3e7c2

Please sign in to comment.