Skip to content

Commit

Permalink
fix(lambda): make the Version hash calculation stable (#12364)
Browse files Browse the repository at this point in the history
In a recent CloudFormation specification update, the `Handler` and `Runtime` properties of the `AWS::Lambda::Function` resource were made optional.
This resulted in the hash calculation of the current Version to change,
as it uses the stringified output of the resource JSON,
and our L1 code generation sorts properties alphabetically,
but the required ones always first -
the resulting order change of the keys resulted in a different hash.
The quick fix for that was to amend the specification to keep those two properties required,
but of course that has an adverse effect on our L1 usability.

Make the hash calculation stable with regards to optional properties by sorting the historically required ones first.
This is future-proof, as there will never be more required properties added to the Function resource
(as that would be a backwards-incompatible change).


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
skinny85 authored Jan 6, 2021
1 parent bf7e92d commit 4da50e5
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@
},
"/",
{
"Ref": "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3Bucket1DDC9C52"
"Ref": "AssetParameters91169429f2b5b85501c7b1b9d7beeb80c9bb6f4891f4e600fcaf65a8817ce0f4S3Bucket06F505B9"
},
"/",
{
Expand All @@ -168,7 +168,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3VersionKey2B4F31C1"
"Ref": "AssetParameters91169429f2b5b85501c7b1b9d7beeb80c9bb6f4891f4e600fcaf65a8817ce0f4S3VersionKey06BEDE93"
}
]
}
Expand All @@ -181,7 +181,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3VersionKey2B4F31C1"
"Ref": "AssetParameters91169429f2b5b85501c7b1b9d7beeb80c9bb6f4891f4e600fcaf65a8817ce0f4S3VersionKey06BEDE93"
}
]
}
Expand Down Expand Up @@ -254,17 +254,17 @@
}
},
"Parameters": {
"AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3Bucket1DDC9C52": {
"AssetParameters91169429f2b5b85501c7b1b9d7beeb80c9bb6f4891f4e600fcaf65a8817ce0f4S3Bucket06F505B9": {
"Type": "String",
"Description": "S3 bucket for asset \"4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1a\""
"Description": "S3 bucket for asset \"91169429f2b5b85501c7b1b9d7beeb80c9bb6f4891f4e600fcaf65a8817ce0f4\""
},
"AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3VersionKey2B4F31C1": {
"AssetParameters91169429f2b5b85501c7b1b9d7beeb80c9bb6f4891f4e600fcaf65a8817ce0f4S3VersionKey06BEDE93": {
"Type": "String",
"Description": "S3 key for asset version \"4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1a\""
"Description": "S3 key for asset version \"91169429f2b5b85501c7b1b9d7beeb80c9bb6f4891f4e600fcaf65a8817ce0f4\""
},
"AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aArtifactHash3AA59378": {
"AssetParameters91169429f2b5b85501c7b1b9d7beeb80c9bb6f4891f4e600fcaf65a8817ce0f4ArtifactHash407EE1C2": {
"Type": "String",
"Description": "Artifact hash for asset \"4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1a\""
"Description": "Artifact hash for asset \"91169429f2b5b85501c7b1b9d7beeb80c9bb6f4891f4e600fcaf65a8817ce0f4\""
},
"AssetParameters0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22dfS3BucketB322F951": {
"Type": "String",
Expand Down
18 changes: 3 additions & 15 deletions packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import * as crypto from 'crypto';
import * as path from 'path';
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as iam from '@aws-cdk/aws-iam';
import * as lambda from '@aws-cdk/aws-lambda';
// hack, as this is not exported by the Lambda module
import { calculateFunctionHash } from '@aws-cdk/aws-lambda/lib/function-hash';
import * as ssm from '@aws-cdk/aws-ssm';
import {
CfnResource, ConstructNode,
ConstructNode,
CustomResource, CustomResourceProvider, CustomResourceProviderRuntime,
Resource, Stack, Stage, Token,
} from '@aws-cdk/core';
Expand Down Expand Up @@ -241,16 +242,3 @@ function addEdgeLambdaToRoleTrustStatement(role: iam.IRole) {
role.assumeRolePolicy.addStatements(statement);
}
}

// Stolen from @aws-lambda/lib/function-hash.ts, which isn't currently exported.
// This should be DRY'ed up (exported by @aws-lambda) before this is marked as stable.
function calculateFunctionHash(fn: lambda.Function) {
const stack = Stack.of(fn);
const functionResource = fn.node.defaultChild as CfnResource;
// render the cloudformation resource from this function
const config = stack.resolve((functionResource as any)._toCloudFormation());

const hash = crypto.createHash('md5');
hash.update(JSON.stringify(config));
return hash.digest('hex');
}
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@
},
"/",
{
"Ref": "AssetParametersa8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefcS3Bucket150DFE4F"
"Ref": "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3BucketEDAACFE7"
},
"/",
{
Expand All @@ -266,7 +266,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParametersa8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefcS3VersionKey304CC738"
"Ref": "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3VersionKey6FF3D50F"
}
]
}
Expand All @@ -279,7 +279,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParametersa8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefcS3VersionKey304CC738"
"Ref": "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3VersionKey6FF3D50F"
}
]
}
Expand Down Expand Up @@ -356,17 +356,17 @@
"Type": "String",
"Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\""
},
"AssetParametersa8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefcS3Bucket150DFE4F": {
"AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3BucketEDAACFE7": {
"Type": "String",
"Description": "S3 bucket for asset \"a8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefc\""
"Description": "S3 bucket for asset \"e31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fd\""
},
"AssetParametersa8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefcS3VersionKey304CC738": {
"AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3VersionKey6FF3D50F": {
"Type": "String",
"Description": "S3 key for asset version \"a8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefc\""
"Description": "S3 key for asset version \"e31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fd\""
},
"AssetParametersa8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefcArtifactHash6868817F": {
"AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdArtifactHash898696F1": {
"Type": "String",
"Description": "Artifact hash for asset \"a8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefc\""
"Description": "Artifact hash for asset \"e31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fd\""
}
}
}
31 changes: 28 additions & 3 deletions packages/@aws-cdk/aws-lambda/lib/function-hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,40 @@ export function calculateFunctionHash(fn: LambdaFunction) {

// render the cloudformation resource from this function
const config = stack.resolve((functionResource as any)._toCloudFormation());
sortProperties(config);
const stringifiedConfig = JSON.stringify(config);

const hash = crypto.createHash('md5');
hash.update(JSON.stringify(config));

hash.update(stringifiedConfig);
return hash.digest('hex');
}

export function trimFromStart(s: string, maxLength: number) {
const desiredLength = Math.min(maxLength, s.length);
const newStart = s.length - desiredLength;
return s.substring(newStart);
}
}

