packages/aws-cdk-lib/aws-lambda/lib/function.ts (838 lines of code) (raw):

import { Construct, IConstruct } from 'constructs'; import { AdotInstrumentationConfig, AdotLambdaExecWrapper } from './adot-layers'; import { AliasOptions, Alias } from './alias'; import { Architecture } from './architecture'; import { Code, CodeConfig } from './code'; import { ICodeSigningConfig } from './code-signing-config'; import { EventInvokeConfigOptions } from './event-invoke-config'; 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 { LambdaInsightsVersion } from './lambda-insights'; import { Version, VersionOptions } from './lambda-version'; import { CfnFunction } from './lambda.generated'; import { LayerVersion, ILayerVersion } from './layers'; import { LogRetentionRetryOptions } from './log-retention'; import { ParamsAndSecretsLayerVersion } from './params-and-secrets-layers'; import { Runtime, RuntimeFamily } from './runtime'; import { RuntimeManagementMode } from './runtime-management'; import { SnapStartConf } from './snapstart-config'; import { addAlias } from './util'; import * as cloudwatch from '../../aws-cloudwatch'; import { IProfilingGroup, ProfilingGroup, ComputePlatform } from '../../aws-codeguruprofiler'; import * as ec2 from '../../aws-ec2'; import * as efs from '../../aws-efs'; import * as iam from '../../aws-iam'; import * as kms from '../../aws-kms'; import * as logs from '../../aws-logs'; import * as sns from '../../aws-sns'; import * as sqs from '../../aws-sqs'; import { Annotations, ArnFormat, CfnResource, Duration, FeatureFlags, Fn, IAspect, Lazy, Names, Size, Stack, Token } from '../../core'; import { UnscopedValidationError, ValidationError } from '../../core/lib/errors'; import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource'; import { LAMBDA_RECOGNIZE_LAYER_VERSION } from '../../cx-api'; /** * X-Ray Tracing Modes (https://docs.aws.amazon.com/lambda/latest/dg/API_TracingConfig.html) */ export enum Tracing { /** * Lambda will respect any tracing header it receives from an upstream service. * If no tracing header is received, Lambda will sample the request based on a fixed rate. Please see the [Using AWS Lambda with AWS X-Ray](https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html) documentation for details on this sampling behavior. */ ACTIVE = 'Active', /** * Lambda will only trace the request from an upstream service * if it contains a tracing header with "sampled=1" */ PASS_THROUGH = 'PassThrough', /** * Lambda will not trace any request. */ DISABLED = 'Disabled', } /** * Lambda service will automatically captures system logs about function invocation * generated by the Lambda service (known as system logs) and sends these logs to a * default CloudWatch log group named after the Lambda function. */ export enum SystemLogLevel { /** * Lambda will capture only logs at info level. */ INFO = 'INFO', /** * Lambda will capture only logs at debug level. */ DEBUG = 'DEBUG', /** * Lambda will capture only logs at warn level. */ WARN = 'WARN', } /** * Lambda service automatically captures logs generated by the function code * (known as application logs) and sends these logs to a default CloudWatch * log group named after the Lambda function. */ export enum ApplicationLogLevel { /** * Lambda will capture only logs at info level. */ INFO = 'INFO', /** * Lambda will capture only logs at debug level. */ DEBUG = 'DEBUG', /** * Lambda will capture only logs at warn level. */ WARN = 'WARN', /** * Lambda will capture only logs at trace level. */ TRACE = 'TRACE', /** * Lambda will capture only logs at error level. */ ERROR = 'ERROR', /** * Lambda will capture only logs at fatal level. */ FATAL = 'FATAL', } /** * This field takes in 2 values either Text or JSON. By setting this value to Text, * will result in the current structure of logs format, whereas, by setting this value to JSON, * Lambda will print the logs as Structured JSON Logs, with the corresponding timestamp and log level * of each event. Selecting ‘JSON’ format will only allow customers to have different log level * Application log level and the System log level. */ export enum LogFormat { /** * Lambda Logs text format. */ TEXT = 'Text', /** * Lambda structured logging in Json format. */ JSON = 'JSON', } /** * This field takes in 2 values either Text or JSON. By setting this value to Text, * will result in the current structure of logs format, whereas, by setting this value to JSON, * Lambda will print the logs as Structured JSON Logs, with the corresponding timestamp and log level * of each event. Selecting ‘JSON’ format will only allow customers to have different log level * Application log level and the System log level. */ export enum LoggingFormat { /** * Lambda Logs text format. */ TEXT = 'Text', /** * Lambda structured logging in Json format. */ JSON = 'JSON', } export enum RecursiveLoop { /** * Allows the recursive loop to happen and does not terminate it. */ ALLOW = 'Allow', /** * Terminates the recursive loop. */ TERMINATE = 'Terminate', } /** * Non runtime options */ export interface FunctionOptions extends EventInvokeConfigOptions { /** * A description of the function. * * @default - No description. */ readonly description?: string; /** * The function execution time (in seconds) after which Lambda terminates * the function. Because the execution time affects cost, set this value * based on the function's expected execution time. * * @default Duration.seconds(3) */ readonly timeout?: Duration; /** * Key-value pairs that Lambda caches and makes available for your Lambda * functions. Use environment variables to apply configuration changes, such * as test and production environment configurations, without changing your * Lambda function source code. * * @default - No environment variables. */ readonly environment?: { [key: string]: string }; /** * A name for the function. * * @default - AWS CloudFormation generates a unique physical ID and uses that * ID for the function's name. For more information, see Name Type. */ readonly functionName?: string; /** * The amount of memory, in MB, that is allocated to your Lambda function. * Lambda uses this value to proportionally allocate the amount of CPU * power. For more information, see Resource Model in the AWS Lambda * Developer Guide. * * @default 128 */ readonly memorySize?: number; /** * The size of the function’s /tmp directory in MiB. * * @default 512 MiB */ readonly ephemeralStorageSize?: Size; /** * Initial policy statements to add to the created Lambda Role. * * You can call `addToRolePolicy` to the created lambda to add statements post creation. * * @default - No policy statements are added to the created Lambda role. */ readonly initialPolicy?: iam.PolicyStatement[]; /** * Lambda execution role. * * This is the role that will be assumed by the function upon execution. * It controls the permissions that the function will have. The Role must * be assumable by the 'lambda.amazonaws.com' service principal. * * The default Role automatically has permissions granted for Lambda execution. If you * provide a Role, you must add the relevant AWS managed policies yourself. * * The relevant managed policies are "service-role/AWSLambdaBasicExecutionRole" and * "service-role/AWSLambdaVPCAccessExecutionRole". * * @default - A unique role will be generated for this lambda function. * Both supplied and generated roles can always be changed by calling `addToRolePolicy`. */ readonly role?: iam.IRole; /** * VPC network to place Lambda network interfaces * * Specify this if the Lambda function needs to access resources in a VPC. * This is required when `vpcSubnets` is specified. * * @default - Function is not placed within a VPC. */ readonly vpc?: ec2.IVpc; /** * Allows outbound IPv6 traffic on VPC functions that are connected to dual-stack subnets. * * Only used if 'vpc' is supplied. * * @default false */ readonly ipv6AllowedForDualStack?: boolean; /** * Where to place the network interfaces within the VPC. * * This requires `vpc` to be specified in order for interfaces to actually be * placed in the subnets. If `vpc` is not specify, this will raise an error. * * Note: Internet access for Lambda Functions requires a NAT Gateway, so picking * public subnets is not allowed (unless `allowPublicSubnet` is set to `true`). * * @default - the Vpc default strategy if not specified */ readonly vpcSubnets?: ec2.SubnetSelection; /** * What security group to associate with the Lambda's network interfaces. * This property is being deprecated, consider using securityGroups instead. * * Only used if 'vpc' is supplied. * * Use securityGroups property instead. * Function constructor will throw an error if both are specified. * * @default - If the function is placed within a VPC and a security group is * not specified, either by this or securityGroups prop, a dedicated security * group will be created for this function. * * @deprecated - This property is deprecated, use securityGroups instead */ readonly securityGroup?: ec2.ISecurityGroup; /** * The list of security groups to associate with the Lambda's network interfaces. * * Only used if 'vpc' is supplied. * * @default - If the function is placed within a VPC and a security group is * not specified, either by this or securityGroup prop, a dedicated security * group will be created for this function. */ readonly securityGroups?: ec2.ISecurityGroup[]; /** * Whether to allow the Lambda to send all network traffic (except ipv6) * * If set to false, you must individually add traffic rules to allow the * Lambda to connect to network targets. * * Do not specify this property if the `securityGroups` or `securityGroup` property is set. * Instead, configure `allowAllOutbound` directly on the security group. * * @default true */ readonly allowAllOutbound?: boolean; /** * Whether to allow the Lambda to send all ipv6 network traffic * * If set to true, there will only be a single egress rule which allows all * outbound ipv6 traffic. If set to false, you must individually add traffic rules to allow the * Lambda to connect to network targets using ipv6. * * Do not specify this property if the `securityGroups` or `securityGroup` property is set. * Instead, configure `allowAllIpv6Outbound` directly on the security group. * * @default false */ readonly allowAllIpv6Outbound?: boolean; /** * Enabled DLQ. If `deadLetterQueue` is undefined, * an SQS queue with default options will be defined for your Function. * * @default - false unless `deadLetterQueue` is set, which implies DLQ is enabled. */ readonly deadLetterQueueEnabled?: boolean; /** * The SQS queue to use if DLQ is enabled. * If SNS topic is desired, specify `deadLetterTopic` property instead. * * @default - SQS queue with 14 day retention period if `deadLetterQueueEnabled` is `true` */ readonly deadLetterQueue?: sqs.IQueue; /** * The SNS topic to use as a DLQ. * Note that if `deadLetterQueueEnabled` is set to `true`, an SQS queue will be created * rather than an SNS topic. Using an SNS topic as a DLQ requires this property to be set explicitly. * * @default - no SNS topic */ readonly deadLetterTopic?: sns.ITopic; /** * Enable AWS X-Ray Tracing for Lambda Function. * * @default Tracing.Disabled */ readonly tracing?: Tracing; /** * Enable SnapStart for Lambda Function. * SnapStart is currently supported for Java 11, Java 17, Python 3.12, Python 3.13, and .NET 8 runtime * * @default - No snapstart */ readonly snapStart?: SnapStartConf; /** * Enable profiling. * @see https://docs.aws.amazon.com/codeguru/latest/profiler-ug/setting-up-lambda.html * * @default - No profiling. */ readonly profiling?: boolean; /** * Profiling Group. * @see https://docs.aws.amazon.com/codeguru/latest/profiler-ug/setting-up-lambda.html * * @default - A new profiling group will be created if `profiling` is set. */ readonly profilingGroup?: IProfilingGroup; /** * Specify the version of CloudWatch Lambda insights to use for monitoring * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights.html * * When used with `DockerImageFunction` or `DockerImageCode`, the Docker image should have * the Lambda insights agent installed. * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-Getting-Started-docker.html * * @default - No Lambda Insights */ readonly insightsVersion?: LambdaInsightsVersion; /** * Specify the configuration of AWS Distro for OpenTelemetry (ADOT) instrumentation * @see https://aws-otel.github.io/docs/getting-started/lambda * * @default - No ADOT instrumentation */ readonly adotInstrumentation?: AdotInstrumentationConfig; /** * Specify the configuration of Parameters and Secrets Extension * @see https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html * @see https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html * * @default - No Parameters and Secrets Extension */ readonly paramsAndSecrets?: ParamsAndSecretsLayerVersion; /** * A list of layers to add to the function's execution environment. You can configure your Lambda function to pull in * additional code during initialization in the form of layers. Layers are packages of libraries or other dependencies * that can be used by multiple functions. * * @default - No layers. */ readonly layers?: ILayerVersion[]; /** * The maximum of concurrent executions you want to reserve for the function. * * @default - No specific limit - account limit. * @see https://docs.aws.amazon.com/lambda/latest/dg/concurrent-executions.html */ readonly reservedConcurrentExecutions?: number; /** * Event sources for this function. * * You can also add event sources using `addEventSource`. * * @default - No event sources. */ readonly events?: IEventSource[]; /** * The number of days log events are kept in CloudWatch Logs. When updating * this property, unsetting it doesn't remove the log retention policy. To * remove the retention policy, set the value to `INFINITE`. * * This is a legacy API and we strongly recommend you move away from it if you can. * Instead create a fully customizable log group with `logs.LogGroup` and use the `logGroup` property * to instruct the Lambda function to send logs to it. * Migrating from `logRetention` to `logGroup` will cause the name of the log group to change. * Users and code and referencing the name verbatim will have to adjust. * * In AWS CDK code, you can access the log group name directly from the LogGroup construct: * ```ts * import * as logs from 'aws-cdk-lib/aws-logs'; * * declare const myLogGroup: logs.LogGroup; * myLogGroup.logGroupName; * ``` * * @default logs.RetentionDays.INFINITE */ readonly logRetention?: logs.RetentionDays; /** * The IAM role for the Lambda function associated with the custom resource * that sets the retention policy. * * This is a legacy API and we strongly recommend you migrate to `logGroup` if you can. * `logGroup` allows you to create a fully customizable log group and instruct the Lambda function to send logs to it. * * @default - A new role is created. */ readonly logRetentionRole?: iam.IRole; /** * When log retention is specified, a custom resource attempts to create the CloudWatch log group. * These options control the retry policy when interacting with CloudWatch APIs. * * This is a legacy API and we strongly recommend you migrate to `logGroup` if you can. * `logGroup` allows you to create a fully customizable log group and instruct the Lambda function to send logs to it. * * @default - Default AWS SDK retry options. */ readonly logRetentionRetryOptions?: LogRetentionRetryOptions; /** * Options for the `lambda.Version` resource automatically created by the * `fn.currentVersion` method. * @default - default options as described in `VersionOptions` */ readonly currentVersionOptions?: VersionOptions; /** * The filesystem configuration for the lambda function * * @default - will not mount any filesystem */ readonly filesystem?: FileSystem; /** * Lambda Functions in a public subnet can NOT access the internet. * Use this property to acknowledge this limitation and still place the function in a public subnet. * @see https://stackoverflow.com/questions/52992085/why-cant-an-aws-lambda-function-inside-a-public-subnet-in-a-vpc-connect-to-the/52994841#52994841 * * @default false */ readonly allowPublicSubnet?: boolean; /** * The AWS KMS key that's used to encrypt your function's environment variables. * * @default - AWS Lambda creates and uses an AWS managed customer master key (CMK). */ readonly environmentEncryption?: kms.IKey; /** * Code signing config associated with this function * * @default - Not Sign the Code */ readonly codeSigningConfig?: ICodeSigningConfig; /** * DEPRECATED * @default [Architecture.X86_64] * @deprecated use `architecture` */ readonly architectures?: Architecture[]; /** * The system architectures compatible with this lambda function. * @default Architecture.X86_64 */ readonly architecture?: Architecture; /** * Sets the runtime management configuration for a function's version. * @default Auto */ readonly runtimeManagementMode?: RuntimeManagementMode; /** * The log group the function sends logs to. * * By default, Lambda functions send logs to an automatically created default log group named /aws/lambda/\<function name\>. * However you cannot change the properties of this auto-created log group using the AWS CDK, e.g. you cannot set a different log retention. * * Use the `logGroup` property to create a fully customizable LogGroup ahead of time, and instruct the Lambda function to send logs to it. * * Providing a user-controlled log group was rolled out to commercial regions on 2023-11-16. * If you are deploying to another type of region, please check regional availability first. * * @default `/aws/lambda/${this.functionName}` - default log group created by Lambda */ readonly logGroup?: logs.ILogGroup; /** * Sets the logFormat for the function. * @deprecated Use `loggingFormat` as a property instead. * @default "Text" */ readonly logFormat?: string; /** * Sets the loggingFormat for the function. * @default LoggingFormat.TEXT */ readonly loggingFormat?: LoggingFormat; /** * Sets the Recursive Loop Protection for Lambda Function. * It lets Lambda detect and terminate unintended recursive loops. * * @default RecursiveLoop.Terminate */ readonly recursiveLoop?: RecursiveLoop; /** * Sets the application log level for the function. * @deprecated Use `applicationLogLevelV2` as a property instead. * @default "INFO" */ readonly applicationLogLevel?: string; /** * Sets the application log level for the function. * @default ApplicationLogLevel.INFO */ readonly applicationLogLevelV2?: ApplicationLogLevel; /** * Sets the system log level for the function. * @deprecated Use `systemLogLevelV2` as a property instead. * @default "INFO" */ readonly systemLogLevel?: string; /** * Sets the system log level for the function. * @default SystemLogLevel.INFO */ readonly systemLogLevelV2?: SystemLogLevel; } 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 defining a function from a Docker image. */ readonly runtime: Runtime; /** * The source code of your Lambda function. You can point to a file in an * Amazon Simple Storage Service (Amazon S3) bucket or specify your source * code as inline text. */ readonly code: Code; /** * The name of the method within your code that Lambda calls to execute * your function. The format includes the file name. It can also include * namespaces and other qualifiers, depending on the runtime. * For more information, see https://docs.aws.amazon.com/lambda/latest/dg/foundation-progmodel.html. * * 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. */ readonly handler: string; } /** * Deploys a file from inside the construct library as a function. * * The supplied file is subject to the 4096 bytes limit of being embedded in a * CloudFormation template. * * The construct includes an associated role with the lambda. * * This construct does not yet reproduce all features from the underlying resource * library. */ export class Function extends FunctionBase { /** * Returns a `lambda.Version` which represents the current version of this * Lambda function. A new version will be created every time the function's * configuration changes. * * You can specify options for this version using the `currentVersionOptions` * prop when initializing the `lambda.Function`. */ public get currentVersion(): Version { if (this._currentVersion) { return this._currentVersion; } if (this._warnIfCurrentVersionCalled) { this.warnInvokeFunctionPermissions(this); } this._currentVersion = new Version(this, 'CurrentVersion', { lambda: this, ...this.currentVersionOptions, }); // override the version's logical ID with a lazy string which includes the // hash of the function itself, so a new version resource is created when // the function configuration changes. const cfn = this._currentVersion.node.defaultChild as CfnResource; const originalLogicalId = this.stack.resolve(cfn.logicalId) as string; cfn.overrideLogicalId(Lazy.uncachedString({ produce: () => { const hash = calculateFunctionHash(this, this.hashMixins.join('')); const logicalId = trimFromStart(originalLogicalId, 255 - 32); return `${logicalId}${hash}`; }, })); return this._currentVersion; } public get resourceArnsForGrantInvoke() { return [this.functionArn, `${this.functionArn}:*`]; } /** @internal */ public static _VER_PROPS: { [key: string]: boolean } = {}; /** * Record whether specific properties in the `AWS::Lambda::Function` resource should * also be associated to the Version resource. * See 'currentVersion' section in the module README for more details. * @param propertyName The property to classify * @param locked whether the property should be associated to the version or not. */ public static classifyVersionProperty(propertyName: string, locked: boolean) { this._VER_PROPS[propertyName] = locked; } /** * Import a lambda function into the CDK using its name */ public static fromFunctionName(scope: Construct, id: string, functionName: string): IFunction { return Function.fromFunctionAttributes(scope, id, { functionArn: Stack.of(scope).formatArn({ service: 'lambda', resource: 'function', resourceName: functionName, arnFormat: ArnFormat.COLON_RESOURCE_NAME, }), sameEnvironment: true, }); } /** * Import a lambda function into the CDK using its ARN. * * For `Function.addPermissions()` to work on this imported lambda, make sure that is * in the same account and region as the stack you are importing it into. */ public static fromFunctionArn(scope: Construct, id: string, functionArn: string): IFunction { return Function.fromFunctionAttributes(scope, id, { functionArn }); } /** * Creates a Lambda function object which represents a function not defined * within this stack. * * For `Function.addPermissions()` to work on this imported lambda, set the sameEnvironment property to true * if this imported lambda is in the same account and region as the stack you are importing it into. * * @param scope The parent construct * @param id The name of the lambda construct * @param attrs the attributes of the function to import */ public static fromFunctionAttributes(scope: Construct, id: string, attrs: FunctionAttributes): IFunction { const functionArn = attrs.functionArn; const functionName = extractNameFromArn(attrs.functionArn); const role = attrs.role; class Import extends FunctionBase { public readonly functionName = functionName; public readonly functionArn = functionArn; public readonly grantPrincipal: iam.IPrincipal; public readonly role = role; public readonly permissionsNode = this.node; public readonly architecture = attrs.architecture ?? Architecture.X86_64; public readonly resourceArnsForGrantInvoke = [this.functionArn, `${this.functionArn}:*`]; protected readonly canCreatePermissions = attrs.sameEnvironment ?? this._isStackAccount(); protected readonly _skipPermissions = attrs.skipPermissions ?? false; constructor(s: Construct, i: string) { super(s, i, { environmentFromArn: functionArn, }); this.grantPrincipal = role || new iam.UnknownPrincipal({ resource: this }); if (attrs.securityGroup) { this._connections = new ec2.Connections({ securityGroups: [attrs.securityGroup], }); } else if (attrs.securityGroupId) { this._connections = new ec2.Connections({ securityGroups: [ec2.SecurityGroup.fromSecurityGroupId(scope, 'SecurityGroup', attrs.securityGroupId)], }); } } } return new Import(scope, id); } /** * Return the given named metric for this Lambda */ public static metricAll(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { return new cloudwatch.Metric({ namespace: 'AWS/Lambda', metricName, ...props, }); } /** * Metric for the number of Errors executing all Lambdas * * @default sum over 5 minutes */ public static metricAllErrors(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metricAll('Errors', { statistic: 'sum', ...props }); } /** * Metric for the Duration executing all Lambdas * * @default average over 5 minutes */ public static metricAllDuration(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metricAll('Duration', props); } /** * Metric for the number of invocations of all Lambdas * * @default sum over 5 minutes */ public static metricAllInvocations(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metricAll('Invocations', { statistic: 'sum', ...props }); } /** * Metric for the number of throttled invocations of all Lambdas * * @default sum over 5 minutes */ public static metricAllThrottles(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metricAll('Throttles', { statistic: 'sum', ...props }); } /** * Metric for the number of concurrent executions across all Lambdas * * @default max over 5 minutes */ public static metricAllConcurrentExecutions(props?: cloudwatch.MetricOptions): cloudwatch.Metric { // Mini-FAQ: why max? This metric is a gauge that is emitted every // minute, so either max or avg or a percentile make sense (but sum // doesn't). Max is more sensitive to spiky load changes which is // probably what you're interested in if you're looking at this metric // (Load spikes may lead to concurrent execution errors that would // otherwise not be visible in the avg) return this.metricAll('ConcurrentExecutions', { statistic: 'max', ...props }); } /** * Metric for the number of unreserved concurrent executions across all Lambdas * * @default max over 5 minutes */ public static metricAllUnreservedConcurrentExecutions(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metricAll('UnreservedConcurrentExecutions', { statistic: 'max', ...props }); } /** * Name of this function */ public readonly functionName: string; /** * ARN of this function */ public readonly functionArn: string; /** * Execution role associated with this function */ public readonly role?: iam.IRole; /** * The runtime configured for this lambda. */ public readonly runtime: Runtime; /** * The principal this Lambda Function is running as */ public readonly grantPrincipal: iam.IPrincipal; /** * The DLQ (as queue) associated with this Lambda Function (this is an optional attribute). */ public readonly deadLetterQueue?: sqs.IQueue; /** * The DLQ (as topic) associated with this Lambda Function (this is an optional attribute). */ public readonly deadLetterTopic?: sns.ITopic; /** * The architecture of this Lambda Function (this is an optional attribute and defaults to X86_64). */ public readonly architecture: Architecture; /** * The timeout configured for this lambda. */ public readonly timeout?: Duration; public readonly permissionsNode = this.node; protected readonly canCreatePermissions = true; /** @internal */ public readonly _layers: ILayerVersion[] = []; /** @internal */ public _logRetention?: logs.LogRetention; private _logGroup?: logs.ILogGroup; /** * Environment variables for this function */ private environment: { [key: string]: EnvironmentConfig } = {}; private readonly currentVersionOptions?: VersionOptions; private _currentVersion?: Version; private _architecture?: Architecture; private hashMixins = new Array<string>(); constructor(scope: Construct, id: string, props: FunctionProps) { super(scope, id, { physicalName: props.functionName, }); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); if (props.functionName && !Token.isUnresolved(props.functionName)) { if (props.functionName.length > 64) { throw new ValidationError(`Function name can not be longer than 64 characters but has ${props.functionName.length} characters.`, this); } if (!/^[a-zA-Z0-9-_]+$/.test(props.functionName)) { throw new ValidationError(`Function name ${props.functionName} can contain only letters, numbers, hyphens, or underscores with no spaces.`, this); } } if (props.description && !Token.isUnresolved(props.description)) { if (props.description.length > 256) { throw new ValidationError(`Function description can not be longer than 256 characters but has ${props.description.length} characters.`, this); } } const managedPolicies = new Array<iam.IManagedPolicy>(); // the arn is in the form of - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole managedPolicies.push(iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')); if (props.vpc) { // Policy that will have ENI creation permissions managedPolicies.push(iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole')); } this.role = props.role || new iam.Role(this, 'ServiceRole', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), managedPolicies, }); this.grantPrincipal = this.role; // add additional managed policies when necessary if (props.filesystem) { const config = props.filesystem.config; if (!Token.isUnresolved(config.localMountPath)) { if (!/^\/mnt\/[a-zA-Z0-9-_.]+$/.test(config.localMountPath)) { throw new ValidationError(`Local mount path should match with ^/mnt/[a-zA-Z0-9-_.]+$ but given ${config.localMountPath}.`, this); } if (config.localMountPath.length > 160) { throw new ValidationError(`Local mount path can not be longer than 160 characters but has ${config.localMountPath.length} characters.`, this); } } if (config.policies) { config.policies.forEach(p => { this.role?.addToPrincipalPolicy(p); }); } } for (const statement of (props.initialPolicy || [])) { this.role.addToPrincipalPolicy(statement); } const code = props.code.bind(this); verifyCodeConfig(code, props); let profilingGroupEnvironmentVariables: { [key: string]: string } = {}; if (props.profilingGroup && props.profiling !== false) { this.validateProfiling(props); props.profilingGroup.grantPublish(this.role); profilingGroupEnvironmentVariables = { AWS_CODEGURU_PROFILER_GROUP_NAME: props.profilingGroup.profilingGroupName, AWS_CODEGURU_PROFILER_TARGET_REGION: props.profilingGroup.env.region, AWS_CODEGURU_PROFILER_GROUP_ARN: props.profilingGroup.profilingGroupArn, AWS_CODEGURU_PROFILER_ENABLED: 'TRUE', }; } else if (props.profiling) { this.validateProfiling(props); const profilingGroup = new ProfilingGroup(this, 'ProfilingGroup', { computePlatform: ComputePlatform.AWS_LAMBDA, }); profilingGroup.grantPublish(this.role); profilingGroupEnvironmentVariables = { AWS_CODEGURU_PROFILER_GROUP_NAME: profilingGroup.profilingGroupName, AWS_CODEGURU_PROFILER_TARGET_REGION: profilingGroup.env.region, AWS_CODEGURU_PROFILER_GROUP_ARN: profilingGroup.profilingGroupArn, AWS_CODEGURU_PROFILER_ENABLED: 'TRUE', }; } const env = { ...profilingGroupEnvironmentVariables, ...props.environment }; for (const [key, value] of Object.entries(env)) { this.addEnvironment(key, value); } // DLQ can be either sns.ITopic or sqs.IQueue const dlqTopicOrQueue = this.buildDeadLetterQueue(props); if (dlqTopicOrQueue !== undefined) { if (this.isQueue(dlqTopicOrQueue)) { this.deadLetterQueue = dlqTopicOrQueue; } else { this.deadLetterTopic = dlqTopicOrQueue; } } let fileSystemConfigs: CfnFunction.FileSystemConfigProperty[] | undefined = undefined; if (props.filesystem) { fileSystemConfigs = [{ arn: props.filesystem.config.arn, localMountPath: props.filesystem.config.localMountPath, }]; } if (props.architecture && props.architectures !== undefined) { throw new ValidationError('Either architecture or architectures must be specified but not both.', this); } if (props.architectures && props.architectures.length > 1) { throw new ValidationError('Only one architecture must be specified.', this); } this._architecture = props.architecture ?? (props.architectures && props.architectures[0]); if (props.ephemeralStorageSize && !props.ephemeralStorageSize.isUnresolved() && (props.ephemeralStorageSize.toMebibytes() < 512 || props.ephemeralStorageSize.toMebibytes() > 10240)) { throw new ValidationError(`Ephemeral storage size must be between 512 and 10240 MB, received ${props.ephemeralStorageSize}.`, this); } const resource: CfnFunction = new CfnFunction(this, 'Resource', { functionName: this.physicalName, description: props.description, code: { s3Bucket: code.s3Location && code.s3Location.bucketName, s3Key: code.s3Location && code.s3Location.objectKey, s3ObjectVersion: code.s3Location && code.s3Location.objectVersion, zipFile: code.inlineCode, imageUri: code.image?.imageUri, sourceKmsKeyArn: code.sourceKMSKeyArn, }, layers: Lazy.list({ produce: () => this.renderLayers() }), // Evaluated on synthesis handler: props.handler === Handler.FROM_IMAGE ? undefined : props.handler, timeout: props.timeout && props.timeout.toSeconds(), packageType: props.runtime === Runtime.FROM_IMAGE ? 'Image' : undefined, runtime: props.runtime === Runtime.FROM_IMAGE ? undefined : 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. environment: Lazy.uncachedAny({ produce: () => this.renderEnvironment() }), memorySize: props.memorySize, ephemeralStorage: props.ephemeralStorageSize ? { size: props.ephemeralStorageSize.toMebibytes(), } : undefined, vpcConfig: this.configureVpc(props), deadLetterConfig: this.buildDeadLetterConfig(dlqTopicOrQueue), reservedConcurrentExecutions: props.reservedConcurrentExecutions, imageConfig: undefinedIfNoKeys({ command: code.image?.cmd, entryPoint: code.image?.entrypoint, workingDirectory: code.image?.workingDirectory, }), kmsKeyArn: props.environmentEncryption?.keyArn, fileSystemConfigs, codeSigningConfigArn: props.codeSigningConfig?.codeSigningConfigArn, architectures: this._architecture ? [this._architecture.name] : undefined, runtimeManagementConfig: props.runtimeManagementMode?.runtimeManagementConfig, snapStart: this.configureSnapStart(props), loggingConfig: this.getLoggingConfig(props), recursiveLoop: props.recursiveLoop, }); if ((props.tracing !== undefined) || (props.adotInstrumentation !== undefined)) { resource.tracingConfig = this.buildTracingConfig(props.tracing ?? Tracing.ACTIVE); } this._logGroup = props.logGroup; resource.node.addDependency(this.role); this.functionName = this.getResourceNameAttribute(resource.ref); this.functionArn = this.getResourceArnAttribute(resource.attrArn, { service: 'lambda', resource: 'function', resourceName: this.physicalName, arnFormat: ArnFormat.COLON_RESOURCE_NAME, }); this.runtime = props.runtime; this.timeout = props.timeout; this.architecture = props.architecture ?? Architecture.X86_64; if (props.layers) { if (props.runtime === Runtime.FROM_IMAGE) { throw new ValidationError('Layers are not supported for container image functions', this); } this.addLayers(...props.layers); } for (const event of props.events || []) { this.addEventSource(event); } // Log retention if (props.logRetention) { if (props.logGroup) { throw new ValidationError('CDK does not support setting logRetention and logGroup', this); } const logRetention = new logs.LogRetention(this, 'LogRetention', { logGroupName: `/aws/lambda/${this.functionName}`, retention: props.logRetention, role: props.logRetentionRole, logRetentionRetryOptions: props.logRetentionRetryOptions as logs.LogRetentionRetryOptions, }); this._logGroup = logs.LogGroup.fromLogGroupArn(this, 'LogGroup', logRetention.logGroupArn); this._logRetention = logRetention; } props.code.bindToResource(resource); // Event Invoke Config if (props.onFailure || props.onSuccess || props.maxEventAge || props.retryAttempts !== undefined) { this.configureAsyncInvoke({ onFailure: props.onFailure, onSuccess: props.onSuccess, maxEventAge: props.maxEventAge, retryAttempts: props.retryAttempts, }); } this.currentVersionOptions = props.currentVersionOptions; if (props.filesystem) { if (!props.vpc) { throw new ValidationError('Cannot configure \'filesystem\' without configuring a VPC.', this); } const config = props.filesystem.config; if (config.dependency) { this.node.addDependency(...config.dependency); } // There could be a race if the Lambda is used in a CustomResource. It is possible for the Lambda to // fail to attach to a given FileSystem if we do not have a dependency on the SecurityGroup ingress/egress // rules that were created between this Lambda's SG & the Filesystem SG. this.connections.securityGroups.forEach(sg => { sg.node.findAll().forEach(child => { if (child instanceof CfnResource && child.cfnResourceType === 'AWS::EC2::SecurityGroupEgress') { resource.node.addDependency(child); } }); }); config.connections?.securityGroups.forEach(sg => { sg.node.findAll().forEach(child => { if (child instanceof CfnResource && child.cfnResourceType === 'AWS::EC2::SecurityGroupIngress') { resource.node.addDependency(child); } }); }); } // Configure Lambda insights this.configureLambdaInsights(props); this.configureAdotInstrumentation(props); this.configureParamsAndSecretsExtension(props); } /** * Adds an environment variable to this Lambda function. * If this is a ref to a Lambda function, this operation results in a no-op. * @param key The environment variable key. * @param value The environment variable's value. * @param options Environment variable options. */ @MethodMetadata() public addEnvironment(key: string, value: string, options?: EnvironmentOptions): this { // Reserved environment variables will fail during cloudformation deploy if they're set. // This check is just to allow CDK to fail faster when these are specified. const reservedEnvironmentVariables = [ '_HANDLER', '_X_AMZN_TRACE_ID', 'AWS_DEFAULT_REGION', 'AWS_REGION', 'AWS_EXECUTION_ENV', 'AWS_LAMBDA_FUNCTION_NAME', 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE', 'AWS_LAMBDA_FUNCTION_VERSION', 'AWS_LAMBDA_INITIALIZATION_TYPE', 'AWS_LAMBDA_LOG_GROUP_NAME', 'AWS_LAMBDA_LOG_STREAM_NAME', 'AWS_ACCESS_KEY', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN', 'AWS_LAMBDA_RUNTIME_API', 'LAMBDA_TASK_ROOT', 'LAMBDA_RUNTIME_DIR', ]; if (reservedEnvironmentVariables.includes(key)) { throw new ValidationError(`${key} environment variable is reserved by the lambda runtime and can not be set manually. See https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html`, this); } this.environment[key] = { value, ...options }; return this; } /** * Get Logging Config property for the function. * This method returns the function LoggingConfig Property if the property is set on the * function and undefined if not. */ private getLoggingConfig(props: FunctionProps): CfnFunction.LoggingConfigProperty | undefined { if (props.logFormat && props.loggingFormat) { throw new ValidationError('Only define LogFormat or LoggingFormat, not both.', this); } if (props.applicationLogLevel && props.applicationLogLevelV2) { throw new ValidationError('Only define applicationLogLevel or applicationLogLevelV2, not both.', this); } if (props.systemLogLevel && props.systemLogLevelV2) { throw new ValidationError('Only define systemLogLevel or systemLogLevelV2, not both.', this); } if (props.applicationLogLevel || props.applicationLogLevelV2 || props.systemLogLevel || props.systemLogLevelV2) { if (props.logFormat !== LogFormat.JSON && props.loggingFormat === undefined) { throw new ValidationError(`To use ApplicationLogLevel and/or SystemLogLevel you must set LogFormat to '${LogFormat.JSON}', got '${props.logFormat}'.`, this); } if (props.loggingFormat !== LoggingFormat.JSON && props.logFormat === undefined) { throw new ValidationError(`To use ApplicationLogLevel and/or SystemLogLevel you must set LoggingFormat to '${LoggingFormat.JSON}', got '${props.loggingFormat}'.`, this); } } let loggingConfig: CfnFunction.LoggingConfigProperty; if (props.logFormat || props.logGroup || props.loggingFormat) { loggingConfig = { logFormat: props.logFormat || props.loggingFormat, systemLogLevel: props.systemLogLevel || props.systemLogLevelV2, applicationLogLevel: props.applicationLogLevel || props.applicationLogLevelV2, logGroup: props.logGroup?.logGroupName, }; return loggingConfig; } return undefined; } /** * Mix additional information into the hash of the Version object * * The Lambda Function construct does its best to automatically create a new * Version when anything about the Function changes (its code, its layers, * any of the other properties). * * However, you can sometimes source information from places that the CDK cannot * look into, like the deploy-time values of SSM parameters. In those cases, * the CDK would not force the creation of a new Version object when it actually * should. * * This method can be used to invalidate the current Version object. Pass in * any string into this method, and make sure the string changes when you know * a new Version needs to be created. * * This method may be called more than once. */ @MethodMetadata() public invalidateVersionBasedOn(x: string) { if (Token.isUnresolved(x)) { throw new ValidationError('invalidateVersionOn: input may not contain unresolved tokens', this); } this.hashMixins.push(x); } /** * Adds one or more Lambda Layers to this Lambda function. * * @param layers the layers to be added. * * @throws if there are already 5 layers on this function, or the layer is incompatible with this function's runtime. */ @MethodMetadata() public addLayers(...layers: ILayerVersion[]): void { for (const layer of layers) { if (this._layers.length === 5) { throw new ValidationError('Unable to add layer: this lambda function already uses 5 layers.', this); } if (layer.compatibleRuntimes && !layer.compatibleRuntimes.find(runtime => runtime.runtimeEquals(this.runtime))) { const runtimes = layer.compatibleRuntimes.map(runtime => runtime.name).join(', '); throw new ValidationError(`This lambda function uses a runtime that is incompatible with this layer (${this.runtime.name} is not in [${runtimes}])`, this); } // Currently no validations for compatible architectures since Lambda service // allows layers configured with one architecture to be used with a Lambda function // from another architecture. this._layers.push(layer); } } /** * Add a new version for this Lambda * * If you want to deploy through CloudFormation and use aliases, you need to * add a new version (with a new name) to your Lambda every time you want to * deploy an update. An alias can then refer to the newly created Version. * * All versions should have distinct names, and you should not delete versions * as long as your Alias needs to refer to them. * * @param name A unique name for this version. * @param codeSha256 The SHA-256 hash of the most recently deployed Lambda * source code, or omit to skip validation. * @param description A description for this version. * @param provisionedExecutions A provisioned concurrency configuration for a * function's version. * @param asyncInvokeConfig configuration for this version when it is invoked * asynchronously. * @returns A new Version object. * * @deprecated This method will create an AWS::Lambda::Version resource which * snapshots the AWS Lambda function *at the time of its creation* and it * won't get updated when the function changes. Instead, use * `this.currentVersion` to obtain a reference to a version resource that gets * automatically recreated when the function configuration (or code) changes. */ @MethodMetadata() public addVersion( name: string, codeSha256?: string, description?: string, provisionedExecutions?: number, asyncInvokeConfig: EventInvokeConfigOptions = {}): Version { return new Version(this, 'Version' + name, { lambda: this, codeSha256, description, provisionedConcurrentExecutions: provisionedExecutions, ...asyncInvokeConfig, }); } /** * Defines an alias for this function. * * The alias will automatically be updated to point to the latest version of * the function as it is being updated during a deployment. * * ```ts * declare const fn: lambda.Function; * * fn.addAlias('Live'); * * // Is equivalent to * * new lambda.Alias(this, 'AliasLive', { * aliasName: 'Live', * version: fn.currentVersion, * }); * ``` * * @param aliasName The name of the alias * @param options Alias options */ @MethodMetadata() public addAlias(aliasName: string, options?: AliasOptions): Alias { return addAlias(this, this.currentVersion, aliasName, options); } /** * The LogGroup where the Lambda function's logs are made available. * * If either `logRetention` is set or this property is called, a CloudFormation custom resource is added to the stack that * pre-creates the log group as part of the stack deployment, if it already doesn't exist, and sets the correct log retention * period (never expire, by default). * * Further, if the log group already exists and the `logRetention` is not set, the custom resource will reset the log retention * to never expire even if it was configured with a different value. */ public get logGroup(): logs.ILogGroup { if (!this._logGroup) { const logRetention = new logs.LogRetention(this, 'LogRetention', { logGroupName: `/aws/lambda/${this.functionName}`, retention: logs.RetentionDays.INFINITE, }); this._logGroup = logs.LogGroup.fromLogGroupArn(this, `${this.node.id}-LogGroup`, logRetention.logGroupArn); } return this._logGroup; } /** @internal */ public _checkEdgeCompatibility(): void { // Check env vars const envEntries = Object.entries(this.environment); for (const [key, config] of envEntries) { if (config.removeInEdge) { delete this.environment[key]; Annotations.of(this).addInfo(`Removed ${key} environment variable for Lambda@Edge compatibility`); } } const envKeys = Object.keys(this.environment); if (envKeys.length !== 0) { throw new ValidationError(`The function ${this.node.path} contains environment variables [${envKeys}] and is not compatible with Lambda@Edge. \ Environment variables can be marked for removal when used in Lambda@Edge by setting the \'removeInEdge\' property in the \'addEnvironment()\' API.`, this); } return; } /** * Configured lambda insights on the function if specified. This is achieved by adding an imported layer which is added to the * list of lambda layers on synthesis. * * https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-extension-versions.html */ private configureLambdaInsights(props: FunctionProps): void { if (props.insightsVersion === undefined) { return; } if (props.runtime !== Runtime.FROM_IMAGE) { // Layers cannot be added to Lambda container images. The image should have the insights agent installed. // See https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-Getting-Started-docker.html this.addLayers(LayerVersion.fromLayerVersionArn(this, 'LambdaInsightsLayer', props.insightsVersion._bind(this, this).arn)); } this.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLambdaInsightsExecutionRolePolicy')); } /** * Add an AWS Distro for OpenTelemetry Lambda layer. * * @param props properties for the ADOT instrumentation */ private configureAdotInstrumentation(props: FunctionProps): void { if (props.adotInstrumentation === undefined) { return; } if (props.runtime === Runtime.FROM_IMAGE) { throw new ValidationError("ADOT Lambda layer can't be configured with container image package type", this); } // This is not the complete list of incompatible runtimes and layer types. We are only // checking for common mistakes on a best-effort basis. if (this.runtime === Runtime.GO_1_X) { throw new ValidationError('Runtime go1.x is not supported by the ADOT Lambda Go SDK', this); } // The Runtime is Python and Adot is set it requires a different EXEC_WRAPPER than the other code bases. if (this.runtime.family === RuntimeFamily.PYTHON && props.adotInstrumentation.execWrapper.valueOf() !== AdotLambdaExecWrapper.INSTRUMENT_HANDLER) { throw new ValidationError('Python Adot Lambda layer requires AdotLambdaExecWrapper.INSTRUMENT_HANDLER', this); } this.addLayers(LayerVersion.fromLayerVersionArn(this, 'AdotLayer', props.adotInstrumentation.layerVersion._bind(this).arn)); this.addEnvironment('AWS_LAMBDA_EXEC_WRAPPER', props.adotInstrumentation.execWrapper); } /** * Add a Parameters and Secrets Extension Lambda layer. */ private configureParamsAndSecretsExtension(props: FunctionProps): void { if (props.paramsAndSecrets === undefined) { return; } const layerVersion = props.paramsAndSecrets._bind(this, this); this.addLayers(LayerVersion.fromLayerVersionArn(this, 'ParamsAndSecretsLayer', layerVersion.arn)); Object.entries(layerVersion.environmentVars).forEach(([key, value]) => this.addEnvironment(key, value.toString())); } private renderLayers() { if (!this._layers || this._layers.length === 0) { return undefined; } if (FeatureFlags.of(this).isEnabled(LAMBDA_RECOGNIZE_LAYER_VERSION)) { this._layers.sort(); } return this._layers.map(layer => layer.layerVersionArn); } private renderEnvironment() { if (!this.environment || Object.keys(this.environment).length === 0) { return undefined; } const variables: { [key: string]: string } = {}; // Sort environment so the hash of the function used to create // `currentVersion` is not affected by key order (this is how lambda does // it). For backwards compatibility we do not sort environment variables in case // _currentVersion is not defined. Otherwise, this would have invalidated // the template, and for example, may cause unneeded updates for nested // stacks. const keys = this._currentVersion ? Object.keys(this.environment).sort() : Object.keys(this.environment); for (const key of keys) { variables[key] = this.environment[key].value; } return { variables }; } /** * If configured, set up the VPC-related properties * * Returns the VpcConfig that should be added to the * Lambda creation properties. */ private configureVpc(props: FunctionProps): CfnFunction.VpcConfigProperty | undefined { if (props.securityGroup && props.securityGroups) { throw new ValidationError('Only one of the function props, securityGroup or securityGroups, is allowed', this); } const hasSecurityGroups = props.securityGroups && props.securityGroups.length > 0; if (!props.vpc) { if (props.allowAllOutbound !== undefined) { throw new ValidationError('Cannot configure \'allowAllOutbound\' without configuring a VPC', this); } if (props.securityGroup) { throw new ValidationError('Cannot configure \'securityGroup\' without configuring a VPC', this); } if (hasSecurityGroups) { throw new ValidationError('Cannot configure \'securityGroups\' without configuring a VPC', this); } if (props.vpcSubnets) { throw new ValidationError('Cannot configure \'vpcSubnets\' without configuring a VPC', this); } if (props.ipv6AllowedForDualStack) { throw new ValidationError('Cannot configure \'ipv6AllowedForDualStack\' without configuring a VPC', this); } if (props.allowAllIpv6Outbound !== undefined) { throw new ValidationError('Cannot configure \'allowAllIpv6Outbound\' without configuring a VPC', this); } return undefined; } if (props.allowAllOutbound !== undefined) { if (props.securityGroup) { throw new ValidationError('Configure \'allowAllOutbound\' directly on the supplied SecurityGroup.', this); } if (hasSecurityGroups) { throw new ValidationError('Configure \'allowAllOutbound\' directly on the supplied SecurityGroups.', this); } } if (props.allowAllIpv6Outbound !== undefined) { if (props.securityGroup) { throw new ValidationError('Configure \'allowAllIpv6Outbound\' directly on the supplied SecurityGroup.', this); } if (hasSecurityGroups) { throw new ValidationError('Configure \'allowAllIpv6Outbound\' directly on the supplied SecurityGroups.', this); } } let securityGroups: ec2.ISecurityGroup[]; if (hasSecurityGroups) { securityGroups = props.securityGroups; } else { const securityGroup = props.securityGroup || new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: props.vpc, description: 'Automatic security group for Lambda Function ' + Names.uniqueId(this), allowAllOutbound: props.allowAllOutbound, allowAllIpv6Outbound: props.allowAllIpv6Outbound, }); securityGroups = [securityGroup]; } this._connections = new ec2.Connections({ securityGroups }); if (props.filesystem) { if (props.filesystem.config.connections) { this.connections.allowTo( props.filesystem.config.connections, props.filesystem.config.connections.defaultPort ?? ec2.Port.tcp(efs.FileSystem.DEFAULT_PORT), ); } } const ipv6AllowedForDualStack = props.ipv6AllowedForDualStack; const allowPublicSubnet = props.allowPublicSubnet ?? false; const selectedSubnets = props.vpc.selectSubnets(props.vpcSubnets); const publicSubnetIds = new Set(props.vpc.publicSubnets.map(s => s.subnetId)); for (const subnetId of selectedSubnets.subnetIds) { if (publicSubnetIds.has(subnetId) && !allowPublicSubnet) { throw new ValidationError('Lambda Functions in a public subnet can NOT access the internet. ' + 'If you are aware of this limitation and would still like to place the function in a public subnet, set `allowPublicSubnet` to true', this); } } this.node.addDependency(selectedSubnets.internetConnectivityEstablished); // List can't be empty here, if we got this far you intended to put your Lambda // in subnets. We're going to guarantee that we get the nice error message by // making VpcNetwork do the selection again. if (props.ipv6AllowedForDualStack !== undefined) { return { ipv6AllowedForDualStack: ipv6AllowedForDualStack, subnetIds: selectedSubnets.subnetIds, securityGroupIds: securityGroups.map(sg => sg.securityGroupId), }; } else { return { subnetIds: selectedSubnets.subnetIds, securityGroupIds: securityGroups.map(sg => sg.securityGroupId), }; } } private configureSnapStart(props: FunctionProps): CfnFunction.SnapStartProperty | undefined { // return/exit if no snapStart included if (!props.snapStart) { return undefined; } // SnapStart does not support Amazon Elastic File System (Amazon EFS), or ephemeral storage greater than 512 MB. // SnapStart doesn't support provisioned concurrency either, but that's configured at the version level, // so it can't be checked at function set up time // SnapStart supports the Java 11 and Java 17 (java11 and java17) managed runtimes. // See https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html Annotations.of(this).addWarningV2('@aws-cdk/aws-lambda:snapStartRequirePublish', 'SnapStart only supports published Lambda versions. Ignore if function already has published versions.'); if (!props.runtime.supportsSnapStart) { throw new ValidationError(`SnapStart currently not supported by runtime ${props.runtime.name}`, this); } if (props.filesystem) { throw new ValidationError('SnapStart is currently not supported using EFS', this); } if (props.ephemeralStorageSize && props.ephemeralStorageSize?.toMebibytes() > 512) { throw new ValidationError('SnapStart is currently not supported using more than 512 MiB Ephemeral Storage', this); } return props.snapStart._render(); } private isQueue(deadLetterQueue: sqs.IQueue | sns.ITopic): deadLetterQueue is sqs.IQueue { return (<sqs.IQueue>deadLetterQueue).queueArn !== undefined; } private buildDeadLetterQueue(props: FunctionProps): sqs.IQueue | sns.ITopic | undefined { if (!props.deadLetterQueue && !props.deadLetterQueueEnabled && !props.deadLetterTopic) { return undefined; } if (props.deadLetterQueue && props.deadLetterQueueEnabled === false) { throw Error('deadLetterQueue defined but deadLetterQueueEnabled explicitly set to false'); } if (props.deadLetterTopic && (props.deadLetterQueue || props.deadLetterQueueEnabled !== undefined)) { throw new ValidationError('deadLetterQueue and deadLetterTopic cannot be specified together at the same time', this); } let deadLetterQueue: sqs.IQueue | sns.ITopic; if (props.deadLetterTopic) { deadLetterQueue = props.deadLetterTopic; this.addToRolePolicy(new iam.PolicyStatement({ actions: ['sns:Publish'], resources: [deadLetterQueue.topicArn], })); } else { deadLetterQueue = props.deadLetterQueue || new sqs.Queue(this, 'DeadLetterQueue', { retentionPeriod: Duration.days(14), }); this.addToRolePolicy(new iam.PolicyStatement({ actions: ['sqs:SendMessage'], resources: [deadLetterQueue.queueArn], })); } return deadLetterQueue; } private buildDeadLetterConfig(deadLetterQueue?: sqs.IQueue | sns.ITopic) { if (deadLetterQueue) { return { targetArn: this.isQueue(deadLetterQueue) ? deadLetterQueue.queueArn : deadLetterQueue.topicArn, }; } else { return undefined; } } private buildTracingConfig(tracing: Tracing) { if (tracing === undefined || tracing === Tracing.DISABLED) { return undefined; } this.addToRolePolicy(new iam.PolicyStatement({ actions: ['xray:PutTraceSegments', 'xray:PutTelemetryRecords'], resources: ['*'], })); return { mode: tracing, }; } private validateProfiling(props: FunctionProps) { if (!props.runtime.supportsCodeGuruProfiling) { throw new ValidationError(`CodeGuru profiling is not supported by runtime ${props.runtime.name}`, this); } if (props.environment && (props.environment.AWS_CODEGURU_PROFILER_GROUP_NAME || props.environment.AWS_CODEGURU_PROFILER_GROUP_ARN || props.environment.AWS_CODEGURU_PROFILER_TARGET_REGION || props.environment.AWS_CODEGURU_PROFILER_ENABLED)) { Annotations.of(this).addWarning('AWS_CODEGURU_PROFILER_GROUP_NAME, AWS_CODEGURU_PROFILER_GROUP_ARN, AWS_CODEGURU_PROFILER_TARGET_REGION, and AWS_CODEGURU_PROFILER_ENABLED should not be set when profiling options enabled'); } } } /** * Environment variables options */ export interface EnvironmentOptions { /** * When used in Lambda@Edge via edgeArn() API, these environment * variables will be removed. If not set, an error will be thrown. * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html#lambda-requirements-lambda-function-configuration * * @default false - using the function in Lambda@Edge will throw */ readonly removeInEdge?: boolean; } /** * Configuration for an environment variable */ interface EnvironmentConfig extends EnvironmentOptions { readonly value: string; } /** * Given an opaque (token) ARN, returns a CloudFormation expression that extracts the function * name from the ARN. * * Function ARNs look like this: * * arn:aws:lambda:region:account-id:function:function-name * * ..which means that in order to extract the `function-name` component from the ARN, we can * split the ARN using ":" and select the component in index 6. * * @returns `FnSelect(6, FnSplit(':', arn))` */ function extractNameFromArn(arn: string) { return Fn.select(6, Fn.split(':', arn)); } export function verifyCodeConfig(code: CodeConfig, props: FunctionProps) { // mutually exclusive const codeType = [code.inlineCode, code.s3Location, code.image]; if (codeType.filter(x => !!x).length !== 1) { throw new UnscopedValidationError('lambda.Code must specify exactly one of: "inlineCode", "s3Location", or "image"'); } if (!!code.image === (props.handler !== Handler.FROM_IMAGE)) { throw new UnscopedValidationError('handler must be `Handler.FROM_IMAGE` when using image asset for Lambda function'); } if (!!code.image === (props.runtime !== Runtime.FROM_IMAGE)) { throw new UnscopedValidationError('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 && !props.runtime.supportsInlineCode) { throw new UnscopedValidationError(`Inline source not allowed for ${props.runtime!.name}`); } } function undefinedIfNoKeys<A extends { [key: string]: unknown }>(struct: A): A | undefined { const allUndefined = Object.values(struct).every(val => val === undefined); return allUndefined ? undefined : struct; } /** * Aspect for upgrading function versions when the provided feature flag * is enabled. This can be necessary when the feature flag * changes the function hash, as such changes must be associated with a new * version. This aspect will change the function description in these cases, * which "validates" the new function hash. */ export class FunctionVersionUpgrade implements IAspect { constructor(private readonly featureFlag: string, private readonly enabled = true) { } public visit(node: IConstruct): void { if (node instanceof Function && this.enabled === FeatureFlags.of(node).isEnabled(this.featureFlag)) { const cfnFunction = node.node.defaultChild as CfnFunction; const desc = cfnFunction.description ? `${cfnFunction.description} ` : ''; cfnFunction.addPropertyOverride('Description', `${desc}version-hash:${calculateFunctionHash(node)}`); } } }