Skip to content

Commit

Permalink
fix(lambda): ever-changing Version hash with LayerVersion from tokens (
Browse files Browse the repository at this point in the history
…#23629)

If `LayerVersions` are referenced using tokens
(`LayerVersion.fromLayerVersionArn(this, 'Layer', /* some deploy-time value */`) then the version hash would incorrectly use the string representation of the tokenized ARN and be different on every deployment, incorrectly trying to create a new `Version` object on every deployment.

Resolve the ARN if we detect this.

However, this will not be complete: we now have the problem that a new Version will not be created if it were necessary, since CDK cannot read the deploy-time value of the ARN and cannot mix it into the Version LogicalID if necessary.

To fix that, add a:

```ts
fn.invalidateVersionBasedOn(...);
```

Function to help invalidate the version using outside information.


----

### All Submissions:

* [ ] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Construct Runtime Dependencies:

* [ ] This PR adds new construct runtime dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-construct-runtime-dependencies)

### New Features

* [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
rix0rrr committed Jan 13, 2023
1 parent dce662c commit 88fc62d
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 5 deletions.
17 changes: 13 additions & 4 deletions packages/@aws-cdk/aws-lambda/lib/function-hash.ts
@@ -1,10 +1,10 @@
import { CfnResource, FeatureFlags, Stack } from '@aws-cdk/core';
import { CfnResource, FeatureFlags, Stack, Token } from '@aws-cdk/core';
import { md5hash } from '@aws-cdk/core/lib/helpers-internal';
import { LAMBDA_RECOGNIZE_LAYER_VERSION, LAMBDA_RECOGNIZE_VERSION_PROPS } from '@aws-cdk/cx-api';
import { Function as LambdaFunction } from './function';
import { ILayerVersion } from './layers';

export function calculateFunctionHash(fn: LambdaFunction) {
export function calculateFunctionHash(fn: LambdaFunction, additional: string = '') {
const stack = Stack.of(fn);

const functionResource = fn.node.defaultChild as CfnResource;
Expand Down Expand Up @@ -34,7 +34,7 @@ export function calculateFunctionHash(fn: LambdaFunction) {
stringifiedConfig = stringifiedConfig + calculateLayersHash(fn._layers);
}

return md5hash(stringifiedConfig);
return md5hash(stringifiedConfig + additional);
}

export function trimFromStart(s: string, maxLength: number) {
Expand Down Expand Up @@ -130,7 +130,16 @@ function calculateLayersHash(layers: ILayerVersion[]): string {
// if there is no layer resource, then the layer was imported
// and we will include the layer arn and runtimes in the hash
if (layerResource === undefined) {
layerConfig[layer.layerVersionArn] = layer.compatibleRuntimes;
// ARN may have unresolved parts in it, but we didn't deal with this previously
// so deal with it now for backwards compatibility.
if (!Token.isUnresolved(layer.layerVersionArn)) {
layerConfig[layer.layerVersionArn] = layer.compatibleRuntimes;
} else {
layerConfig[layer.node.id] = {
arn: stack.resolve(layer.layerVersionArn),
runtimes: layer.compatibleRuntimes?.map(r => r.name),
};
}
continue;
}
const config = stack.resolve((layerResource as any)._toCloudFormation());
Expand Down
28 changes: 27 additions & 1 deletion packages/@aws-cdk/aws-lambda/lib/function.ts
Expand Up @@ -436,7 +436,7 @@ export class Function extends FunctionBase {

cfn.overrideLogicalId(Lazy.uncachedString({
produce: () => {
const hash = calculateFunctionHash(this);
const hash = calculateFunctionHash(this, this.hashMixins.join(''));
const logicalId = trimFromStart(originalLogicalId, 255 - 32);
return `${logicalId}${hash}`;
},
Expand Down Expand Up @@ -664,6 +664,7 @@ export class Function extends FunctionBase {
private _currentVersion?: Version;

private _architecture?: Architecture;
private hashMixins = new Array<string>();

constructor(scope: Construct, id: string, props: FunctionProps) {
super(scope, id, {
Expand Down Expand Up @@ -940,6 +941,31 @@ export class Function extends FunctionBase {
return this;
}

/**
* Mix additional information into the hash of the Version object
*
* The Lambda Function construct does its best to automatically create a new
* Version when anything about the Function changes (its code, its layers,
* any of the other properties).
*
* However, you can sometimes source information from places that the CDK cannot
* look into, like the deploy-time values of SSM parameters. In those cases,
* the CDK would not force the creation of a new Version object when it actually
* should.
*
* This method can be used to invalidate the current Version object. Pass in
* any string into this method, and make sure the string changes when you know
* a new Version needs to be created.
*
* This method may be called more than once.
*/
public invalidateVersionBasedOn(x: string) {
if (Token.isUnresolved(x)) {
throw new Error('invalidateVersionOn: input may not contain unresolved tokens');
}
this.hashMixins.push(x);
}

/**
* Adds one or more Lambda Layers to this Lambda function.
*
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-lambda/package.json
Expand Up @@ -94,6 +94,7 @@
"@types/aws-lambda": "^8.10.109",
"@types/jest": "^27.5.2",
"@types/lodash": "^4.14.191",
"@aws-cdk/aws-ssm": "0.0.0",
"jest": "^27.5.1",
"lodash": "^4.17.21"
},
Expand Down
83 changes: 83 additions & 0 deletions packages/@aws-cdk/aws-lambda/test/function-hash.test.ts
@@ -1,4 +1,6 @@
import * as path from 'path';
import { Template } from '@aws-cdk/assertions';
import * as ssm from '@aws-cdk/aws-ssm';
import { resourceSpecification } from '@aws-cdk/cfnspec';
import { App, CfnOutput, CfnResource, Stack } from '@aws-cdk/core';
import * as cxapi from '@aws-cdk/cx-api';
Expand Down Expand Up @@ -440,3 +442,84 @@ describe('function hash', () => {
});
});
});

test('imported layer hashes are consistent', () => {
// GIVEN
const app = new App({
context: {
'@aws-cdk/aws-lambda:recognizeLayerVersion': true,
},
});

// WHEN
const stack1 = new Stack(app, 'Stack1');
const param1 = ssm.StringParameter.fromStringParameterName(stack1, 'Param', 'ParamName');
const fn1 = new lambda.Function(stack1, 'Fn', {
code: lambda.Code.fromInline('asdf'),
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_18_X,
layers: [
lambda.LayerVersion.fromLayerVersionArn(stack1, 'MyLayer',
`arn:aws:lambda:${stack1.region}:<AccountID>:layer:IndexCFN:${param1.stringValue}`),
],
});
fn1.currentVersion; // Force creation of version

const stack2 = new Stack(app, 'Stack2');
const param2 = ssm.StringParameter.fromStringParameterName(stack2, 'Param', 'ParamName');
const fn2 = new lambda.Function(stack2, 'Fn', {
code: lambda.Code.fromInline('asdf'),
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_18_X,
layers: [
lambda.LayerVersion.fromLayerVersionArn(stack2, 'MyLayer',
`arn:aws:lambda:${stack1.region}:<AccountID>:layer:IndexCFN:${param2.stringValue}`),
],
});
fn2.currentVersion; // Force creation of version

// THEN
const template1 = Template.fromStack(stack1);
const template2 = Template.fromStack(stack2);

expect(template1.toJSON()).toEqual(template2.toJSON());
});

test.each([false, true])('can invalidate version hash using invalidateVersionBasedOn: %p', (doIt) => {
// GIVEN
const app = new App();

// WHEN
const stack1 = new Stack(app, 'Stack1');
const fn1 = new lambda.Function(stack1, 'Fn', {
code: lambda.Code.fromInline('asdf'),
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_18_X,
});
if (doIt) {
fn1.invalidateVersionBasedOn('abc');
}
fn1.currentVersion; // Force creation of version

const stack2 = new Stack(app, 'Stack2');
const fn2 = new lambda.Function(stack2, 'Fn', {
code: lambda.Code.fromInline('asdf'),
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_18_X,
});
if (doIt) {
fn1.invalidateVersionBasedOn('xyz');
}
fn2.currentVersion; // Force creation of version

// THEN
const template1 = Template.fromStack(stack1);
const template2 = Template.fromStack(stack2);

if (doIt) {
expect(template1.toJSON()).not.toEqual(template2.toJSON());
} else {
expect(template1.toJSON()).toEqual(template2.toJSON());
}

});

0 comments on commit 88fc62d

Please sign in to comment.