Skip to content

Commit

Permalink
feat(cli): support hotswapping Lambda function's description and envi…
Browse files Browse the repository at this point in the history
…ronment variables

This change allows Lambda function to be hotswap'ed when there's change
in the function's description and/or environment variables. These changes
are categorized as configuration changes and are updated by calling
`updateFunctionConfiguration`. Since the existing waiter
"UpdateFunctionCodeToFinish" is now used to wait for both code update
and configuration update, I renamed it to "UpdateFunctionPropertiesToFinish".
  • Loading branch information
huyphan committed Jul 31, 2022
1 parent 0ea8a1c commit c5ebc75
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 24 deletions.
3 changes: 2 additions & 1 deletion packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,8 @@ and that you have the necessary IAM permissions to update the resources that are
Hotswapping is currently supported for the following changes
(additional changes will be supported in the future):

- Code asset (including Docker image and inline code) and tag changes of AWS Lambda functions.
- Code asset (including Docker image and inline code), tag changes, and configuration changes (only
description and environment variables are supported) of AWS Lambda functions.
- AWS Lambda Versions and Aliases changes.
- Definition changes of AWS Step Functions State Machines.
- Container asset changes of AWS ECS Services.
Expand Down
63 changes: 48 additions & 15 deletions packages/aws-cdk/lib/api/hotswap/lambda-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ async function isLambdaFunctionCodeOnlyChange(
const propertyUpdates = change.propertyUpdates;
let code: LambdaFunctionCode | undefined = undefined;
let tags: LambdaFunctionTags | undefined = undefined;
let description: string | undefined = undefined;
let environment: { [key: string]: string } | undefined = undefined;

for (const updatedPropName in propertyUpdates) {
const updatedProp = propertyUpdates[updatedPropName];
Expand Down Expand Up @@ -175,12 +177,19 @@ async function isLambdaFunctionCodeOnlyChange(
tags = { tagUpdates };
}
break;
case 'Description':
description = updatedProp.newValue;
break;
case 'Environment':
environment = updatedProp.newValue;
break;
default:
return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT;
}
}

return code || tags ? { code, tags } : ChangeHotswapImpact.IRRELEVANT;
const configurations = description || environment ? { description, environment } : undefined;
return code || tags || configurations ? { code, tags, configurations } : ChangeHotswapImpact.IRRELEVANT;
}

