Skip to content

Commit

Permalink
feat(lambda): container images (#11809)
Browse files Browse the repository at this point in the history
Adds support for deploying ECR container images as Lambda function
handlers.
  • Loading branch information
Niranjan Jayakar authored Dec 1, 2020
1 parent 7708242 commit 02ced10
Show file tree
Hide file tree
Showing 15 changed files with 799 additions and 22 deletions.
26 changes: 26 additions & 0 deletions packages/@aws-cdk/aws-lambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,32 @@ to our CDK project directory. This is especially important when we want to share
this construct through a library. Different programming languages will have
different techniques for bundling resources into libraries.

### Docker Images

Lambda functions allow specifying their handlers within docker images. The docker
image can be an image from ECR or a local asset that the CDK will package and load
into ECR.

The following `DockerImageFunction` construct uses a local folder with a
Dockerfile as the asset that will be used as the function handler.

```ts
new lambda.DockerImageFunction(this, 'AssetFunction', {
code: DockerImageCode.fromAssetImage(path.join(__dirname, 'docker-handler')),
});
```

You can also specify an image that already exists in ECR as the function handler.

```ts
import * as ecr from '@aws-cdk/aws-ecr';
const repo = new ecr.Repository(this, 'Repository');

new lambda.DockerImageFunction(this, 'ECRFunction', {
code: DockerImageCode.fromEcrImage(repo),
});
```

### Execution Role

Lambda functions assume an IAM role during execution. In CDK by default, Lambda
Expand Down
174 changes: 171 additions & 3 deletions packages/@aws-cdk/aws-lambda/lib/code.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import * as ecr from '@aws-cdk/aws-ecr';
import * as ecr_assets from '@aws-cdk/aws-ecr-assets';
import * as iam from '@aws-cdk/aws-iam';
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';

/**
* Represents the Lambda Handler Code.
*/
export abstract class Code {
/**
* @returns `LambdaS3Code` associated with the specified S3 object.
* Lambda handler code as an S3 object.
* @param bucket The S3 bucket
* @param key The object key
* @param objectVersion Optional S3 object version
Expand All @@ -14,13 +20,15 @@ export abstract class Code {
}

/**
* DEPRECATED
* @deprecated use `fromBucket`
*/
public static bucket(bucket: s3.IBucket, key: string, objectVersion?: string): S3Code {
return this.fromBucket(bucket, key, objectVersion);
}

/**
* Inline code for Lambda handler
* @returns `LambdaInlineCode` with inline code.
* @param code The actual handler code (limited to 4KiB)
*/
Expand All @@ -29,6 +37,7 @@ export abstract class Code {
}

/**
* DEPRECATED
* @deprecated use `fromInline`
*/
public static inline(code: string): InlineCode {
Expand All @@ -45,6 +54,7 @@ export abstract class Code {
}

/**
* DEPRECATED
* @deprecated use `fromAsset`
*/
public static asset(path: string): AssetCode {
Expand All @@ -62,12 +72,31 @@ export abstract class Code {
}

/**
* DEPRECATED
* @deprecated use `fromCfnParameters`
*/
public static cfnParameters(props?: CfnParametersCodeProps): CfnParametersCode {
return this.fromCfnParameters(props);
}

/**
* Use an existing ECR image as the Lambda code.
* @param repository the ECR repository that the image is in
* @param props properties to further configure the selected image
*/
public static fromEcrImage(repository: ecr.IRepository, props?: EcrImageCodeProps) {
return new EcrImageCode(repository, props);
}

/**
* Create an ECR image from the specified asset and bind it as the Lambda code.
* @param directory the directory from which the asset must be created
* @param props properties to further configure the selected image
*/
public static fromAssetImage(directory: string, props: AssetImageCodeProps = {}) {
return new AssetImageCode(directory, props);
}

/**
* Determines whether this Code is inline code or not.
*
Expand Down Expand Up @@ -95,16 +124,54 @@ export abstract class Code {
}
}

/**
* Result of binding `Code` into a `Function`.
*/
export interface CodeConfig {
/**
* The location of the code in S3 (mutually exclusive with `inlineCode`).
* The location of the code in S3 (mutually exclusive with `inlineCode` and `image`).
* @default - code is not an s3 location
*/
readonly s3Location?: s3.Location;

/**
* Inline code (mutually exclusive with `s3Location`).
* Inline code (mutually exclusive with `s3Location` and `image`).
* @default - code is not inline code
*/
readonly inlineCode?: string;

/**
* Docker image configuration (mutually exclusive with `s3Location` and `inlineCode`).
* @default - code is not an ECR container image
*/
readonly image?: CodeImageConfig;
}

/**
* Result of the bind when an ECR image is used.
*/
export interface CodeImageConfig {
/**
* URI to the Docker image.
*/
readonly imageUri: string;

/**
* Specify or override the CMD on the specified Docker image or Dockerfile.
* This needs to be in the 'exec form', viz., `[ 'executable', 'param1', 'param2' ]`.
* @see https://docs.docker.com/engine/reference/builder/#cmd
* @default - use the CMD specified in the docker image or Dockerfile.
*/
readonly cmd?: string[];

/**
* Specify or override the ENTRYPOINT on the specified Docker image or Dockerfile.
* An ENTRYPOINT allows you to configure a container that will run as an executable.
* This needs to be in the 'exec form', viz., `[ 'executable', 'param1', 'param2' ]`.
* @see https://docs.docker.com/engine/reference/builder/#entrypoint
* @default - use the ENTRYPOINT in the docker image or Dockerfile.
*/
readonly entrypoint?: string[];
}

/**
Expand Down Expand Up @@ -313,3 +380,104 @@ export class CfnParametersCode extends Code {
}
}
}

/**
* Properties to initialize a new EcrImageCode
*/
export interface EcrImageCodeProps {
/**
* Specify or override the CMD on the specified Docker image or Dockerfile.
* This needs to be in the 'exec form', viz., `[ 'executable', 'param1', 'param2' ]`.
* @see https://docs.docker.com/engine/reference/builder/#cmd
* @default - use the CMD specified in the docker image or Dockerfile.
*/
readonly cmd?: string[];

/**
* Specify or override the ENTRYPOINT on the specified Docker image or Dockerfile.
* An ENTRYPOINT allows you to configure a container that will run as an executable.
* This needs to be in the 'exec form', viz., `[ 'executable', 'param1', 'param2' ]`.
* @see https://docs.docker.com/engine/reference/builder/#entrypoint
* @default - use the ENTRYPOINT in the docker image or Dockerfile.
*/
readonly entrypoint?: string[];

/**
* The image tag to use when pulling the image from ECR.
* @default 'latest'
*/
readonly tag?: string;
}

/**
* Represents a Docker image in ECR that can be bound as Lambda Code.
*/
export class EcrImageCode extends Code {
public readonly isInline: boolean = false;

constructor(private readonly repository: ecr.IRepository, private readonly props: EcrImageCodeProps = {}) {
super();
}

public bind(_: cdk.Construct): CodeConfig {
this.repository.grantPull(new iam.ServicePrincipal('lambda.amazonaws.com'));

return {
image: {
imageUri: this.repository.repositoryUriForTag(this.props?.tag ?? 'latest'),
cmd: this.props.cmd,
entrypoint: this.props.entrypoint,
},
};
}
}

/**
* Properties to initialize a new AssetImage
*/
export interface AssetImageCodeProps extends ecr_assets.DockerImageAssetOptions {
/**
* Specify or override the CMD on the specified Docker image or Dockerfile.
* This needs to be in the 'exec form', viz., `[ 'executable', 'param1', 'param2' ]`.
* @see https://docs.docker.com/engine/reference/builder/#cmd
* @default - use the CMD specified in the docker image or Dockerfile.
*/
readonly cmd?: string[];

/**
* Specify or override the ENTRYPOINT on the specified Docker image or Dockerfile.
* An ENTRYPOINT allows you to configure a container that will run as an executable.
* This needs to be in the 'exec form', viz., `[ 'executable', 'param1', 'param2' ]`.
* @see https://docs.docker.com/engine/reference/builder/#entrypoint
* @default - use the ENTRYPOINT in the docker image or Dockerfile.
*/
readonly entrypoint?: string[];
}

/**
* Represents an ECR image that will be constructed from the specified asset and can be bound as Lambda code.
*/
export class AssetImageCode extends Code {
public readonly isInline: boolean = false;

constructor(private readonly directory: string, private readonly props: AssetImageCodeProps) {
super();
}

public bind(scope: cdk.Construct): CodeConfig {
const asset = new ecr_assets.DockerImageAsset(scope, 'AssetImage', {
directory: this.directory,
...this.props,
});

asset.repository.grantPull(new iam.ServicePrincipal('lambda.amazonaws.com'));

return {
image: {
imageUri: asset.imageUri,
entrypoint: this.props.entrypoint,
cmd: this.props.cmd,
},
};
}
}
53 changes: 45 additions & 8 deletions packages/@aws-cdk/aws-lambda/lib/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { IEventSource } from './event-source';
import { FileSystem } from './filesystem';
import { FunctionAttributes, FunctionBase, IFunction } from './function-base';
import { calculateFunctionHash, trimFromStart } from './function-hash';
import { Handler } from './handler';
import { Version, VersionOptions } from './lambda-version';
import { CfnFunction } from './lambda.generated';
import { ILayerVersion } from './layers';
Expand Down Expand Up @@ -288,6 +289,8 @@ export interface FunctionProps extends FunctionOptions {
* The runtime environment for the Lambda function that you are uploading.
* For valid values, see the Runtime property in the AWS Lambda Developer
* Guide.
*
* Use `Runtime.FROM_IMAGE` when when defining a function from a Docker image.
*/
readonly runtime: Runtime;

Expand All @@ -304,6 +307,8 @@ export interface FunctionProps extends FunctionOptions {
* namespaces and other qualifiers, depending on the runtime.
* For more information, see https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-features.html#gettingstarted-features-programmingmodel.
*
* Use `Handler.FROM_IMAGE` when defining a function from a Docker image.
*
* NOTE: If you specify your source code as inline text by specifying the
* ZipFile property within the Code property, specify index.function_name as
* the handler.
Expand Down Expand Up @@ -557,7 +562,7 @@ export class Function extends FunctionBase {
}

const code = props.code.bind(this);
verifyCodeConfig(code, props.runtime);
verifyCodeConfig(code, props);

let profilingGroupEnvironmentVariables: { [key: string]: string } = {};
if (props.profilingGroup && props.profiling !== false) {
Expand Down Expand Up @@ -590,6 +595,8 @@ export class Function extends FunctionBase {

this.deadLetterQueue = this.buildDeadLetterQueue(props);

const UNDEFINED_MARKER = '$$$undefined';

const resource: CfnFunction = new CfnFunction(this, 'Resource', {
functionName: this.physicalName,
description: props.description,
Expand All @@ -598,11 +605,13 @@ export class Function extends FunctionBase {
s3Key: code.s3Location && code.s3Location.objectKey,
s3ObjectVersion: code.s3Location && code.s3Location.objectVersion,
zipFile: code.inlineCode,
imageUri: code.image?.imageUri,
},
layers: Lazy.list({ produce: () => this.layers.map(layer => layer.layerVersionArn) }, { omitEmpty: true }),
handler: props.handler,
handler: props.handler === Handler.FROM_IMAGE ? UNDEFINED_MARKER : props.handler,
timeout: props.timeout && props.timeout.toSeconds(),
runtime: props.runtime.name,
packageType: props.runtime === Runtime.FROM_IMAGE ? 'Image' : undefined,
runtime: props.runtime === Runtime.FROM_IMAGE ? UNDEFINED_MARKER : props.runtime?.name,
role: this.role.roleArn,
// Uncached because calling '_checkEdgeCompatibility', which gets called in the resolve of another
// Token, actually *modifies* the 'environment' map.
Expand All @@ -612,8 +621,21 @@ export class Function extends FunctionBase {
deadLetterConfig: this.buildDeadLetterConfig(this.deadLetterQueue),
tracingConfig: this.buildTracingConfig(props),
reservedConcurrentExecutions: props.reservedConcurrentExecutions,
imageConfig: undefinedIfNoKeys({
command: code.image?.cmd,
entryPoint: code.image?.entrypoint,
}),
});

// since patching the CFN spec to make Runtime and Handler optional causes a
// change in the order of the JSON keys, which results in a change of
// function hash (and invalidation of all lambda functions everywhere), we
// are using a marker to indicate this fields needs to be erased using an
// escape hatch. this should be fixed once the new spec is published and a
// patch is no longer needed.
if (resource.runtime === UNDEFINED_MARKER) { resource.addPropertyOverride('Runtime', undefined); }
if (resource.handler === UNDEFINED_MARKER) { resource.addPropertyOverride('Handler', undefined); }

resource.node.addDependency(this.role);

this.functionName = this.getResourceNameAttribute(resource.ref);
Expand Down Expand Up @@ -966,14 +988,29 @@ function extractNameFromArn(arn: string) {
return Fn.select(6, Fn.split(':', arn));
}

export function verifyCodeConfig(code: CodeConfig, runtime: Runtime) {
export function verifyCodeConfig(code: CodeConfig, props: FunctionProps) {
// mutually exclusive
if ((!code.inlineCode && !code.s3Location) || (code.inlineCode && code.s3Location)) {
throw new Error('lambda.Code must specify one of "inlineCode" or "s3Location" but not both');
const codeType = [code.inlineCode, code.s3Location, code.image];

if (codeType.filter(x => !!x).length !== 1) {
throw new Error('lambda.Code must specify exactly one of: "inlineCode", "s3Location", or "image"');
}

if (!!code.image === (props.handler !== Handler.FROM_IMAGE)) {
throw new Error('handler must be `Handler.FROM_IMAGE` when using image asset for Lambda function');
}

if (!!code.image === (props.runtime !== Runtime.FROM_IMAGE)) {
throw new Error('runtime must be `Runtime.FROM_IMAGE` when using image asset for Lambda function');
}

// if this is inline code, check that the runtime supports
if (code.inlineCode && !runtime.supportsInlineCode) {
throw new Error(`Inline source not allowed for ${runtime.name}`);
if (code.inlineCode && !props.runtime.supportsInlineCode) {
throw new Error(`Inline source not allowed for ${props.runtime!.name}`);
}
}

function undefinedIfNoKeys<A>(struct: A): A | undefined {
const allUndefined = Object.values(struct).every(val => val === undefined);
return allUndefined ? undefined : struct;
}
Loading

0 comments on commit 02ced10

Please sign in to comment.