Skip to content

Commit d0b8512

Browse files
authored
feat(cli): support hotswapping Lambda functions with inline code (#18408)
Similarly to #18319, this PR supports hotswap of Lambda functions that use `InlineCode`. `InlineCode` uses [CloudFormation `ZipFile` feature](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#:~:text=requires%3A%20No%20interruption-,ZipFile,-\(Node.js%20and). To emulate its behavior, we create a zip file of the provided inline code with its filename `index.js` or `index.py` according to the runtime (CFn only supports python or nodejs for ZipFile), and pass the file's binary buffer to Lambda API. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 2b0b5ea commit d0b8512

File tree

3 files changed

+220
-3
lines changed

3 files changed

+220
-3
lines changed

packages/aws-cdk/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ and that you have the necessary IAM permissions to update the resources that are
362362
Hotswapping is currently supported for the following changes
363363
(additional changes will be supported in the future):
364364

365-
- Code asset (including Docker image) and tag changes of AWS Lambda functions.
365+
- Code asset (including Docker image and inline code) and tag changes of AWS Lambda functions.
366366
- AWS Lambda Versions and Aliases changes.
367367
- Definition changes of AWS Step Functions State Machines.
368368
- Container asset changes of AWS ECS Services.

packages/aws-cdk/lib/api/hotswap/lambda-functions.ts

+71-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { Writable } from 'stream';
2+
import * as archiver from 'archiver';
13
import { flatMap } from '../../util';
24
import { ISDK } from '../aws-auth';
3-
import { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template';
5+
import { CfnEvaluationException, EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template';
46
import { ChangeHotswapImpact, ChangeHotswapResult, HotswapOperation, HotswappableChangeCandidate } from './common';
57

68
/**
@@ -108,7 +110,7 @@ async function isLambdaFunctionCodeOnlyChange(
108110
switch (updatedPropName) {
109111
case 'Code':
110112
let foundCodeDifference = false;
111-
let s3Bucket, s3Key, imageUri;
113+
let s3Bucket, s3Key, imageUri, functionCodeZip;
112114

113115
for (const newPropName in updatedProp.newValue) {
114116
switch (newPropName) {
@@ -124,6 +126,18 @@ async function isLambdaFunctionCodeOnlyChange(
124126
foundCodeDifference = true;
125127
imageUri = await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue[newPropName]);
126128
break;
129+
case 'ZipFile':
130+
foundCodeDifference = true;
131+
// We must create a zip package containing a file with the inline code
132+
const functionCode = await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue[newPropName]);
133+
const functionRuntime = await evaluateCfnTemplate.evaluateCfnExpression(change.newValue.Properties?.Runtime);
134+
if (!functionRuntime) {
135+
return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT;
136+
}
137+
// file extension must be chosen depending on the runtime
138+
const codeFileExt = determineCodeFileExtFromRuntime(functionRuntime);
139+
functionCodeZip = await zipString(`index.${codeFileExt}`, functionCode);
140+
break;
127141
default:
128142
return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT;
129143
}
@@ -133,6 +147,7 @@ async function isLambdaFunctionCodeOnlyChange(
133147
s3Bucket,
134148
s3Key,
135149
imageUri,
150+
functionCodeZip,
136151
};
137152
}
138153
break;
@@ -173,6 +188,7 @@ interface LambdaFunctionCode {
173188
readonly s3Bucket?: string;
174189
readonly s3Key?: string;
175190
readonly imageUri?: string;
191+
readonly functionCodeZip?: Buffer;
176192
}
177193

178194
enum TagDeletion {
@@ -221,6 +237,7 @@ class LambdaFunctionHotswapOperation implements HotswapOperation {
221237
S3Bucket: resource.code.s3Bucket,
222238
S3Key: resource.code.s3Key,
223239
ImageUri: resource.code.imageUri,
240+
ZipFile: resource.code.functionCodeZip,
224241
}).promise();
225242

226243
// only if the code changed is there any point in publishing a new Version
@@ -288,3 +305,55 @@ class LambdaFunctionHotswapOperation implements HotswapOperation {
288305
return Promise.all(operations);
289306
}
290307
}
308+
309+
/**
310+
* Compress a string as a file, returning a promise for the zip buffer
311+
* https://github.com/archiverjs/node-archiver/issues/342
312+
*/
313+
function zipString(fileName: string, rawString: string): Promise<Buffer> {
314+
return new Promise((resolve, reject) => {
315+
const buffers: Buffer[] = [];
316+
317+
const converter = new Writable();
318+
319+
converter._write = (chunk: Buffer, _: string, callback: () => void) => {
320+
buffers.push(chunk);
321+
process.nextTick(callback);
322+
};
323+
324+
converter.on('finish', () => {
325+
resolve(Buffer.concat(buffers));
326+
});
327+
328+
const archive = archiver('zip');
329+
330+
archive.on('error', (err) => {
331+
reject(err);
332+
});
333+
334+
archive.pipe(converter);
335+
336+
archive.append(rawString, {
337+
name: fileName,
338+
date: new Date('1980-01-01T00:00:00.000Z'), // Add date to make resulting zip file deterministic
339+
});
340+
341+
void archive.finalize();
342+
});
343+
}
344+
345+
/**
346+
* Get file extension from Lambda runtime string.
347+
* We use this extension to create a deployment package from Lambda inline code.
348+
*/
349+
function determineCodeFileExtFromRuntime(runtime: string): string {
350+
if (runtime.startsWith('node')) {
351+
return 'js';
352+
}
353+
if (runtime.startsWith('python')) {
354+
return 'py';
355+
}
356+
// Currently inline code only supports Node.js and Python, ignoring other runtimes.
357+
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#aws-properties-lambda-function-code-properties
358+
throw new CfnEvaluationException(`runtime ${runtime} is unsupported, only node.js and python runtimes are currently supported.`);
359+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { Lambda } from 'aws-sdk';
2+
import * as setup from './hotswap-test-setup';
3+
4+
let mockUpdateLambdaCode: (params: Lambda.Types.UpdateFunctionCodeRequest) => Lambda.Types.FunctionConfiguration;
5+
let mockTagResource: (params: Lambda.Types.TagResourceRequest) => {};
6+
let mockUntagResource: (params: Lambda.Types.UntagResourceRequest) => {};
7+
let hotswapMockSdkProvider: setup.HotswapMockSdkProvider;
8+
9+
beforeEach(() => {
10+
hotswapMockSdkProvider = setup.setupHotswapTests();
11+
mockUpdateLambdaCode = jest.fn();
12+
mockTagResource = jest.fn();
13+
mockUntagResource = jest.fn();
14+
hotswapMockSdkProvider.stubLambda({
15+
updateFunctionCode: mockUpdateLambdaCode,
16+
tagResource: mockTagResource,
17+
untagResource: mockUntagResource,
18+
});
19+
});
20+
21+
test('calls the updateLambdaCode() API when it receives only a code difference in a Lambda function (Inline Node.js code)', async () => {
22+
// GIVEN
23+
setup.setCurrentCfnStackTemplate({
24+
Resources: {
25+
Func: {
26+
Type: 'AWS::Lambda::Function',
27+
Properties: {
28+
Code: {
29+
ZipFile: 'exports.handler = () => {return true}',
30+
},
31+
Runtime: 'nodejs14.x',
32+
FunctionName: 'my-function',
33+
},
34+
},
35+
},
36+
});
37+
const newCode = 'exports.handler = () => {return false}';
38+
const cdkStackArtifact = setup.cdkStackArtifactOf({
39+
template: {
40+
Resources: {
41+
Func: {
42+
Type: 'AWS::Lambda::Function',
43+
Properties: {
44+
Code: {
45+
ZipFile: newCode,
46+
},
47+
Runtime: 'nodejs14.x',
48+
FunctionName: 'my-function',
49+
},
50+
},
51+
},
52+
},
53+
});
54+
55+
// WHEN
56+
const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact);
57+
58+
// THEN
59+
expect(deployStackResult).not.toBeUndefined();
60+
expect(mockUpdateLambdaCode).toHaveBeenCalledWith({
61+
FunctionName: 'my-function',
62+
ZipFile: expect.any(Buffer),
63+
});
64+
});
65+
66+
test('calls the updateLambdaCode() API when it receives only a code difference in a Lambda function (Inline Python code)', async () => {
67+
// GIVEN
68+
setup.setCurrentCfnStackTemplate({
69+
Resources: {
70+
Func: {
71+
Type: 'AWS::Lambda::Function',
72+
Properties: {
73+
Code: {
74+
ZipFile: 'def handler(event, context):\n return True',
75+
},
76+
Runtime: 'python3.9',
77+
FunctionName: 'my-function',
78+
},
79+
},
80+
},
81+
});
82+
const cdkStackArtifact = setup.cdkStackArtifactOf({
83+
template: {
84+
Resources: {
85+
Func: {
86+
Type: 'AWS::Lambda::Function',
87+
Properties: {
88+
Code: {
89+
ZipFile: 'def handler(event, context):\n return False',
90+
},
91+
Runtime: 'python3.9',
92+
FunctionName: 'my-function',
93+
},
94+
},
95+
},
96+
},
97+
});
98+
99+
// WHEN
100+
const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact);
101+
102+
// THEN
103+
expect(deployStackResult).not.toBeUndefined();
104+
expect(mockUpdateLambdaCode).toHaveBeenCalledWith({
105+
FunctionName: 'my-function',
106+
ZipFile: expect.any(Buffer),
107+
});
108+
});
109+
110+
test('throw a CfnEvaluationException when it receives an unsupported function runtime', async () => {
111+
// GIVEN
112+
setup.setCurrentCfnStackTemplate({
113+
Resources: {
114+
Func: {
115+
Type: 'AWS::Lambda::Function',
116+
Properties: {
117+
Code: {
118+
ZipFile: 'def handler(event:, context:) true end',
119+
},
120+
Runtime: 'ruby2.7',
121+
FunctionName: 'my-function',
122+
},
123+
},
124+
},
125+
});
126+
const cdkStackArtifact = setup.cdkStackArtifactOf({
127+
template: {
128+
Resources: {
129+
Func: {
130+
Type: 'AWS::Lambda::Function',
131+
Properties: {
132+
Code: {
133+
ZipFile: 'def handler(event:, context:) false end',
134+
},
135+
Runtime: 'ruby2.7',
136+
FunctionName: 'my-function',
137+
},
138+
},
139+
},
140+
},
141+
});
142+
143+
// WHEN
144+
const tryHotswap = hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact);
145+
146+
// THEN
147+
await expect(tryHotswap).rejects.toThrow('runtime ruby2.7 is unsupported, only node.js and python runtimes are currently supported.');
148+
});

0 commit comments

Comments
 (0)