Skip to content

Commit 8036706

Browse files
committed
add python lambda function
1 parent 7c85864 commit 8036706

5 files changed

Lines changed: 344 additions & 10 deletions

File tree

packages/cdkConstructs/src/config/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ export async function getTrustStoreVersion(trustStoreFile: string, region: strin
3535
export function calculateVersionedStackName(baseStackName: string, version: string): string {
3636
return `${baseStackName}-${version.replaceAll(".", "-")}`
3737
}
38+
39+
export {LAMBDA_INSIGHTS_LAYER_ARNS} from "./lambdaInsights"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-extension-versions.html
2+
// for latest ARNs
3+
export const LAMBDA_INSIGHTS_LAYER_ARNS = {
4+
x64: "arn:aws:lambda:eu-west-2:580247275435:layer:LambdaInsightsExtension:64",
5+
arm64: "arn:aws:lambda:eu-west-2:580247275435:layer:LambdaInsightsExtension-Arm64:31"
6+
} as const
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
import {Construct} from "constructs"
2+
import {Duration, Fn, RemovalPolicy} from "aws-cdk-lib"
3+
import {
4+
ManagedPolicy,
5+
PolicyStatement,
6+
Role,
7+
ServicePrincipal,
8+
IManagedPolicy
9+
} from "aws-cdk-lib/aws-iam"
10+
import {Key} from "aws-cdk-lib/aws-kms"
11+
import {Stream} from "aws-cdk-lib/aws-kinesis"
12+
import {
13+
Architecture,
14+
CfnFunction,
15+
LayerVersion,
16+
Runtime,
17+
Function as LambdaFunctionResource,
18+
Code,
19+
ILayerVersion
20+
} from "aws-cdk-lib/aws-lambda"
21+
import {CfnLogGroup, CfnSubscriptionFilter, LogGroup} from "aws-cdk-lib/aws-logs"
22+
import {join} from "path"
23+
import {LAMBDA_INSIGHTS_LAYER_ARNS} from "../config"
24+
25+
export interface PythonLambdaFunctionProps {
26+
/**
27+
* Name of the lambda function. The log group name is also based on this name.
28+
*
29+
*/
30+
readonly functionName: string
31+
/**
32+
* The base directory for resolving the package base path and entry point.
33+
* Should point to the monorepo root.
34+
*/
35+
readonly projectBaseDir: string
36+
/**
37+
* The relative path from projectBaseDir to the base folder where the lambda function code is located.
38+
*
39+
*/
40+
readonly packageBasePath: string
41+
/**
42+
* The function handler (file and method). Example: `index.handler` for `index.py` file and `handler` method.
43+
*/
44+
readonly handler: string
45+
/**
46+
* A map of environment variables to set for the lambda function.
47+
*/
48+
readonly environmentVariables?: {[key: string]: string}
49+
/**
50+
* Optional additional IAM policies to attach to role the lambda executes as.
51+
*/
52+
readonly additionalPolicies?: Array<IManagedPolicy>
53+
/**
54+
* The number of days to retain logs in CloudWatch Logs.
55+
* @default 30 days
56+
*/
57+
readonly logRetentionInDays: number
58+
/**
59+
* The log level for the lambda function.
60+
* @default "INFO"
61+
*/
62+
readonly logLevel: string
63+
/**
64+
* Optional location of dependencies to include as a separate Lambda layer.
65+
*/
66+
readonly dependencyLocation?: string
67+
/**
68+
* Optional list of Lambda layers to attach to the function.
69+
*/
70+
readonly layers?: Array<ILayerVersion>
71+
/**
72+
* Optional timeout in seconds for the Lambda function.
73+
* @default 50 seconds
74+
*/
75+
readonly timeoutInSeconds?: number
76+
/**
77+
* Optional runtime for the Lambda function.
78+
* @default Runtime.PYTHON_3_14
79+
*/
80+
readonly runtime?: Runtime
81+
/**
82+
* Optional architecture for the Lambda function. Defaults to x86_64.
83+
* @default Architecture.X86_64
84+
*/
85+
readonly architecture?: Architecture
86+
}
87+
88+
export class PythonLambdaFunction extends Construct {
89+
/**
90+
* The managed policy that allows execution of the Lambda function.
91+
*
92+
* Use this policy to grant other AWS resources permission to invoke this Lambda function.
93+
*
94+
* @example
95+
* ```typescript
96+
* // Grant API Gateway permission to invoke the Lambda
97+
* apiGatewayRole.addManagedPolicy(lambdaConstruct.executionPolicy);
98+
* ```
99+
*/
100+
public readonly executionPolicy: ManagedPolicy
101+
102+
/**
103+
* The Lambda function instance.
104+
*
105+
* Provides access to the underlying AWS Lambda function for additional configuration
106+
* or to reference its ARN, name, or other properties.
107+
*
108+
* @example
109+
* ```typescript
110+
* // Get the function ARN
111+
* const functionArn = lambdaConstruct.function.functionArn;
112+
*
113+
* // Add additional environment variables
114+
* lambdaConstruct.function.addEnvironment('NEW_VAR', 'value');
115+
* ```
116+
*/
117+
public readonly function: LambdaFunctionResource
118+
119+
/**
120+
* The IAM role assumed by the Lambda function during execution.
121+
*/
122+
public readonly executionRole: Role
123+
124+
/**
125+
* Creates a new PythonLambdaFunction construct.
126+
*
127+
* This construct creates:
128+
* - A python Lambda function with
129+
* - CloudWatch log group with KMS encryption
130+
* - Managed IAM policy for writing logs
131+
* - IAM role for execution with necessary permissions
132+
* - Subscription filter on logs so they are forwarded to splunk
133+
* - Managed IAM policy for invoking the Lambda function
134+
*
135+
* It also
136+
* - attaches the Lambda Insights layer for monitoring.
137+
* - adds cfnGuard suppressions for common issues.
138+
* - adds cdk-nag suppressions for common issues.
139+
*
140+
* @example
141+
* ```typescript
142+
* const lambdaFunction = new PythonLambdaFunction(this, 'MyFunction', {
143+
* functionName: 'my-lambda',
144+
* projectBaseDir: '/path/to/monorepo'
145+
* packageBasePath: 'packages/my-lambda',
146+
* handler: 'app.handler.handler',
147+
* environmentVariables: {
148+
* TABLE_NAME: 'my-table'
149+
* },
150+
* logRetentionInDays: 30,
151+
* logLevel: 'INFO'
152+
* });
153+
* @param scope - The scope in which to define this construct
154+
* @param id - The scoped construct ID. Must be unique amongst siblings in the same scope
155+
* @param props - Configuration properties for the Lambda function
156+
*/
157+
public constructor(scope: Construct, id: string, props: PythonLambdaFunctionProps) {
158+
super(scope, id)
159+
// Destructure with defaults
160+
const {
161+
functionName,
162+
projectBaseDir,
163+
packageBasePath,
164+
handler,
165+
environmentVariables,
166+
additionalPolicies = [], // Default to empty array
167+
logRetentionInDays = 30, // Default retention
168+
logLevel = "INFO", // Default log level
169+
dependencyLocation,
170+
layers = [], // Default to empty array
171+
timeoutInSeconds = 50,
172+
runtime = Runtime.PYTHON_3_14,
173+
architecture = Architecture.X86_64
174+
} = props
175+
176+
// Import shared cloud resources from cross-stack references
177+
const cloudWatchLogsKmsKey = Key.fromKeyArn(
178+
this, "cloudWatchLogsKmsKey", Fn.importValue("account-resources:CloudwatchLogsKmsKeyArn"))
179+
180+
const cloudwatchEncryptionKMSPolicy = ManagedPolicy.fromManagedPolicyArn(
181+
this, "cloudwatchEncryptionKMSPolicyArn", Fn.importValue("account-resources:CloudwatchEncryptionKMSPolicyArn"))
182+
183+
const splunkDeliveryStream = Stream.fromStreamArn(
184+
this, "SplunkDeliveryStream", Fn.importValue("lambda-resources:SplunkDeliveryStream"))
185+
186+
const splunkSubscriptionFilterRole = Role.fromRoleArn(
187+
this, "splunkSubscriptionFilterRole", Fn.importValue("lambda-resources:SplunkSubscriptionFilterRole"))
188+
189+
const lambdaInsightsLogGroupPolicy = ManagedPolicy.fromManagedPolicyArn(
190+
this, "lambdaInsightsLogGroupPolicy", Fn.importValue("lambda-resources:LambdaInsightsLogGroupPolicy"))
191+
192+
const insightsLambdaLayerArn = architecture === Architecture.ARM_64
193+
? LAMBDA_INSIGHTS_LAYER_ARNS.arm64
194+
: LAMBDA_INSIGHTS_LAYER_ARNS.x64
195+
const insightsLambdaLayer = LayerVersion.fromLayerVersionArn(
196+
this, "LayerFromArn", insightsLambdaLayerArn)
197+
198+
// Log group with encryption and retention
199+
const logGroup = new LogGroup(this, "LambdaLogGroup", {
200+
encryptionKey: cloudWatchLogsKmsKey,
201+
logGroupName: `/aws/lambda/${functionName}`,
202+
retention: logRetentionInDays,
203+
removalPolicy: RemovalPolicy.DESTROY
204+
})
205+
206+
// Suppress CFN guard rules for log group
207+
const cfnlogGroup = logGroup.node.defaultChild as CfnLogGroup
208+
cfnlogGroup.cfnOptions.metadata = {
209+
guard: {
210+
SuppressedRules: [
211+
"CW_LOGGROUP_RETENTION_PERIOD_CHECK"
212+
]
213+
}
214+
}
215+
216+
// Send logs to Splunk
217+
new CfnSubscriptionFilter(this, "LambdaLogsSplunkSubscriptionFilter", {
218+
destinationArn: splunkDeliveryStream.streamArn,
219+
filterPattern: "",
220+
logGroupName: logGroup.logGroupName,
221+
roleArn: splunkSubscriptionFilterRole.roleArn
222+
})
223+
224+
// Create managed policy for Lambda CloudWatch logs access
225+
const putLogsManagedPolicy = new ManagedPolicy(this, "LambdaPutLogsManagedPolicy", {
226+
description: `write to ${functionName} logs`,
227+
statements: [
228+
new PolicyStatement({
229+
actions: [
230+
"logs:CreateLogStream",
231+
"logs:PutLogEvents"
232+
],
233+
resources: [
234+
logGroup.logGroupArn,
235+
`${logGroup.logGroupArn}:log-stream:*`
236+
]
237+
})
238+
]
239+
})
240+
241+
// Aggregate all required policies for Lambda execution
242+
const requiredPolicies: Array<IManagedPolicy> = [
243+
putLogsManagedPolicy,
244+
lambdaInsightsLogGroupPolicy,
245+
cloudwatchEncryptionKMSPolicy,
246+
...(additionalPolicies ?? [])
247+
]
248+
249+
const role = new Role(this, "LambdaRole", {
250+
assumedBy: new ServicePrincipal("lambda.amazonaws.com"),
251+
managedPolicies: requiredPolicies
252+
})
253+
254+
const layersToAdd = [insightsLambdaLayer]
255+
if (dependencyLocation) {
256+
const dependencyLayer = new LayerVersion(this, "DependencyLayer", {
257+
removalPolicy: RemovalPolicy.DESTROY,
258+
code: Code.fromAsset(join(projectBaseDir, dependencyLocation)),
259+
compatibleArchitectures: [architecture]
260+
})
261+
layersToAdd.push(dependencyLayer)
262+
}
263+
layersToAdd.push(...layers)
264+
265+
// Create Lambda function with Python runtime and monitoring
266+
const lambdaFunction = new LambdaFunctionResource(this, functionName, {
267+
runtime: runtime,
268+
memorySize: 256,
269+
timeout: Duration.seconds(timeoutInSeconds),
270+
architecture,
271+
handler: handler,
272+
code: Code.fromAsset(join(projectBaseDir, packageBasePath)),
273+
role,
274+
environment: {
275+
...environmentVariables,
276+
POWERTOOLS_LOG_LEVEL: logLevel
277+
},
278+
logGroup,
279+
layers: layersToAdd
280+
})
281+
282+
// Suppress CFN guard rules for Lambda function
283+
const cfnLambda = lambdaFunction.node.defaultChild as CfnFunction
284+
cfnLambda.cfnOptions.metadata = {
285+
guard: {
286+
SuppressedRules: [
287+
"LAMBDA_DLQ_CHECK",
288+
"LAMBDA_INSIDE_VPC",
289+
"LAMBDA_CONCURRENCY_CHECK"
290+
]
291+
}
292+
}
293+
294+
// Create policy for external services to invoke this Lambda
295+
const executionManagedPolicy = new ManagedPolicy(this, "ExecuteLambdaManagedPolicy", {
296+
description: `execute lambda ${functionName}`,
297+
statements: [
298+
new PolicyStatement({
299+
actions: ["lambda:InvokeFunction"],
300+
resources: [lambdaFunction.functionArn]
301+
})
302+
]
303+
})
304+
305+
// Export Lambda function and sexecution policy for use by other constructs
306+
this.function = lambdaFunction
307+
this.executionPolicy = executionManagedPolicy
308+
this.executionRole = role
309+
}
310+
}

0 commit comments

Comments
 (0)