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

fix(lambda): function version ignores layer version changes #20150

Merged
merged 29 commits into from
May 31, 2022
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6061750
start
kaizencc Mar 14, 2022
ca89e1a
more progress
kaizencc Mar 14, 2022
6562425
add feature flag to lock lambda layer order
kaizencc Mar 16, 2022
5bb2c19
layer stuff
kaizencc Mar 17, 2022
c5af8f5
merge
kaizencc Apr 29, 2022
97f7f8c
revive branch and fix test
kaizencc Apr 29, 2022
144b38d
bake layer version into function version hash
kaizencc Apr 29, 2022
2bac2c9
add docstring
kaizencc Apr 29, 2022
5d45ae6
Merge branch 'master' into conroy/lambdalayer
kaizencc May 11, 2022
78fd23b
update test to better capture old behavior
kaizencc May 12, 2022
56db632
update integ tests
kaizencc May 13, 2022
a050989
Merge branch 'master' into conroy/lambdalayer
kaizencc May 24, 2022
68d06b3
Merge branch 'master' of https://github.com/aws/aws-cdk into conroy/l…
kaizencc May 24, 2022
3f62785
Merge branch 'master' into conroy/lambdalayer
Naumel May 25, 2022
4e6b561
Merge branch 'master' into conroy/lambdalayer
kaizencc May 26, 2022
0068a22
functionversionupgrade aspect
kaizencc May 26, 2022
8df2619
Merge branch 'master' of https://github.com/aws/aws-cdk into conroy/l…
kaizencc May 26, 2022
d850443
update integ tests and generalize functionversionupgrade
kaizencc May 26, 2022
404fcd3
add documentation
kaizencc May 26, 2022
594a590
Merge branch 'conroy/lambdalayer' of https://github.com/aws/aws-cdk i…
kaizencc May 26, 2022
dc4a0d2
fix for imported layers
kaizencc May 26, 2022
757ef2d
update triggers test
kaizencc May 26, 2022
a0a617b
update cloudfront snapshots
kaizencc May 26, 2022
5967433
update codedeploy snapshots
kaizencc May 27, 2022
3404aa3
Merge branch 'master' into conroy/lambdalayer
kaizencc May 27, 2022
47f7115
make layers internal api
kaizencc May 31, 2022
3be936f
readme updates
kaizencc May 31, 2022
72a3197
bake version arn into layer hash
kaizencc May 31, 2022
de03832
Merge branch 'master' into conroy/lambdalayer
mergify[bot] May 31, 2022
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
29 changes: 28 additions & 1 deletion packages/@aws-cdk/aws-lambda/lib/function-hash.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as crypto from 'crypto';
import { CfnResource, FeatureFlags, Stack } from '@aws-cdk/core';
import { LAMBDA_RECOGNIZE_VERSION_PROPS } from '@aws-cdk/cx-api';
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) {
const stack = Stack.of(fn);
Expand Down Expand Up @@ -29,6 +30,10 @@ export function calculateFunctionHash(fn: LambdaFunction) {
stringifiedConfig = JSON.stringify(config);
}

if (FeatureFlags.of(fn).isEnabled(LAMBDA_RECOGNIZE_LAYER_VERSION)) {
stringifiedConfig = stringifiedConfig + calculateLayersHash(fn.layers);
}

const hash = crypto.createHash('md5');
hash.update(stringifiedConfig);
return hash.digest('hex');
Expand Down Expand Up @@ -117,3 +122,25 @@ function sortProperties(properties: any) {
}
return ret;
}

