Skip to content

Commit

Permalink
feat(lambda): docker code
Browse files Browse the repository at this point in the history
Add `Code.fromDockerImage` and `Code.fromDockerAsset` that offer a generic way
of working with Docker for Lambda code.

`Code.fromDockerImage`: run a command in an existing Docker image
`Code.fromDockerAsset`: build an image and then run a command in it

Both `Code` classes take an `assetPath` prop that corresponds to the path of the
asset directory that will contain the build output of the Docker container. This
path is automatically mounted at `/asset` in the container. Using a combination
of image and command, the container is then responsible for putting content at
this location. Additional volumes can be mounted if needed.

This will allow to refactor `aws-lambda-nodejs` and create other language
specific modules.
  • Loading branch information
jogold committed May 10, 2020
1 parent 685a4bf commit 3840a4a
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 0 deletions.
157 changes: 157 additions & 0 deletions packages/@aws-cdk/aws-lambda/lib/code.ts
Original file line number Diff line number Diff line change
@@ -1,6 +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';

export abstract class Code {
/**
Expand Down Expand Up @@ -43,6 +45,20 @@ export abstract class Code {
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 @@ -206,6 +222,143 @@ export class AssetCode extends Code {
}
}

/**
* A Docker volume
*/
export interface DockerVolume {
/**
* The path to the file or directory on the host machine
*/
readonly hostPath: string;

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

/**
* Docker run options
*/
export interface DockerRunOptions {
/**
* 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.
*
* @default - no additional volumes are mounted
*/
readonly volumes?: DockerVolume[];
}

/**
* Options for DockerImageCode
*/
export interface DockerImageCodeOptions extends DockerRunOptions {
/**
* The Docker image where the command will run.
*/
readonly image: string;
}

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

const volumes = options.volumes || [];

const dockerArgs: string[] = [
'run', '--rm',
'-v', `${options.assetPath}:/asset`,
...flatten(volumes.map(v => ['-v', `${v.hostPath}:${v.containerPath}`])),
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);
}
}

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

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

/**
* Lambda code from a command run in a Docker image built from a Dockerfile.
*/
export class DockerAssetCode extends DockerImageCode {
constructor(options: DockerAssetCodeOptions) {
const buildArgs = options.buildArgs || {};

const dockerArgs: string[] = [
'build',
...flatten(Object.entries(buildArgs).map(([k, v]) => ['--build-arg', `${k}=${v}`])),
options.dockerPath,
];

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()}`);
}

const match = docker.stdout.toString().match(/Successfully built ([a-z0-9]+)/);

if (!match) {
throw new Error('Failed to extract image ID from Docker build output');
}

super({
assetPath: options.assetPath,
command: options.command,
image: match[1],
volumes: options.volumes,
});
}
}

export interface ResourceBindOptions {
/**
* The name of the CloudFormation property to annotate with asset metadata.
Expand Down Expand Up @@ -312,3 +465,7 @@ export class CfnParametersCode extends Code {
}
}
}

function flatten(x: string[][]) {
return Array.prototype.concat([], ...x);
}
103 changes: 103 additions & 0 deletions packages/@aws-cdk/aws-lambda/test/integ.docker.expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
{
"Resources": {
"FunctionServiceRole675BB04A": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
]
]
}
]
}
},
"Function76856677": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": {
"Ref": "AssetParametersb30f71084d0e75786c0a52e418612bc916c98f85291d24847aa53400a0c735e8S3BucketB1206B28"
},
"S3Key": {
"Fn::Join": [
"",
[
{
"Fn::Select": [
0,
{
"Fn::Split": [
"||",
{
"Ref": "AssetParametersb30f71084d0e75786c0a52e418612bc916c98f85291d24847aa53400a0c735e8S3VersionKeyDD15AE2C"
}
]
}
]
},
{
"Fn::Select": [
1,
{
"Fn::Split": [
"||",
{
"Ref": "AssetParametersb30f71084d0e75786c0a52e418612bc916c98f85291d24847aa53400a0c735e8S3VersionKeyDD15AE2C"
}
]
}
]
}
]
]
}
},
"Handler": "index.handler",
"Role": {
"Fn::GetAtt": [
"FunctionServiceRole675BB04A",
"Arn"
]
},
"Runtime": "python3.6"
},
"DependsOn": [
"FunctionServiceRole675BB04A"
]
}
},
"Parameters": {
"AssetParametersb30f71084d0e75786c0a52e418612bc916c98f85291d24847aa53400a0c735e8S3BucketB1206B28": {
"Type": "String",
"Description": "S3 bucket for asset \"b30f71084d0e75786c0a52e418612bc916c98f85291d24847aa53400a0c735e8\""
},
"AssetParametersb30f71084d0e75786c0a52e418612bc916c98f85291d24847aa53400a0c735e8S3VersionKeyDD15AE2C": {
"Type": "String",
"Description": "S3 key for asset version \"b30f71084d0e75786c0a52e418612bc916c98f85291d24847aa53400a0c735e8\""
},
"AssetParametersb30f71084d0e75786c0a52e418612bc916c98f85291d24847aa53400a0c735e8ArtifactHash9F155131": {
"Type": "String",
"Description": "Artifact hash for asset \"b30f71084d0e75786c0a52e418612bc916c98f85291d24847aa53400a0c735e8\""
}
}
}
27 changes: 27 additions & 0 deletions packages/@aws-cdk/aws-lambda/test/integ.docker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { App, Construct, Stack, StackProps } from '@aws-cdk/core';
import * as path from 'path';
import * as lambda from '../lib';

class TestStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

new lambda.Function(this, 'Function', {
code: lambda.Code.fromDockerImage({
image: 'python:3.6',
assetPath: path.join(__dirname, 'python-lambda-handler'), // this is /asset in the container
command: [
'pip3', 'install',
'-r', '/asset/requirements.txt',
'-t', '/asset',
],
}),
runtime: lambda.Runtime.PYTHON_3_6,
handler: 'index.handler',
});
}
}

const app = new App();
new TestStack(app, 'cdk-integ-lambda-docker');
app.synth();
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import requests

def handler(event, context):
r = requests.get('https://aws.amazon.com')
print(r.status_code)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
requests==2.23.0
Loading

0 comments on commit 3840a4a

Please sign in to comment.