packages/constructs/L3/dataops/dataops-lambda-l3-construct/lib/dataops-lambda-l3-construct.ts (260 lines of code) (raw):

/*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ import { MdaaSecurityGroupRuleProps } from '@aws-mdaa/ec2-constructs'; import { Ec2L3Construct, Ec2L3ConstructProps } from '@aws-mdaa/ec2-l3-construct'; import { EventBridgeHelper, EventBridgeProps } from '@aws-mdaa/eventbridge-helper'; import { MdaaKmsKey } from '@aws-mdaa/kms-constructs'; import { MdaaL3Construct, MdaaL3ConstructProps } from '@aws-mdaa/l3-construct'; import { MdaaDockerImageFunction, MdaaDockerImageFunctionProps, MdaaLambdaFunction, MdaaLambdaFunctionOptions, MdaaLambdaFunctionProps, MdaaLambdaRole, } from '@aws-mdaa/lambda-constructs'; import { aws_events_targets, Duration, Size } from 'aws-cdk-lib'; import { SecurityGroup, Subnet, Vpc } from 'aws-cdk-lib/aws-ec2'; import { RuleTargetInput } from 'aws-cdk-lib/aws-events'; import { IKey } from 'aws-cdk-lib/aws-kms'; import { Code, DockerImageCode, Function as LambdaFunction, IFunction, LayerVersion, Runtime, } from 'aws-cdk-lib/aws-lambda'; import { MdaaNagSuppressions } from '@aws-mdaa/construct'; //NOSONAR import { Construct } from 'constructs'; export interface VpcConfigProps { /** * The ID of the VPC on which the Lambda will be deployed */ readonly vpcId: string; /** * The IDs of the subnets on which the Lambda will be deployed */ readonly subnetIds: string[]; /** * If specified, the function will use this security group for * it's VPC connection. Otherwise a new security group will * be created. */ readonly securityGroupId?: string; /** * List of egress rules to be added to the function SG */ readonly securityGroupEgressRules?: MdaaSecurityGroupRuleProps; } export interface FunctionProps extends FunctionOptions { /** * Function source code location */ readonly srcDir: string; /** * The Lambda handler in the source code */ readonly handler?: string; /** * The name of the Lambda runtime. IE 'python3.8' 'nodejs14.x' */ readonly runtime?: string; /** * If true, srcDir is expected to contain a DockerFile */ readonly dockerBuild?: boolean; } export interface FunctionOptions { /** * The basic function name */ readonly functionName: string; /** * A description of the function. * * @default - No description. */ readonly description?: string; /** * The arn of the role with which the function will be executed */ readonly roleArn: string; /** * EventBridge props */ readonly eventBridge?: EventBridgeProps; /** * If specified, function will be VPC bound */ readonly vpcConfig?: VpcConfigProps; /** * The maximum age of a request (in seconds) that Lambda sends to a function for * processing. * * Minimum: 60 seconds * Maximum: 6 hours * * @default 21600 seconds (6 hours) */ readonly maxEventAgeSeconds?: number; /** * The maximum number of times to retry when the function returns an error. * * Minimum: 0 * Maximum: 2 * * @default 2 */ readonly retryAttempts?: number; /** * List of layer names generated by this config to be added to the function */ readonly generatedLayerNames?: string[]; /** * List of existing named layer version Arns to be directly added to the function */ /** @jsii ignore */ readonly layerArns?: { [name: string]: 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 3 */ readonly timeoutSeconds?: number; /** * 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; }; /** * 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; /** * 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 memorySizeMB?: number; /** * The size of the function’s /tmp directory in MB. * * @default 512 MiB */ readonly ephemeralStorageSizeMB?: number; } export interface LayerProps { /** * The source directory or zip file */ readonly src: string; /** * Description of the layer */ readonly description?: string; /** * Layer name */ readonly layerName: string; } export interface LambdaFunctionL3ConstructProps extends MdaaL3ConstructProps { /** * Arn of KMS key which will be used to encrypt the function environments and dead letter queues */ readonly kmsArn: string; /** * List of layer definitions */ readonly layers?: LayerProps[]; /** * List of function definitions */ readonly functions?: FunctionProps[]; } export class LambdaFunctionL3Construct extends MdaaL3Construct { protected readonly props: LambdaFunctionL3ConstructProps; private readonly projectKmsKey: IKey; public readonly functionsMap: { [name: string]: LambdaFunction } = {}; constructor(scope: Construct, id: string, props: LambdaFunctionL3ConstructProps) { super(scope, id, props); this.props = props; this.projectKmsKey = MdaaKmsKey.fromKeyArn(this.scope, 'project-kms', this.props.kmsArn); const generatedLayers = Object.fromEntries( this.props.layers?.map(layerProps => { return [layerProps.layerName, this.createLambdaLayer(layerProps)]; }) || [], ); // Build our functions! this.props.functions?.forEach(functionProps => { this.functionsMap[functionProps.functionName] = this.createFunctionFromProps(functionProps, generatedLayers); }); //Remove unneeded inline policies which CDK automatically adds to execution role //We add a resource policy to the DLQ which allows the execution role to write to it. //This avoids hitting NIST.800.53.R5-IAMNoInlinePolicy and HIPAA.Security-IAMNoInlinePolicy this.scope.node.children.forEach(child => { if (child.node.id.startsWith('LambdaRole')) { this.node.tryRemoveChild(child.node.id); } }); } private createLambdaLayer(layerProps: LayerProps): LayerVersion { return new LayerVersion(this.scope, `layer-${layerProps.layerName}`, { code: Code.fromAsset(layerProps.src), layerVersionName: this.props.naming.resourceName(layerProps.layerName, 64), description: layerProps.description, }); } /** @jsii ignore */ private createFunctionFromProps( functionProps: FunctionProps, generatedLayersByName: { [name: string]: LayerVersion }, ): LambdaFunction { const role = MdaaLambdaRole.fromRoleArn( this.scope, `lambda-role-${functionProps.functionName}`, functionProps.roleArn, ); let functionVpcProps = {}; if (functionProps.vpcConfig) { const securityGroup = functionProps.vpcConfig.securityGroupId ? SecurityGroup.fromSecurityGroupId( this, `${functionProps.functionName}-sg`, functionProps.vpcConfig.securityGroupId, ) : this.createFunctionSecurityGroup( `${functionProps.functionName}-sg`, functionProps.vpcConfig?.vpcId, functionProps.vpcConfig.securityGroupEgressRules, ); const vpc = Vpc.fromVpcAttributes(this, `vpc-${functionProps.functionName}`, { availabilityZones: ['dummy'], vpcId: functionProps.vpcConfig.vpcId, }); const subnets = functionProps.vpcConfig.subnetIds.map(id => { return Subnet.fromSubnetId(this, `${functionProps.functionName}-subnet-${id}`, id); }); functionVpcProps = { securityGroups: [securityGroup], vpc: vpc, vpcSubnets: { subnets: subnets, }, }; } const dlq = EventBridgeHelper.createDlq( this.scope, this.props.naming, functionProps.functionName, this.projectKmsKey, role, ); const lambdaOptions: MdaaLambdaFunctionOptions = { ...functionVpcProps, functionName: functionProps.functionName, description: functionProps.description, role: role, environmentEncryption: this.projectKmsKey, naming: this.props.naming, deadLetterQueue: dlq, retryAttempts: functionProps.retryAttempts, maxEventAge: functionProps.maxEventAgeSeconds ? Duration.seconds(functionProps.maxEventAgeSeconds) : undefined, timeout: functionProps.timeoutSeconds ? Duration.seconds(functionProps.timeoutSeconds) : undefined, environment: functionProps.environment, reservedConcurrentExecutions: functionProps.reservedConcurrentExecutions, memorySize: functionProps.memorySizeMB, ephemeralStorageSize: functionProps.ephemeralStorageSizeMB ? Size.mebibytes(functionProps.ephemeralStorageSizeMB) : undefined, }; const lambdaFunction = this.createDockerOrLambdaFunction(lambdaOptions, functionProps, generatedLayersByName); //An inline policy to allow the Lambda role to write to DLQ is automatically added, //but this triggers Nags. Instead, we use the Queue Resource policy, //and remove the inline policy here. role.node.tryRemoveChild('Policy'); MdaaNagSuppressions.addCodeResourceSuppressions( lambdaFunction, [ { id: 'NIST.800.53.R5-LambdaConcurrency', reason: 'Concurrency Limits not required.' }, { id: 'NIST.800.53.R5-LambdaInsideVPC', reason: 'VPC Not Required' }, { id: 'HIPAA.Security-LambdaConcurrency', reason: 'Concurrency Limits not required.' }, { id: 'PCI.DSS.321-LambdaConcurrency', reason: 'Concurrency Limits not required.' }, { id: 'HIPAA.Security-LambdaInsideVPC', reason: 'VPC Not Required' }, { id: 'PCI.DSS.321-LambdaInsideVPC', reason: 'VPC Not Required' }, ], true, ); if (functionProps.eventBridge) { this.createFunctionEventBridgeRules(functionProps.eventBridge, functionProps.functionName, lambdaFunction); } return lambdaFunction; } private createDockerOrLambdaFunction( lambdaOptions: MdaaLambdaFunctionOptions, functionProps: FunctionProps, generatedLayersByName: { [name: string]: LayerVersion }, ): LambdaFunction { if (functionProps.dockerBuild) { const lambdaProps: MdaaDockerImageFunctionProps = { ...lambdaOptions, code: DockerImageCode.fromImageAsset(functionProps.srcDir), }; return new MdaaDockerImageFunction(this.scope, functionProps.functionName, lambdaProps); } else { if (!functionProps.runtime) { throw new Error('Function runtime must be defined for non-docker functions'); } if (!functionProps.handler) { throw new Error('Function handler must be defined for non-docker functions'); } const existingLayers = Object.entries(functionProps.layerArns || {}).map(entry => LayerVersion.fromLayerVersionArn(this.scope, `${functionProps.functionName}-${entry[0]}`, entry[1]), ); const generatedLayers = functionProps.generatedLayerNames?.map(generatedLayerName => { const generatedLayer = generatedLayersByName[generatedLayerName]; if (!generatedLayer) { throw new Error(`Function references non-existant generated layer ${generatedLayerName}`); } return generatedLayer; }); const lambdaProps: MdaaLambdaFunctionProps = { ...lambdaOptions, runtime: new Runtime(functionProps.runtime), code: Code.fromAsset(functionProps.srcDir), handler: functionProps.handler, layers: [...(generatedLayers || []), ...existingLayers], }; return new MdaaLambdaFunction(this.scope, functionProps.functionName, lambdaProps); } } private createFunctionSecurityGroup( sgName: string, vpcId: string, securityGroupEgressRules?: MdaaSecurityGroupRuleProps, ): SecurityGroup { const ec2L3Props: Ec2L3ConstructProps = { ...(this.props as MdaaL3ConstructProps), adminRoles: [], securityGroups: { [sgName]: { vpcId: vpcId, egressRules: securityGroupEgressRules, }, }, }; const ec2Construct = new Ec2L3Construct(this, `ec2`, ec2L3Props); return ec2Construct.securityGroups[sgName]; } private createFunctionEventBridgeRules( eventBridgeProps: EventBridgeProps, functionName: string, lambdaFunction: IFunction, ) { const dlq = EventBridgeHelper.createDlq( this.scope, this.props.naming, `${functionName}-events`, this.projectKmsKey, ); const eventBridgeRuleProps = EventBridgeHelper.createNamedEventBridgeRuleProps(eventBridgeProps, functionName); Object.entries(eventBridgeRuleProps).forEach(propsEntry => { const ruleName = propsEntry[0]; const ruleProps = propsEntry[1]; const target = new aws_events_targets.LambdaFunction(lambdaFunction, { deadLetterQueue: dlq, maxEventAge: eventBridgeProps.maxEventAgeSeconds ? Duration.seconds(eventBridgeProps.maxEventAgeSeconds) : undefined, retryAttempts: eventBridgeProps.retryAttempts, event: RuleTargetInput.fromObject(ruleProps.input), }); EventBridgeHelper.createEventBridgeRuleForTarget(this.scope, this.props.naming, target, ruleName, ruleProps); }); } }