function calculateLayersHash(layers: ILayerVersion[]): string {
const layerConfig: {[key: string]: any } = {};
for (const layer of layers) {
const stack = Stack.of(layer);
const layerResource = layer.node.defaultChild as CfnResource;
kaizencc marked this conversation as resolved.
Show resolved Hide resolved
const config = stack.resolve((layerResource as any)._toCloudFormation());
const resources = config.Resources;
const resourceKeys = Object.keys(resources);
if (resourceKeys.length !== 1) {
throw new Error(`Expected one rendered CloudFormation resource but found ${resourceKeys.length}`);
}
const logicalId = resourceKeys[0];
const properties = resources[logicalId].Properties;
// all properties require replacement, so they are all version locked.
layerConfig[layer.node.id] = properties;
}

const hash = crypto.createHash('md5');
hash.update(JSON.stringify(layerConfig));
return hash.digest('hex');
}
23 changes: 19 additions & 4 deletions packages/@aws-cdk/aws-lambda/lib/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import * as kms from '@aws-cdk/aws-kms';
import * as logs from '@aws-cdk/aws-logs';
import * as sns from '@aws-cdk/aws-sns';
import * as sqs from '@aws-cdk/aws-sqs';
import { Annotations, ArnFormat, CfnResource, Duration, Fn, Lazy, Names, Size, Stack, Token } from '@aws-cdk/core';
import { Annotations, ArnFormat, CfnResource, Duration, FeatureFlags, Fn, Lazy, Names, Size, Stack, Token } from '@aws-cdk/core';
import { LAMBDA_RECOGNIZE_LAYER_VERSION } from '@aws-cdk/cx-api';
import { Construct } from 'constructs';
import { Architecture } from './architecture';
import { Code, CodeConfig } from './code';
Expand Down Expand Up @@ -640,7 +641,10 @@ export class Function extends FunctionBase {

protected readonly canCreatePermissions = true;

private readonly layers: ILayerVersion[] = [];
/**
* The lambda layers that this function depends on.
*/
public readonly layers: ILayerVersion[] = [];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alternative to making this public is to send the layers directly into the calculateFunctionHash() like: calculateFunctionHash(fn, fn.layers). That seems awkward to me so I chose to make this public.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

calculateFunctionHash is a private function, so I don't really care about its signature. I care more about making this public, which this change does... so I'd rather you didn't.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marked @internal instead. I need this to be publicly accessible for the aspect as well, and if my understanding of @internal is correct, this makes the most sense.


private _logGroup?: logs.ILogGroup;

Expand Down Expand Up @@ -771,7 +775,7 @@ export class Function extends FunctionBase {
zipFile: code.inlineCode,
imageUri: code.image?.imageUri,
},
layers: Lazy.list({ produce: () => this.layers.map(layer => layer.layerVersionArn) }, { omitEmpty: true }), // Evaluated on synthesis
layers: Lazy.list({ produce: () => this.renderLayers() }), // Evaluated on synthesis
handler: props.handler === Handler.FROM_IMAGE ? undefined : props.handler,
timeout: props.timeout && props.timeout.toSeconds(),
packageType: props.runtime === Runtime.FROM_IMAGE ? 'Image' : undefined,
Expand Down Expand Up @@ -914,7 +918,6 @@ export class Function extends FunctionBase {
// Currently no validations for compatible architectures since Lambda service
// allows layers configured with one architecture to be used with a Lambda function
// from another architecture.

this.layers.push(layer);
}
}
Expand Down Expand Up @@ -1044,6 +1047,18 @@ Environment variables can be marked for removal when used in Lambda@Edge by sett
this.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLambdaInsightsExecutionRolePolicy'));
}

private renderLayers() {
if (!this.layers || this.layers.length === 0) {
return undefined;
}

if (FeatureFlags.of(this).isEnabled(LAMBDA_RECOGNIZE_LAYER_VERSION)) {
this.layers.sort();
}

return this.layers.map(layer => layer.layerVersionArn);
}