function sortProperties(templateObject: any): void {
// templateObject is of the shape: { Resources: { LogicalId: { Type: 'Function', Properties: { ... } }}}
const resources = templateObject.Resources;
const logicalId = Object.keys(resources)[0];
const properties = resources[logicalId].Properties;
const ret: any = {};
// We take all required properties in the order that they were historically,
// to make sure the hash we calculate is stable.
// There cannot be more required properties added in the future,
// as that would be a backwards-incompatible change.
const requiredProperties = ['Code', 'Handler', 'Role', 'Runtime'];
for (const requiredProperty of requiredProperties) {
ret[requiredProperty] = properties[requiredProperty];
}
// then, add all of the non-required properties,
// in the original order
for (const property of Object.keys(properties)) {
if (requiredProperties.indexOf(property) === -1) {
ret[property] = properties[property];
}
}
templateObject.Resources[logicalId].Properties = ret;
}
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-lambda/test/function-hash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe('function hash', () => {

describe('impact of env variables order on hash', () => {

test('without "currentVersion", we preserve old behavior to avoid unnesesary invalidation of templates', () => {
test('without "currentVersion", we preserve old behavior to avoid unnecessary invalidation of templates', () => {
const stack1 = new Stack();
const fn1 = new lambda.Function(stack1, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
Expand Down

This file was deleted.

0 comments on commit 4da50e5

Please sign in to comment.