Skip to content

Commit 0446e4a

Browse files
authored
fix(lambda): use of currentVersion fails deployment after upgrade (#26777)
Between version `2.87.0` and version `2.88.0`, the hash calculation used to make sure that `fn.currentVersion` is automatically updated when a new version of the Lambda Function is deployed changed. This causes a creation of a new Version upon upgrading CDK, but that new Version creation will fail because the underlying Function hasn't changed. The change was due to property ordering used in calculating a unique hash for the Function configuration. This change restores the property ordering to the pre-2.88.0 behavior. Fixes #26739. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 6d315b8 commit 0446e4a

File tree

3 files changed

+155
-44
lines changed

3 files changed

+155
-44
lines changed

packages/aws-cdk-lib/aws-lambda/lib/function-hash.ts

+92-44
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,16 @@ export function calculateFunctionHash(fn: LambdaFunction, additional: string = '
88
const stack = Stack.of(fn);
99

1010
const functionResource = fn.node.defaultChild as CfnResource;
11-
12-
// render the cloudformation resource from this function
13-
const config = stack.resolve((functionResource as any)._toCloudFormation());
14-
// config is of the shape: { Resources: { LogicalId: { Type: 'Function', Properties: { ... } }}}
15-
const resources = config.Resources;
16-
const resourceKeys = Object.keys(resources);
17-
if (resourceKeys.length !== 1) {
18-
throw new Error(`Expected one rendered CloudFormation resource but found ${resourceKeys.length}`);
19-
}
20-
const logicalId = resourceKeys[0];
21-
const properties = resources[logicalId].Properties;
11+
const { properties, template, logicalId } = resolveSingleResourceProperties(stack, functionResource);
2212

2313
let stringifiedConfig;
2414
if (FeatureFlags.of(fn).isEnabled(LAMBDA_RECOGNIZE_VERSION_PROPS)) {
25-
const updatedProps = sortProperties(filterUsefulKeys(properties));
15+
const updatedProps = sortFunctionProperties(filterUsefulKeys(properties));
2616
stringifiedConfig = JSON.stringify(updatedProps);
2717
} else {
28-
const sorted = sortProperties(properties);
29-
config.Resources[logicalId].Properties = sorted;
30-
stringifiedConfig = JSON.stringify(config);
18+
const sorted = sortFunctionProperties(properties);
19+
template.Resources[logicalId].Properties = sorted;
20+
stringifiedConfig = JSON.stringify(template);
3121
}
3222

3323
if (FeatureFlags.of(fn).isEnabled(LAMBDA_RECOGNIZE_LAYER_VERSION)) {
@@ -103,26 +93,6 @@ function filterUsefulKeys(properties: any) {
10393
return ret;
10494
}
10595

106-
function sortProperties(properties: any) {
107-
const ret: any = {};
108-
// We take all required properties in the order that they were historically,
109-
// to make sure the hash we calculate is stable.
110-
// There cannot be more required properties added in the future,
111-
// as that would be a backwards-incompatible change.
112-
const requiredProperties = ['Code', 'Handler', 'Role', 'Runtime'];
113-
for (const requiredProperty of requiredProperties) {
114-
ret[requiredProperty] = properties[requiredProperty];
115-
}
116-
// then, add all of the non-required properties,
117-
// in the original order
118-
for (const property of Object.keys(properties)) {
119-
if (requiredProperties.indexOf(property) === -1) {
120-
ret[property] = properties[property];
121-
}
122-
}
123-
return ret;
124-
}
125-
12696
function calculateLayersHash(layers: ILayerVersion[]): string {
12797
const layerConfig: {[key: string]: any } = {};
12898
for (const layer of layers) {
@@ -143,17 +113,95 @@ function calculateLayersHash(layers: ILayerVersion[]): string {
143113
}
144114
continue;
145115
}
146-
const config = stack.resolve((layerResource as any)._toCloudFormation());
147-
const resources = config.Resources;
148-
const resourceKeys = Object.keys(resources);
149-
if (resourceKeys.length !== 1) {
150-
throw new Error(`Expected one rendered CloudFormation resource but found ${resourceKeys.length}`);
151-
}
152-
const logicalId = resourceKeys[0];
153-
const properties = resources[logicalId].Properties;
116+
117+
const { properties } = resolveSingleResourceProperties(stack, layerResource);
118+
154119
// all properties require replacement, so they are all version locked.
155-
layerConfig[layer.node.id] = properties;
120+
layerConfig[layer.node.id] = sortLayerVersionProperties(properties);
156121
}
157122

158123
return md5hash(JSON.stringify(layerConfig));
159124
}
125+
126+
/**
127+
* Sort properties in an object according to a sort order of known keys
128+
*
129+
* Any additional keys are added at the end, but also sorted.
130+
*
131+
* We only sort one level deep, because we rely on the fact that everything
132+
* that needs to be sorted happens to be sorted by the codegen already, and
133+
* we explicitly rely on some objects NOT being sorted.
134+
*/
135+
class PropertySort {
136+
constructor(private readonly knownKeysOrder: string[]) {
137+
}
138+
139+
public sortObject(properties: any): any {
140+
const ret: any = {};
141+
142+
// Scratch-off set for keys we don't know about yet
143+
const unusedKeys = new Set(Object.keys(properties));
144+
for (const prop of this.knownKeysOrder) {
145+
ret[prop] = properties[prop];
146+
unusedKeys.delete(prop);
147+
}
148+
149+
for (const prop of Array.from(unusedKeys).sort()) {
150+
ret[prop] = properties[prop];
151+
}
152+
153+
return ret;
154+
}
155+
}
156+
157+
/**
158+
* Sort properties in a stable order, even as we switch to new codegen
159+
*
160+
* <=2.87.0, we used to generate properties in the order that they occurred in
161+
* the CloudFormation spec. >= 2.88.0, we switched to a new spec source, which
162+
* sorts the properties lexicographically. The order change changed the hash,
163+
* even though the properties themselves have not changed.
164+
*
165+
* We now have a set of properties with the sort order <=2.87.0, and add any
166+
* additional properties later on, but also sort them.
167+
*
168+
* We should be making sure that the orderings for all subobjects
169+
* between 2.87.0 and 2.88.0 are the same, but fortunately all the subobjects
170+
* were already in lexicographic order in <=2.87.0 so we only need to sort some
171+
* top-level properties on the resource.
172+
*
173+
* We also can't deep-sort everything, because for backwards compatibility
174+
* reasons we have a test that ensures that environment variables are not
175+
* lexicographically sorted, but emitted in the order they are added in source
176+
* code, so for now we rely on the codegen being lexicographically sorted.
177+
*/
178+
function sortFunctionProperties(properties: any) {
179+
return new PropertySort([
180+
// <= 2.87 explicitly fixed order
181+
'Code', 'Handler', 'Role', 'Runtime',
182+
// <= 2.87 implicitly fixed order
183+
'Architectures', 'CodeSigningConfigArn', 'DeadLetterConfig', 'Description', 'Environment',
184+
'EphemeralStorage', 'FileSystemConfigs', 'FunctionName', 'ImageConfig', 'KmsKeyArn', 'Layers',
185+
'MemorySize', 'PackageType', 'ReservedConcurrentExecutions', 'RuntimeManagementConfig', 'SnapStart',
186+
'Tags', 'Timeout', 'TracingConfig', 'VpcConfig',
187+
]).sortObject(properties);
188+
}
189+
190+
function sortLayerVersionProperties(properties: any) {
191+
return new PropertySort([
192+
// <=2.87.0 implicit sort order
193+
'Content', 'CompatibleArchitectures', 'CompatibleRuntimes', 'Description',
194+
'LayerName', 'LicenseInfo',
195+
]).sortObject(properties);
196+
}
197+
198+
function resolveSingleResourceProperties(stack: Stack, res: CfnResource): any {
199+
const template = stack.resolve(res._toCloudFormation());
200+
const resources = template.Resources;
201+
const resourceKeys = Object.keys(resources);
202+
if (resourceKeys.length !== 1) {
203+
throw new Error(`Expected one rendered CloudFormation resource but found ${resourceKeys.length}`);
204+
}
205+
const logicalId = resourceKeys[0];
206+
return { properties: resources[logicalId].Properties, template, logicalId };
207+
}

packages/aws-cdk-lib/aws-lambda/test/function.test.ts

+62
Original file line numberDiff line numberDiff line change
@@ -3246,6 +3246,68 @@ test('set SnapStart to desired value', () => {
32463246
});
32473247
});
32483248