private renderEnvironment() {
if (!this.environment || Object.keys(this.environment).length === 0) {
return undefined;
Expand Down
4 changes: 1 addition & 3 deletions packages/@aws-cdk/aws-lambda/lib/layers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,7 @@ export class LayerVersion extends LayerVersionBase {
if (props.compatibleRuntimes && props.compatibleRuntimes.length === 0) {
throw new Error('Attempted to define a Lambda layer that supports no runtime!');
}
if (props.code.isInline) {
throw new Error('Lambda layers cannot be created from inline code');
}

// Allow usage of the code in this context...
const code = props.code.bind(this);
if (code.inlineCode) {
Expand Down
110 changes: 108 additions & 2 deletions packages/@aws-cdk/aws-lambda/test/function-hash.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as path from 'path';
import { resourceSpecification } from '@aws-cdk/cfnspec';
import { App, CfnOutput, CfnResource, Stack } from '@aws-cdk/core';
import { LAMBDA_RECOGNIZE_VERSION_PROPS } from '@aws-cdk/cx-api';
import { LAMBDA_RECOGNIZE_LAYER_VERSION, LAMBDA_RECOGNIZE_VERSION_PROPS } from '@aws-cdk/cx-api';
import * as lambda from '../lib';
import { calculateFunctionHash, trimFromStart, VERSION_LOCKED } from '../lib/function-hash';

Expand Down Expand Up @@ -123,8 +123,114 @@ describe('function hash', () => {
expect(calculateFunctionHash(fn2)).toEqual('ffedf6424a18a594a513129dc97bf53c');
});

describe('impact of env variables order on hash', () => {
describe('lambda layers', () => {
let stack1: Stack;
let layer1: lambda.LayerVersion;
let layer2: lambda.LayerVersion;
beforeAll(() => {
stack1 = new Stack();
layer1 = new lambda.LayerVersion(stack1, 'MyLayer', {
code: lambda.Code.fromAsset(path.join(__dirname, 'layer-code')),
compatibleRuntimes: [lambda.Runtime.NODEJS_12_X],
license: 'Apache-2.0',
description: 'A layer to test the L2 construct',
});
layer2 = new lambda.LayerVersion(stack1, 'MyLayer2', {
code: lambda.Code.fromAsset(path.join(__dirname, 'layer-code')),
compatibleRuntimes: [lambda.Runtime.NODEJS_12_X],
license: 'Apache-2.0',
description: 'A layer to test the L2 construct',
});
});

test('same configuration yields the same hash', () => {
const stack2 = new Stack();
const fn1 = new lambda.Function(stack2, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
code: lambda.Code.fromInline('foo'),
handler: 'index.handler',
layers: [layer1],
});

const stack3 = new Stack();
const fn2 = new lambda.Function(stack3, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
code: lambda.Code.fromInline('foo'),
handler: 'index.handler',
layers: [layer1],
});

expect(calculateFunctionHash(fn1)).toEqual(calculateFunctionHash(fn2));
expect(calculateFunctionHash(fn1)).toEqual('028f8a4cb1c719f29e70b7b3c0f2a9d7');
});

test('different layers impacts hash', () => {
const stack2 = new Stack();
const fn1 = new lambda.Function(stack2, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
code: lambda.Code.fromInline('foo'),
handler: 'index.handler',
layers: [layer1],
});

const stack3 = new Stack();
const fn2 = new lambda.Function(stack3, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
code: lambda.Code.fromInline('foo'),
handler: 'index.handler',
layers: [layer2],
});

expect(calculateFunctionHash(fn1)).toEqual('028f8a4cb1c719f29e70b7b3c0f2a9d7');
expect(calculateFunctionHash(fn2)).toEqual('e74647bf81c4d532137545c8234726f3');
});

describe('impact of lambda layer order on hash', () => {
test('without feature flag, preserve old behavior to avoid unnecessary invalidation of templates', () => {
const stack2 = new Stack();
const fn1 = new lambda.Function(stack2, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
code: lambda.Code.fromInline('foo'),
handler: 'index.handler',
layers: [layer1, layer2],
});

const stack3 = new Stack();
const fn2 = new lambda.Function(stack3, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
code: lambda.Code.fromInline('foo'),
handler: 'index.handler',
layers: [layer2, layer1],
});

expect(calculateFunctionHash(fn1)).not.toEqual(calculateFunctionHash(fn2));
kaizencc marked this conversation as resolved.
Show resolved Hide resolved
});

test('with feature flag, we sort layers so order is consistent', () => {
const app = new App({ context: { [LAMBDA_RECOGNIZE_LAYER_VERSION]: true } });

const stack2 = new Stack(app, 'stack1');
const fn1 = new lambda.Function(stack2, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
code: lambda.Code.fromInline('foo'),
handler: 'index.handler',
layers: [layer1, layer2],
});

const stack3 = new Stack(app, 'stack2');
const fn2 = new lambda.Function(stack3, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
code: lambda.Code.fromInline('foo'),
handler: 'index.handler',
layers: [layer2, layer1],
});

expect(calculateFunctionHash(fn1)).toEqual(calculateFunctionHash(fn2));
});
});
});

