Skip to content

Commit 0670329

Browse files
committed
add python lambda construct
1 parent 8036706 commit 0670329

3 files changed

Lines changed: 415 additions & 2 deletions

File tree

packages/cdkConstructs/src/constructs/PythonLambdaFunction.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ export class PythonLambdaFunction extends Construct {
267267
runtime: runtime,
268268
memorySize: 256,
269269
timeout: Duration.seconds(timeoutInSeconds),
270+
functionName: functionName,
270271
architecture,
271272
handler: handler,
272273
code: Code.fromAsset(join(projectBaseDir, packageBasePath)),
Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
import {App, assertions, Stack} from "aws-cdk-lib"
2+
import {Template, Match} from "aws-cdk-lib/assertions"
3+
import {ManagedPolicy, PolicyStatement, Role} from "aws-cdk-lib/aws-iam"
4+
import {LogGroup} from "aws-cdk-lib/aws-logs"
5+
import {
6+
Architecture,
7+
Function as LambdaFunction,
8+
LayerVersion,
9+
Runtime
10+
} from "aws-cdk-lib/aws-lambda"
11+
import {resolve} from "node:path"
12+
import {
13+
beforeAll,
14+
describe,
15+
expect,
16+
test
17+
} from "vitest"
18+
19+
import {PythonLambdaFunction} from "../../src/constructs/PythonLambdaFunction"
20+
21+
describe("pythonFunctionConstruct works correctly", () => {
22+
let stack: Stack
23+
let app: App
24+
let template: assertions.Template
25+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
26+
let lambdaLogGroupResource: any
27+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
28+
let lambdaRoleResource: any
29+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
30+
let lambdaResource: any
31+
32+
beforeAll(() => {
33+
app = new App()
34+
stack = new Stack(app, "pythonLambdaConstructStack")
35+
const functionConstruct = new PythonLambdaFunction(stack, "dummyPythonFunction", {
36+
functionName: "testPythonLambda",
37+
projectBaseDir: resolve(__dirname, "../../../.."),
38+
packageBasePath: "packages/cdkConstructs",
39+
handler: "index.handler",
40+
environmentVariables: {},
41+
logRetentionInDays: 30,
42+
logLevel: "INFO"
43+
})
44+
template = Template.fromStack(stack)
45+
const lambdaLogGroup = functionConstruct.node.tryFindChild("LambdaLogGroup") as LogGroup
46+
const lambdaRole = functionConstruct.node.tryFindChild("LambdaRole") as Role
47+
const cfnLambda = functionConstruct.node.tryFindChild("testPythonLambda") as LambdaFunction
48+
lambdaRoleResource = stack.resolve(lambdaRole.roleName)
49+
lambdaLogGroupResource = stack.resolve(lambdaLogGroup.logGroupName)
50+
lambdaResource = stack.resolve(cfnLambda.functionName)
51+
})
52+
53+
test("We have found log group, role and lambda", () => {
54+
expect(lambdaRoleResource).not.toBe(undefined)
55+
expect(lambdaLogGroupResource).not.toBe(undefined)
56+
expect(lambdaResource).not.toBe(undefined)
57+
})
58+
59+
test("it has the correct log group", () => {
60+
template.hasResourceProperties("AWS::Logs::LogGroup", {
61+
LogGroupName: "/aws/lambda/testPythonLambda",
62+
KmsKeyId: {"Fn::ImportValue": "account-resources:CloudwatchLogsKmsKeyArn"},
63+
RetentionInDays: 30
64+
})
65+
})
66+
67+
test("it has the correct policy for writing logs", () => {
68+
template.hasResourceProperties("AWS::IAM::ManagedPolicy", {
69+
Description: "write to testPythonLambda logs",
70+
PolicyDocument: {
71+
Version: "2012-10-17",
72+
Statement: [{
73+
Action: ["logs:CreateLogStream", "logs:PutLogEvents"],
74+
Effect: "Allow",
75+
Resource: [
76+
{"Fn::GetAtt": [lambdaLogGroupResource.Ref, "Arn"]},
77+
{"Fn::Join": ["", [{"Fn::GetAtt": [lambdaLogGroupResource.Ref, "Arn"]}, ":log-stream:*"]]}
78+
]
79+
}]
80+
}
81+
})
82+
})
83+
84+
test("it has the correct subscription filter", () => {
85+
template.hasResourceProperties("AWS::Logs::SubscriptionFilter", {
86+
LogGroupName: {"Ref": lambdaLogGroupResource.Ref},
87+
FilterPattern: "",
88+
RoleArn: {"Fn::ImportValue": "lambda-resources:SplunkSubscriptionFilterRole"},
89+
DestinationArn: {"Fn::ImportValue": "lambda-resources:SplunkDeliveryStream"}
90+
})
91+
})
92+
93+
test("it has the correct role", () => {
94+
template.hasResourceProperties("AWS::IAM::Role", {
95+
AssumeRolePolicyDocument: {
96+
Version: "2012-10-17",
97+
Statement: [{
98+
Action: "sts:AssumeRole",
99+
Effect: "Allow",
100+
Principal: {Service: "lambda.amazonaws.com"}
101+
}]
102+
},
103+
ManagedPolicyArns: Match.arrayWith([
104+
{"Fn::ImportValue": "lambda-resources:LambdaInsightsLogGroupPolicy"},
105+
{"Fn::ImportValue": "account-resources:CloudwatchEncryptionKMSPolicyArn"}
106+
])
107+
})
108+
})
109+
110+
test("it has the correct lambda", () => {
111+
template.hasResourceProperties("AWS::Lambda::Function", {
112+
Handler: "index.handler",
113+
Runtime: "python3.14",
114+
FunctionName: "testPythonLambda",
115+
MemorySize: 256,
116+
Architectures: ["x86_64"],
117+
Timeout: 50,
118+
LoggingConfig: {
119+
LogGroup: lambdaLogGroupResource
120+
},
121+
Environment: {
122+
Variables: {
123+
POWERTOOLS_LOG_LEVEL: "INFO"
124+
}
125+
},
126+
Layers: ["arn:aws:lambda:eu-west-2:580247275435:layer:LambdaInsightsExtension:64"],
127+
Role: {"Fn::GetAtt": [lambdaRoleResource.Ref, "Arn"]}
128+
})
129+
})
130+
131+
test("it has the correct policy for executing the lambda", () => {
132+
template.hasResourceProperties("AWS::IAM::ManagedPolicy", {
133+
Description: "execute lambda testPythonLambda",
134+
PolicyDocument: {
135+
Version: "2012-10-17",
136+
Statement: [{
137+
Action: "lambda:InvokeFunction",
138+
Effect: "Allow",
139+
Resource: {"Fn::GetAtt": [lambdaResource.Ref, "Arn"]}
140+
}]
141+
}
142+
})
143+
})
144+
})
145+
146+
describe("pythonFunctionConstruct works correctly with environment variables", () => {
147+
let stack: Stack
148+
let app: App
149+
let template: assertions.Template
150+
151+
beforeAll(() => {
152+
app = new App()
153+
stack = new Stack(app, "pythonLambdaConstructStack")
154+
new PythonLambdaFunction(stack, "dummyPythonFunction", {
155+
functionName: "testPythonLambda",
156+
projectBaseDir: resolve(__dirname, "../../../.."),
157+
packageBasePath: "packages/cdkConstructs",
158+
handler: "index.handler",
159+
environmentVariables: {foo: "bar"},
160+
logRetentionInDays: 30,
161+
logLevel: "DEBUG"
162+
})
163+
template = Template.fromStack(stack)
164+
})
165+
166+
test("environment variables are added correctly", () => {
167+
template.hasResourceProperties("AWS::Lambda::Function", {
168+
Runtime: "python3.14",
169+
FunctionName: "testPythonLambda",
170+
Environment: {Variables: {foo: "bar", POWERTOOLS_LOG_LEVEL: "DEBUG"}}
171+
})
172+
})
173+
})
174+
175+
describe("pythonFunctionConstruct works correctly with additional policies", () => {
176+
let stack: Stack
177+
let app: App
178+
let template: assertions.Template
179+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
180+
let testPolicyResource: any
181+
182+
beforeAll(() => {
183+
app = new App()
184+
stack = new Stack(app, "pythonLambdaConstructStack")
185+
const testPolicy = new ManagedPolicy(stack, "testPolicy", {
186+
description: "test policy",
187+
statements: [
188+
new PolicyStatement({
189+
actions: ["logs:CreateLogStream"],
190+
resources: ["*"]
191+
})
192+
]
193+
})
194+
new PythonLambdaFunction(stack, "dummyPythonFunction", {
195+
functionName: "testPythonLambda",
196+
projectBaseDir: resolve(__dirname, "../../../.."),
197+
packageBasePath: "packages/cdkConstructs",
198+
handler: "index.handler",
199+
environmentVariables: {},
200+
additionalPolicies: [testPolicy],
201+
logRetentionInDays: 30,
202+
logLevel: "INFO"
203+
})
204+
template = Template.fromStack(stack)
205+
testPolicyResource = stack.resolve(testPolicy.managedPolicyArn)
206+
})
207+
208+
test("it has the correct policies in the role", () => {
209+
template.hasResourceProperties("AWS::IAM::Role", {
210+
ManagedPolicyArns: Match.arrayWith([
211+
{"Fn::ImportValue": "lambda-resources:LambdaInsightsLogGroupPolicy"},
212+
{"Fn::ImportValue": "account-resources:CloudwatchEncryptionKMSPolicyArn"},
213+
{Ref: testPolicyResource.Ref}
214+
])
215+
})
216+
})
217+
})
218+
219+
describe("pythonFunctionConstruct works correctly with additional layers", () => {
220+
let stack: Stack
221+
let app: App
222+
let template: assertions.Template
223+
224+
beforeAll(() => {
225+
app = new App()
226+
stack = new Stack(app, "pythonLambdaConstructStack")
227+
const parameterAndSecretsLayerArn =
228+
"arn:aws:lambda:eu-west-2:133256977650:layer:AWS-Parameters-and-Secrets-Lambda-Extension:20"
229+
const parameterAndSecretsLayer = LayerVersion.fromLayerVersionArn(
230+
stack, "AdditionalLayerFromArn", parameterAndSecretsLayerArn)
231+
new PythonLambdaFunction(stack, "dummyPythonFunction", {
232+
functionName: "testPythonLambda",
233+
projectBaseDir: resolve(__dirname, "../../../.."),
234+
packageBasePath: "packages/cdkConstructs",
235+
handler: "index.handler",
236+
environmentVariables: {},
237+
logRetentionInDays: 30,
238+
logLevel: "INFO",
239+
layers: [parameterAndSecretsLayer]
240+
})
241+
template = Template.fromStack(stack)
242+
})
243+
244+
test("it has the correct layers added", () => {
245+
template.hasResourceProperties("AWS::Lambda::Function", {
246+
Layers: [
247+
"arn:aws:lambda:eu-west-2:580247275435:layer:LambdaInsightsExtension:64",
248+
"arn:aws:lambda:eu-west-2:133256977650:layer:AWS-Parameters-and-Secrets-Lambda-Extension:20"
249+
]
250+
})
251+
})
252+
})
253+
254+
describe("pythonFunctionConstruct works correctly with dependency layer", () => {
255+
let stack: Stack
256+
let app: App
257+
let template: assertions.Template
258+
259+
beforeAll(() => {
260+
app = new App()
261+
stack = new Stack(app, "pythonLambdaConstructStack")
262+
new PythonLambdaFunction(stack, "dummyPythonFunction", {
263+
functionName: "testPythonLambda",
264+
projectBaseDir: resolve(__dirname, "../../../.."),
265+
packageBasePath: "packages/cdkConstructs",
266+
dependencyLocation: "packages/cdkConstructs/tests/src",
267+
handler: "index.handler",
268+
environmentVariables: {},
269+
logRetentionInDays: 30,
270+
logLevel: "INFO"
271+
})
272+
template = Template.fromStack(stack)
273+
})
274+
275+
test("it creates a lambda layer", () => {
276+
template.hasResourceProperties("AWS::Lambda::LayerVersion", {
277+
CompatibleArchitectures: ["x86_64"]
278+
})
279+
})
280+
281+
test("it adds both insights and dependency layers", () => {
282+
template.hasResourceProperties("AWS::Lambda::Function", {
283+
Layers: Match.arrayWith([
284+
"arn:aws:lambda:eu-west-2:580247275435:layer:LambdaInsightsExtension:64",
285+
Match.objectLike({
286+
Ref: Match.stringLikeRegexp("DependencyLayer")
287+
})
288+
])
289+
})
290+
})
291+
})
292+
293+
describe("pythonFunctionConstruct works correctly with custom timeout", () => {
294+
let stack: Stack
295+
let app: App
296+
let template: assertions.Template
297+
298+
beforeAll(() => {
299+
app = new App()
300+
stack = new Stack(app, "pythonLambdaConstructStack")
301+
new PythonLambdaFunction(stack, "dummyPythonFunction", {
302+
functionName: "testPythonLambda",
303+
projectBaseDir: resolve(__dirname, "../../../.."),
304+
packageBasePath: "packages/cdkConstructs",
305+
handler: "index.handler",
306+
environmentVariables: {},
307+
logRetentionInDays: 30,
308+
logLevel: "INFO",
309+
timeoutInSeconds: 120
310+
})
311+
template = Template.fromStack(stack)
312+
})
313+
314+
test("it has the correct timeout", () => {
315+
template.hasResourceProperties("AWS::Lambda::Function", {
316+
Timeout: 120
317+
})
318+
})
319+
})
320+
321+
describe("pythonFunctionConstruct works correctly with different runtime", () => {
322+
let stack: Stack
323+
let app: App
324+
let template: assertions.Template
325+
326+
beforeAll(() => {
327+
app = new App()
328+
stack = new Stack(app, "pythonLambdaConstructStack")
329+
new PythonLambdaFunction(stack, "dummyPythonFunction", {
330+
functionName: "testPythonLambda",
331+
projectBaseDir: resolve(__dirname, "../../../.."),
332+
packageBasePath: "packages/cdkConstructs",
333+
handler: "index.handler",
334+
environmentVariables: {},
335+
logRetentionInDays: 30,
336+
logLevel: "INFO",
337+
runtime: Runtime.PYTHON_3_12
338+
})
339+
template = Template.fromStack(stack)
340+
})
341+
342+
test("it has correct runtime", () => {
343+
template.hasResourceProperties("AWS::Lambda::Function", {
344+
Runtime: "python3.12"
345+
})
346+
})
347+
})
348+
349+
describe("pythonFunctionConstruct works correctly with different architecture", () => {
350+
let stack: Stack
351+
let app: App
352+
let template: assertions.Template
353+
354+
beforeAll(() => {
355+
app = new App()
356+
stack = new Stack(app, "pythonLambdaConstructStack")
357+
new PythonLambdaFunction(stack, "dummyPythonFunction", {
358+
functionName: "testPythonLambda",
359+
projectBaseDir: resolve(__dirname, "../../../.."),
360+
packageBasePath: "packages/cdkConstructs",
361+
handler: "index.handler",
362+
environmentVariables: {},
363+
logRetentionInDays: 30,
364+
logLevel: "INFO",
365+
architecture: Architecture.ARM_64
366+
})
367+
template = Template.fromStack(stack)
368+
})
369+
370+
test("it has correct architecture and layer", () => {
371+
template.hasResourceProperties("AWS::Lambda::Function", {
372+
Architectures: ["arm64"],
373+
Layers: ["arn:aws:lambda:eu-west-2:580247275435:layer:LambdaInsightsExtension-Arm64:31"]
374+
})
375+
})
376+
})

0 commit comments

Comments
 (0)