3249+
test('test 2.87.0 version hash stability', () => {
3250+
// GIVEN
3251+
const app = new cdk.App({
3252+
context: {
3253+
'@aws-cdk/aws-lambda:recognizeLayerVersion': true,
3254+
},
3255+
});
3256+
const stack = new cdk.Stack(app, 'Stack');
3257+
3258+
// WHEN
3259+
const layer = new lambda.LayerVersion(stack, 'MyLayer', {
3260+
code: lambda.Code.fromAsset(path.join(__dirname, 'x.zip')),
3261+
compatibleRuntimes: [
3262+
lambda.Runtime.NODEJS_18_X,
3263+
],
3264+
});
3265+
3266+
const role = new iam.Role(stack, 'MyRole', {
3267+
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
3268+
managedPolicies: [
3269+
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
3270+
iam.ManagedPolicy.fromAwsManagedPolicyName('AWSXRayDaemonWriteAccess'),
3271+
],
3272+
});
3273+
3274+
const lambdaFn = new lambda.Function(stack, 'MyLambda', {
3275+
runtime: lambda.Runtime.NODEJS_18_X,
3276+
memorySize: 128,
3277+
handler: 'index.handler',
3278+
timeout: cdk.Duration.seconds(30),
3279+
environment: {
3280+
VARIABLE_1: 'ONE',
3281+
},
3282+
code: lambda.Code.fromAsset(path.join(__dirname, 'x.zip')),
3283+
role,
3284+
currentVersionOptions: {
3285+
removalPolicy: cdk.RemovalPolicy.RETAIN,
3286+
},
3287+
layers: [
3288+
layer,
3289+
],
3290+
});
3291+
3292+
new lambda.Alias(stack, 'MyAlias', {
3293+
aliasName: 'current',
3294+
version: lambdaFn.currentVersion,
3295+
});
3296+
3297+
// THEN
3298+
// Precalculated version hash using 2.87.0 version
3299+
Template.fromStack(stack).hasResource('AWS::Lambda::Alias', {
3300+
Properties: {
3301+
FunctionVersion: {
3302+
'Fn::GetAtt': [
3303+
'MyLambdaCurrentVersionE7A382CCd55a48b26bd9a860d8842137f2243c37',
3304+
'Version',
3305+
],
3306+
},
3307+
},
3308+
});
3309+
});
3310+
32493311
function newTestLambda(scope: constructs.Construct) {
32503312
return new lambda.Function(scope, 'MyLambda', {
32513313
code: new lambda.InlineCode('foo'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
x

0 commit comments

Comments
 (0)