packages/aws-cdk-lib/aws-lambda/lib/code.ts (319 lines of code) (raw):
import { spawnSync } from 'child_process';
import { Construct } from 'constructs';
import * as ecr from '../../aws-ecr';
import * as ecr_assets from '../../aws-ecr-assets';
import * as iam from '../../aws-iam';
import { IKey } from '../../aws-kms';
import * as s3 from '../../aws-s3';
import * as s3_assets from '../../aws-s3-assets';
import * as cdk from '../../core';
import { UnscopedValidationError, ValidationError } from '../../core/lib/errors';
/**
* Represents the Lambda Handler Code.
*/
export abstract class Code {
/**
* Lambda handler code as an S3 object.
* @param bucket The S3 bucket
* @param key The object key
* @param objectVersion Optional S3 object version
*/
public static fromBucket(bucket: s3.IBucket, key: string, objectVersion?: string): S3Code {
return new S3Code(bucket, key, objectVersion);
}
/**
* Lambda handler code as an S3 object.
* @param bucket The S3 bucket
* @param key The object key
* @param options Optional parameters for setting the code, current optional parameters to set here are
* 1. `objectVersion` to set S3 object version
* 2. `sourceKMSKey` to set KMS Key for encryption of code
*/
public static fromBucketV2 (bucket: s3.IBucket, key: string, options?: BucketOptions): S3CodeV2 {
return new S3CodeV2(bucket, key, options);
}
/**
* 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 (the resulting zip file cannot exceed 4MB)
*/
public static fromInline(code: string): InlineCode {
return new InlineCode(code);
}
/**
* DEPRECATED
* @deprecated use `fromInline`
*/
public static inline(code: string): InlineCode {
return this.fromInline(code);
}
/**
* Loads the function code from a local disk path.
*
* @param path Either a directory with the Lambda code bundle or a .zip file
*/
public static fromAsset(path: string, options?: s3_assets.AssetOptions): AssetCode {
return new AssetCode(path, options);
}
/**
* Runs a command to build the code asset that will be used.
*
* @param output Where the output of the command will be directed, either a directory or a .zip file with the output Lambda code bundle
* * For example, if you use the command to run a build script (e.g., [ 'node', 'bundle_code.js' ]), and the build script generates a directory `/my/lambda/code`
* containing code that should be ran in a Lambda function, then output should be set to `/my/lambda/code`
* @param command The command which will be executed to generate the output, for example, [ 'node', 'bundle_code.js' ]
* @param options options for the custom command, and other asset options -- but bundling options are not allowed.
*/
public static fromCustomCommand(
output: string,
command: string[],
options?: CustomCommandOptions,
): AssetCode {
if (command.length === 0) {
throw new UnscopedValidationError('command must contain at least one argument. For example, ["node", "buildFile.js"].');
}
const cmd = command[0];
const commandArguments = command.splice(1);
const proc = options?.commandOptions === undefined
? spawnSync(cmd, commandArguments) // use the default spawnSyncOptions
: spawnSync(cmd, commandArguments, options.commandOptions);
if (proc.error) {
throw new UnscopedValidationError(`Failed to execute custom command: ${proc.error}`);
}
if (proc.status !== 0) {
throw new UnscopedValidationError(`${command.join(' ')} exited with status: ${proc.status}\n\nstdout: ${proc.stdout?.toString().trim()}\n\nstderr: ${proc.stderr?.toString().trim()}`);
}
return new AssetCode(output, options);
}
/**
* Loads the function code from an asset created by a Docker build.
*
* By default, 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 {
let imagePath = options.imagePath ?? '/asset/.';
// ensure imagePath ends with /. to copy the **content** at this path
if (imagePath.endsWith('/')) {
imagePath = `${imagePath}.`;
} else if (!imagePath.endsWith('/.')) {
imagePath = `${imagePath}/.`;
}
const assetPath = cdk.DockerImage
.fromBuild(path, options)
.cp(imagePath, options.outputPath);
return new AssetCode(assetPath);
}
/**
* DEPRECATED
* @deprecated use `fromAsset`
*/
public static asset(path: string): AssetCode {
return this.fromAsset(path);
}
/**
* Creates a new Lambda source defined using CloudFormation parameters.
*
* @returns a new instance of `CfnParametersCode`
* @param props optional construction properties of `CfnParametersCode`
*/
public static fromCfnParameters(props?: CfnParametersCodeProps): CfnParametersCode {
return new CfnParametersCode(props);
}
/**
* 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.
*
* @deprecated this value is ignored since inline is now determined based on the
* the `inlineCode` field of `CodeConfig` returned from `bind()`.
*/
public abstract readonly isInline: boolean;
/**
* Called when the lambda or layer is initialized to allow this object to bind
* to the stack, add resources and have fun.
*
* @param scope The binding scope. Don't be smart about trying to down-cast or
* assume it's initialized. You may just use it as a construct scope.
*/
public abstract bind(scope: Construct): CodeConfig;
/**
* Called after the CFN function resource has been created to allow the code
* class to bind to it. Specifically it's required to allow assets to add
* metadata for tooling like SAM CLI to be able to find their origins.
*/
public bindToResource(_resource: cdk.CfnResource, _options?: ResourceBindOptions) {
return;
}
}
/**
* Result of binding `Code` into a `Function`.
*/
export interface CodeConfig {
/**
* 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` 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;
/**
* The ARN of the KMS key used to encrypt the handler code.
* @default - the default server-side encryption with Amazon S3 managed keys(SSE-S3) key will be used.
*/
readonly sourceKMSKeyArn?: string;
}
/**
* 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[];
/**
* Specify or override the WORKDIR on the specified Docker image or Dockerfile.
* A WORKDIR allows you to configure the working directory the container will use.
* @see https://docs.docker.com/engine/reference/builder/#workdir
* @default - use the WORKDIR in the docker image or Dockerfile.
*/
readonly workingDirectory?: string;
}
/**
* Lambda code from an S3 archive.
*/
export class S3Code extends Code {
public readonly isInline = false;
private bucketName: string;
constructor(bucket: s3.IBucket, private key: string, private objectVersion?: string) {
super();
if (!bucket.bucketName) {
throw new ValidationError('bucketName is undefined for the provided bucket', bucket);
}
this.bucketName = bucket.bucketName;
}
public bind(_scope: Construct): CodeConfig {
return {
s3Location: {
bucketName: this.bucketName,
objectKey: this.key,
objectVersion: this.objectVersion,
},
};
}
}
/**
* Lambda code from an S3 archive. With option to set KMSKey for encryption.
*/
export class S3CodeV2 extends Code {
public readonly isInline = false;
private bucketName: string;
constructor(bucket: s3.IBucket, private key: string, private options?: BucketOptions) {
super();
if (!bucket.bucketName) {
throw new ValidationError('bucketName is undefined for the provided bucket', bucket);
}
this.bucketName = bucket.bucketName;
}
public bind(_scope: Construct): CodeConfig {
return {
s3Location: {
bucketName: this.bucketName,
objectKey: this.key,
objectVersion: this.options?.objectVersion,
},
sourceKMSKeyArn: this.options?.sourceKMSKey?.keyArn,
};
}
}
/**
* Lambda code from an inline string.
*/
export class InlineCode extends Code {
public readonly isInline = true;
constructor(private code: string) {
super();
if (code.length === 0) {
throw new UnscopedValidationError('Lambda inline code cannot be empty');
}
}
public bind(_scope: Construct): CodeConfig {
return {
inlineCode: this.code,
};
}
}
/**
* Lambda code from a local directory.
*/
export class AssetCode extends Code {
public readonly isInline = false;
private asset?: s3_assets.Asset;
/**
* @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: 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,
deployTime: true,
...this.options,
});
} else if (cdk.Stack.of(this.asset) !== cdk.Stack.of(scope)) {
throw new ValidationError(`Asset is already associated with another stack '${cdk.Stack.of(this.asset).stackName}'. ` +
'Create a new Code instance for every stack.', scope);
}
if (!this.asset.isZipArchive) {
throw new ValidationError(`Asset must be a .zip file or a directory (${this.path})`, scope);
}
return {
s3Location: {
bucketName: this.asset.s3BucketName,
objectKey: this.asset.s3ObjectKey,
},
sourceKMSKeyArn: this.options.sourceKMSKey?.keyArn,
};
}
public bindToResource(resource: cdk.CfnResource, options: ResourceBindOptions = { }) {
if (!this.asset) {
throw new ValidationError('bindToResource() must be called after bind()', resource);
}
const resourceProperty = options.resourceProperty || 'Code';
// https://github.com/aws/aws-cdk/issues/1432
this.asset.addResourceMetadata(resource, resourceProperty);
}
}
export interface ResourceBindOptions {
/**
* The name of the CloudFormation property to annotate with asset metadata.
* @see https://github.com/aws/aws-cdk/issues/1432
* @default Code
*/
readonly resourceProperty?: string;
}
/**
* Construction properties for `CfnParametersCode`.
*/
export interface CfnParametersCodeProps {
/**
* The CloudFormation parameter that represents the name of the S3 Bucket
* where the Lambda code will be located in.
* Must be of type 'String'.
*
* @default a new parameter will be created
*/
readonly bucketNameParam?: cdk.CfnParameter;
/**
* The CloudFormation parameter that represents the path inside the S3 Bucket
* where the Lambda code will be located at.
* Must be of type 'String'.
*
* @default a new parameter will be created
*/
readonly objectKeyParam?: cdk.CfnParameter;
/**
* The ARN of the KMS key used to encrypt the handler code.
* @default - the default server-side encryption with Amazon S3 managed keys(SSE-S3) key will be used.
*/
readonly sourceKMSKey?: IKey;
}
/**
* Lambda code defined using 2 CloudFormation parameters.
* Useful when you don't have access to the code of your Lambda from your CDK code, so you can't use Assets,
* and you want to deploy the Lambda in a CodePipeline, using CloudFormation Actions -
* you can fill the parameters using the `#assign` method.
*/
export class CfnParametersCode extends Code {
public readonly isInline = false;
private _bucketNameParam?: cdk.CfnParameter;
private _objectKeyParam?: cdk.CfnParameter;
private _sourceKMSKey?: IKey;
constructor(props: CfnParametersCodeProps = {}) {
super();
this._bucketNameParam = props.bucketNameParam;
this._objectKeyParam = props.objectKeyParam;
this._sourceKMSKey = props.sourceKMSKey;
}
public bind(scope: Construct): CodeConfig {
if (!this._bucketNameParam) {
this._bucketNameParam = new cdk.CfnParameter(scope, 'LambdaSourceBucketNameParameter', {
type: 'String',
});
}
if (!this._objectKeyParam) {
this._objectKeyParam = new cdk.CfnParameter(scope, 'LambdaSourceObjectKeyParameter', {
type: 'String',
});
}
return {
s3Location: {
bucketName: this._bucketNameParam.valueAsString,
objectKey: this._objectKeyParam.valueAsString,
},
sourceKMSKeyArn: this._sourceKMSKey?.keyArn,
};
}
/**
* Create a parameters map from this instance's CloudFormation parameters.
*
* It returns a map with 2 keys that correspond to the names of the parameters defined in this Lambda code,
* and as values it contains the appropriate expressions pointing at the provided S3 location
* (most likely, obtained from a CodePipeline Artifact by calling the `artifact.s3Location` method).
* The result should be provided to the CloudFormation Action
* that is deploying the Stack that the Lambda with this code is part of,
* in the `parameterOverrides` property.
*
* @param location the location of the object in S3 that represents the Lambda code
*/
public assign(location: s3.Location): { [name: string]: any } {
const ret: { [name: string]: any } = {};
ret[this.bucketNameParam] = location.bucketName;
ret[this.objectKeyParam] = location.objectKey;
return ret;
}
public get bucketNameParam(): string {
if (this._bucketNameParam) {
return this._bucketNameParam.logicalId;
} else {
throw new UnscopedValidationError('Pass CfnParametersCode to a Lambda Function before accessing the bucketNameParam property');
}
}
public get objectKeyParam(): string {
if (this._objectKeyParam) {
return this._objectKeyParam.logicalId;
} else {
throw new UnscopedValidationError('Pass CfnParametersCode to a Lambda Function before accessing the objectKeyParam property');
}
}
}
/**
* 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[];
/**
* Specify or override the WORKDIR on the specified Docker image or Dockerfile.
* A WORKDIR allows you to configure the working directory the container will use.
* @see https://docs.docker.com/engine/reference/builder/#workdir
* @default - use the WORKDIR in the docker image or Dockerfile.
*/
readonly workingDirectory?: string;
/**
* The image tag to use when pulling the image from ECR.
* @default 'latest'
* @deprecated use `tagOrDigest`
*/
readonly tag?: string;
/**
* The image tag or digest to use when pulling the image from ECR (digests must start with `sha256:`).
* @default 'latest'
*/
readonly tagOrDigest?: 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(_scope: Construct): CodeConfig {
this.repository.grantPull(new iam.ServicePrincipal('lambda.amazonaws.com'));
return {
image: {
imageUri: this.repository.repositoryUriForTagOrDigest(this.props?.tagOrDigest ?? this.props?.tag ?? 'latest'),
cmd: this.props.cmd,
entrypoint: this.props.entrypoint,
workingDirectory: this.props.workingDirectory,
},
};
}
}
/**
* 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[];
/**
* Specify or override the WORKDIR on the specified Docker image or Dockerfile.
* A WORKDIR allows you to configure the working directory the container will use.
* @see https://docs.docker.com/engine/reference/builder/#workdir
* @default - use the WORKDIR in the docker image or Dockerfile.
*/
readonly workingDirectory?: 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;
private asset?: ecr_assets.DockerImageAsset;
constructor(private readonly directory: string, private readonly props: AssetImageCodeProps) {
super();
}
public bind(scope: Construct): CodeConfig {
// If the same AssetImageCode is used multiple times, retain only the first instantiation.
if (!this.asset) {
this.asset = new ecr_assets.DockerImageAsset(scope, 'AssetImage', {
directory: this.directory,
...this.props,
});
this.asset.repository.grantPull(new iam.ServicePrincipal('lambda.amazonaws.com'));
} else if (cdk.Stack.of(this.asset) !== cdk.Stack.of(scope)) {
throw new ValidationError(`Asset is already associated with another stack '${cdk.Stack.of(this.asset).stackName}'. ` +
'Create a new Code instance for every stack.', scope);
}
return {
image: {
imageUri: this.asset.imageUri,
entrypoint: this.props.entrypoint,
cmd: this.props.cmd,
workingDirectory: this.props.workingDirectory,
},
};
}
public bindToResource(resource: cdk.CfnResource, options: ResourceBindOptions = { }) {
if (!this.asset) {
throw new ValidationError('bindToResource() must be called after bind()', resource);
}
const resourceProperty = options.resourceProperty || 'Code.ImageUri';
// https://github.com/aws/aws-cdk/issues/14593
this.asset.addResourceMetadata(resource, resourceProperty);
}
}
/**
* 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;
}
/**
* Options for creating `AssetCode` with a custom command, such as running a buildfile.
*/
export interface CustomCommandOptions extends s3_assets.AssetOptions {
/**
* options that are passed to the spawned process, which determine the characteristics of the spawned process.
*
* @default: see `child_process.SpawnSyncOptions` (https://nodejs.org/api/child_process.html#child_processspawnsynccommand-args-options).
*/
readonly commandOptions?: { [options: string]: any };
}
/**
* Optional parameters for creating code using bucket
*/
export interface BucketOptions {
/**
* Optional S3 object version
*/
readonly objectVersion?: string;
/**
* The ARN of the KMS key used to encrypt the handler code.
* @default - the default server-side encryption with Amazon S3 managed keys(SSE-S3) key will be used.
*/
readonly sourceKMSKey?: IKey;
}