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);
});
}
}