interface CfnDiffTagValue {
Expand All @@ -203,9 +212,15 @@ interface LambdaFunctionTags {
readonly tagUpdates: { [tag : string] : string | TagDeletion };
}

interface LambdaFunctionConfigurations {
readonly description?: string;
readonly environment?: { [key: string]: string };
}

interface LambdaFunctionChange {
readonly code?: LambdaFunctionCode;
readonly tags?: LambdaFunctionTags;
readonly configurations?: LambdaFunctionConfigurations;
}

interface LambdaFunctionResource {
Expand Down Expand Up @@ -235,16 +250,32 @@ class LambdaFunctionHotswapOperation implements HotswapOperation {
const resource = this.lambdaFunctionResource.resource;
const operations: Promise<any>[] = [];

if (resource.code !== undefined) {
const updateFunctionCodeResponse = await lambda.updateFunctionCode({
FunctionName: this.lambdaFunctionResource.physicalName,
S3Bucket: resource.code.s3Bucket,
S3Key: resource.code.s3Key,
ImageUri: resource.code.imageUri,
ZipFile: resource.code.functionCodeZip,
}).promise();
if (resource.code !== undefined || resource.configurations !== undefined) {
if (resource.code !== undefined) {
const updateFunctionCodeResponse = await lambda.updateFunctionCode({
FunctionName: this.lambdaFunctionResource.physicalName,
S3Bucket: resource.code.s3Bucket,
S3Key: resource.code.s3Key,
ImageUri: resource.code.imageUri,
ZipFile: resource.code.functionCodeZip,
}).promise();

await this.waitForLambdasPropertiesUpdateToFinish(updateFunctionCodeResponse, lambda);
}

await this.waitForLambdasCodeUpdateToFinish(updateFunctionCodeResponse, lambda);
if (resource.configurations !== undefined) {
const updateRequest: AWS.Lambda.UpdateFunctionConfigurationRequest = {
FunctionName: this.lambdaFunctionResource.physicalName,
};
if (resource.configurations.description !== undefined) {
updateRequest.Description = resource.configurations.description;
}
if (resource.configurations.environment !== undefined) {
updateRequest.Environment = resource.configurations.environment;
}
const updateFunctionCodeResponse = await lambda.updateFunctionConfiguration(updateRequest).promise();
await this.waitForLambdasPropertiesUpdateToFinish(updateFunctionCodeResponse, lambda);
}

// only if the code changed is there any point in publishing a new Version
if (this.lambdaFunctionResource.publishVersion) {
Expand Down Expand Up @@ -308,7 +339,9 @@ class LambdaFunctionHotswapOperation implements HotswapOperation {
* or very slowly. For example, Zip based functions _not_ in a VPC can take ~1 second whereas VPC
* or Container functions can take ~25 seconds (and 'idle' VPC functions can take minutes).
*/
private async waitForLambdasCodeUpdateToFinish(currentFunctionConfiguration: AWS.Lambda.FunctionConfiguration, lambda: AWS.Lambda): Promise<void> {
private async waitForLambdasPropertiesUpdateToFinish(
currentFunctionConfiguration: AWS.Lambda.FunctionConfiguration, lambda: AWS.Lambda,
): Promise<void> {
const functionIsInVpcOrUsesDockerForCode = currentFunctionConfiguration.VpcConfig?.VpcId ||
currentFunctionConfiguration.PackageType === 'Image';

Expand All @@ -318,8 +351,8 @@ class LambdaFunctionHotswapOperation implements HotswapOperation {
const delaySeconds = functionIsInVpcOrUsesDockerForCode ? 5 : 1;

// configure a custom waiter to wait for the function update to complete
(lambda as any).api.waiters.updateFunctionCodeToFinish = {
name: 'UpdateFunctionCodeToFinish',
(lambda as any).api.waiters.updateFunctionPropertiesToFinish = {
name: 'UpdateFunctionPropertiesToFinish',
operation: 'getFunction',
// equates to 1 minute for zip function not in a VPC and
// 5 minutes for container functions or function in a VPC
Expand All @@ -341,8 +374,8 @@ class LambdaFunctionHotswapOperation implements HotswapOperation {
],
};

const updateFunctionCodeWaiter = new (AWS as any).ResourceWaiter(lambda, 'updateFunctionCodeToFinish');
await updateFunctionCodeWaiter.wait({
const updateFunctionPropertiesWaiter = new (AWS as any).ResourceWaiter(lambda, 'updateFunctionPropertiesToFinish');
await updateFunctionPropertiesWaiter.wait({
FunctionName: this.lambdaFunctionResource.physicalName,
}).promise();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ test('calls the getFunction() API with a delay of 5', async () => {
// THEN
expect(mockMakeRequest).toHaveBeenCalledWith('getFunction', { FunctionName: 'my-function' });
expect(hotswapMockSdkProvider.getLambdaApiWaiters()).toEqual(expect.objectContaining({
updateFunctionCodeToFinish: expect.objectContaining({
name: 'UpdateFunctionCodeToFinish',
updateFunctionPropertiesToFinish: expect.objectContaining({
name: 'UpdateFunctionPropertiesToFinish',
delay: 5,
}),
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { Lambda } from 'aws-sdk';
import * as setup from './hotswap-test-setup';

let mockUpdateLambdaCode: (params: Lambda.Types.UpdateFunctionCodeRequest) => Lambda.Types.FunctionConfiguration;
let mockUpdateLambdaConfiguration: (
params: Lambda.Types.UpdateFunctionConfigurationRequest
) => Lambda.Types.FunctionConfiguration;
let mockTagResource: (params: Lambda.Types.TagResourceRequest) => {};
let mockUntagResource: (params: Lambda.Types.UntagResourceRequest) => {};
let mockMakeRequest: (operation: string, params: any) => AWS.Request<any, AWS.AWSError>;
Expand All @@ -10,6 +13,7 @@ let hotswapMockSdkProvider: setup.HotswapMockSdkProvider;
beforeEach(() => {
hotswapMockSdkProvider = setup.setupHotswapTests();
mockUpdateLambdaCode = jest.fn().mockReturnValue({});
mockUpdateLambdaConfiguration = jest.fn().mockReturnValue({});
mockTagResource = jest.fn();
mockUntagResource = jest.fn();
mockMakeRequest = jest.fn().mockReturnValue({
Expand All @@ -19,6 +23,7 @@ beforeEach(() => {
});
hotswapMockSdkProvider.stubLambda({
updateFunctionCode: mockUpdateLambdaCode,
updateFunctionConfiguration: mockUpdateLambdaConfiguration,
tagResource: mockTagResource,
untagResource: mockUntagResource,
}, {
Expand Down Expand Up @@ -593,8 +598,8 @@ test('calls getFunction() after function code is updated with delay 1', async ()
// THEN
expect(mockMakeRequest).toHaveBeenCalledWith('getFunction', { FunctionName: 'my-function' });
expect(hotswapMockSdkProvider.getLambdaApiWaiters()).toEqual(expect.objectContaining({
updateFunctionCodeToFinish: expect.objectContaining({
name: 'UpdateFunctionCodeToFinish',
updateFunctionPropertiesToFinish: expect.objectContaining({
name: 'UpdateFunctionPropertiesToFinish',
delay: 1,
}),
}));
Expand Down Expand Up @@ -654,8 +659,8 @@ test('calls getFunction() after function code is updated and VpcId is empty stri

// THEN
expect(hotswapMockSdkProvider.getLambdaApiWaiters()).toEqual(expect.objectContaining({
updateFunctionCodeToFinish: expect.objectContaining({
name: 'UpdateFunctionCodeToFinish',
updateFunctionPropertiesToFinish: expect.objectContaining({
name: 'UpdateFunctionPropertiesToFinish',
delay: 1,
}),
}));
Expand Down Expand Up @@ -715,9 +720,188 @@ test('calls getFunction() after function code is updated on a VPC function with

// THEN
expect(hotswapMockSdkProvider.getLambdaApiWaiters()).toEqual(expect.objectContaining({
updateFunctionCodeToFinish: expect.objectContaining({
name: 'UpdateFunctionCodeToFinish',
updateFunctionPropertiesToFinish: expect.objectContaining({
name: 'UpdateFunctionPropertiesToFinish',
delay: 5,
}),
}));
});


test('calls the updateLambdaConfiguration() API when it receives difference in Description field of a Lambda function', async () => {
// GIVEN
setup.setCurrentCfnStackTemplate({
Resources: {
Func: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: 's3-bucket',
S3Key: 's3-key',
},
FunctionName: 'my-function',
Description: 'Old Description',
},
Metadata: {
'aws:asset:path': 'asset-path',
},
},
},
});
const cdkStackArtifact = setup.cdkStackArtifactOf({
template: {
Resources: {
Func: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: 's3-bucket',
S3Key: 's3-key',
},
FunctionName: 'my-function',
Description: 'New Description',
},
Metadata: {
'aws:asset:path': 'asset-path',
},
},
},
},
});

// WHEN
const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact);

// THEN
expect(deployStackResult).not.toBeUndefined();
expect(mockUpdateLambdaConfiguration).toHaveBeenCalledWith({
FunctionName: 'my-function',
Description: 'New Description',
});
});

test('calls the updateLambdaConfiguration() API when it receives difference in Environment field of a Lambda function', async () => {
// GIVEN
setup.setCurrentCfnStackTemplate({
Resources: {
Func: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: 's3-bucket',
S3Key: 's3-key',
},
FunctionName: 'my-function',
Environment: {
Variables: {
Key1: 'Value1',
Key2: 'Value2',
},
},
},
Metadata: {
'aws:asset:path': 'asset-path',
},
},
},
});
const cdkStackArtifact = setup.cdkStackArtifactOf({
template: {
Resources: {
Func: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: 's3-bucket',
S3Key: 's3-key',
},
FunctionName: 'my-function',
Environment: {
Variables: {
Key1: 'Value1',
Key2: 'Value2',
NewKey: 'NewValue',
},
},
},
Metadata: {
'aws:asset:path': 'asset-path',
},
},
},
},
});

// WHEN
const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact);

// THEN
expect(deployStackResult).not.toBeUndefined();
expect(mockUpdateLambdaConfiguration).toHaveBeenCalledWith({
FunctionName: 'my-function',
Environment: {
Variables: {
Key1: 'Value1',
Key2: 'Value2',
NewKey: 'NewValue',
},
},
});
});

test('calls both updateLambdaCode() and updateLambdaConfiguration() API when it receives both code and configuration change', async () => {
// GIVEN
setup.setCurrentCfnStackTemplate({
Resources: {
Func: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: 'current-bucket',
S3Key: 'current-key',
},
FunctionName: 'my-function',
Description: 'Old Description',
},
Metadata: {
'aws:asset:path': 'asset-path',
},
},
},
});
const cdkStackArtifact = setup.cdkStackArtifactOf({
template: {
Resources: {
Func: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: 'new-bucket',
S3Key: 'new-key',
},
FunctionName: 'my-function',
Description: 'New Description',
},
Metadata: {
'aws:asset:path': 'asset-path',
},
},
},
},
});

// WHEN
const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact);

// THEN
expect(deployStackResult).not.toBeUndefined();
expect(mockUpdateLambdaConfiguration).toHaveBeenCalledWith({
FunctionName: 'my-function',
Description: 'New Description',
});
expect(mockUpdateLambdaCode).toHaveBeenCalledWith({
FunctionName: 'my-function',
S3Bucket: 'new-bucket',
S3Key: 'new-key',
});
});

0 comments on commit c5ebc75

Please sign in to comment.