Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): support hotswapping Lambda function's description and environment variables #21532

Merged
merged 5 commits into from
Aug 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,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',
});
});
Loading