Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lambda): container images #11809

Merged
merged 19 commits into from
Dec 1, 2020
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