describe('impact of env variables order on hash', () => {
test('without "currentVersion", we preserve old behavior to avoid unnecessary invalidation of templates', () => {
const stack1 = new Stack();
const fn1 = new lambda.Function(stack1, 'MyFunction', {
Expand Down
65 changes: 65 additions & 0 deletions packages/@aws-cdk/aws-lambda/test/function.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import * as sqs from '@aws-cdk/aws-sqs';
import { testDeprecated } from '@aws-cdk/cdk-build-tools';
import * as cdk from '@aws-cdk/core';
import { Lazy, Size } from '@aws-cdk/core';
import { LAMBDA_RECOGNIZE_LAYER_VERSION } from '@aws-cdk/cx-api';
import * as constructs from 'constructs';
import * as _ from 'lodash';
import * as lambda from '../lib';
import { calculateFunctionHash } from '../lib/function-hash';

describe('function', () => {
test('default function', () => {
Expand Down Expand Up @@ -1437,6 +1439,69 @@ describe('function', () => {
expect(bindTarget).toEqual(fn);
});

test('layer is baked into the function version', () => {
// GIVEN
const stack = new cdk.Stack(undefined, 'TestStack');
const bucket = new s3.Bucket(stack, 'Bucket');
const code = new lambda.S3Code(bucket, 'ObjectKey');

const fn = new lambda.Function(stack, 'fn', {
runtime: lambda.Runtime.NODEJS_14_X,
code: lambda.Code.fromInline('exports.main = function() { console.log("DONE"); }'),
handler: 'index.main',
});

const fnHash = calculateFunctionHash(fn);

// WHEN
const layer = new lambda.LayerVersion(stack, 'LayerVersion', {
code,
compatibleRuntimes: [lambda.Runtime.NODEJS_14_X],
});

fn.addLayers(layer);

const newFnHash = calculateFunctionHash(fn);

expect(fnHash).not.toEqual(newFnHash);
});

test('with feature flag, layer version is baked into function version', () => {
kaizencc marked this conversation as resolved.
Show resolved Hide resolved
// GIVEN
const app = new cdk.App({ context: { [LAMBDA_RECOGNIZE_LAYER_VERSION]: true } });
const stack = new cdk.Stack(app, 'TestStack');
const bucket = new s3.Bucket(stack, 'Bucket');
const code = new lambda.S3Code(bucket, 'ObjectKey');
const layer = new lambda.LayerVersion(stack, 'LayerVersion', {
code,
compatibleRuntimes: [lambda.Runtime.NODEJS_14_X],
});

// function with layer
const fn = new lambda.Function(stack, 'fn', {
runtime: lambda.Runtime.NODEJS_14_X,
code: lambda.Code.fromInline('exports.main = function() { console.log("DONE"); }'),
handler: 'index.main',
layers: [layer],
});

const fnHash = calculateFunctionHash(fn);

// use escape hatch to change the content of the layer
// this simulates updating the layer code which changes the version.
const cfnLayer = layer.node.defaultChild as lambda.CfnLayerVersion;
const newCode = (new lambda.S3Code(bucket, 'NewObjectKey')).bind(layer);
cfnLayer.content = {
s3Bucket: newCode.s3Location!.bucketName,
s3Key: newCode.s3Location!.objectKey,
s3ObjectVersion: newCode.s3Location!.objectVersion,
};

const newFnHash = calculateFunctionHash(fn);

expect(fnHash).not.toEqual(newFnHash);
});

test('using an incompatible layer', () => {
// GIVEN
const stack = new cdk.Stack(undefined, 'TestStack');
Expand Down
3 changes: 3 additions & 0 deletions packages/@aws-cdk/cx-api/lib/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ export const EFS_DEFAULT_ENCRYPTION_AT_REST = '@aws-cdk/aws-efs:defaultEncryptio
*/
export const LAMBDA_RECOGNIZE_VERSION_PROPS = '@aws-cdk/aws-lambda:recognizeVersionProps';

export const LAMBDA_RECOGNIZE_LAYER_VERSION = '@aws-cdk/aws-lambda:recognizeLayerVersion';

/**
* Enable this feature flag to have cloudfront distributions use the security policy TLSv1.2_2021 by default.
*
Expand Down Expand Up @@ -255,6 +257,7 @@ export const FUTURE_FLAGS: { [key: string]: boolean } = {
[RDS_LOWERCASE_DB_IDENTIFIER]: true,
[EFS_DEFAULT_ENCRYPTION_AT_REST]: true,
[LAMBDA_RECOGNIZE_VERSION_PROPS]: true,
[LAMBDA_RECOGNIZE_LAYER_VERSION]: true,
Naumel marked this conversation as resolved.
Show resolved Hide resolved
[CLOUDFRONT_DEFAULT_SECURITY_POLICY_TLS_V1_2_2021]: true,
[ECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVER]: true,
[EC2_UNIQUE_IMDSV2_LAUNCH_TEMPLATE_NAME]: true,
Expand Down