Skip to content

Commit

Permalink
fix(lambda): function version ignores layer version changes (#20150)
Browse files Browse the repository at this point in the history
Fixes #19098.

This introduces two bug fixes that are hidden behind a feature flag to preserve the current hash:

- lambda layer order is ignored by the hash now 
- lambda layer version is included in the hash (along with other lambda layer attributes)

I also added a few more tests around this area to confirm the current behavior which should help demonstrate what the feature flag will change.

----

### All Submissions:

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

### Adding new Unconventional Dependencies:

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

### New Features

* [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/master/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
kaizencc authored May 31, 2022
1 parent 68761dc commit f19ecef
Show file tree
Hide file tree
Showing 46 changed files with 507 additions and 86 deletions.
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":"17.0.0"}
{"version":"20.0.0"}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"LambdaServiceRoleA8ED4D3B"
]
},
"LambdaCurrentVersionDF706F6A9a632a294ae3a9cd4d550f1c4e26619d": {
"LambdaCurrentVersionDF706F6A1ee13d0fa54e9f5621e8c7b616fc53fc": {
"Type": "AWS::Lambda::Version",
"Properties": {
"FunctionName": {
Expand All @@ -72,7 +72,7 @@
{
"EventType": "origin-request",
"LambdaFunctionARN": {
"Ref": "LambdaCurrentVersionDF706F6A9a632a294ae3a9cd4d550f1c4e26619d"
"Ref": "LambdaCurrentVersionDF706F6A1ee13d0fa54e9f5621e8c7b616fc53fc"
}
}
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": "18.0.0",
"version": "20.0.0",
"testCases": {
"aws-cloudfront/test/integ.distribution-lambda": {
"integ.distribution-lambda": {
"stacks": [
"integ-distribution-lambda"
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "17.0.0",
"version": "20.0.0",
"artifacts": {
"Tree": {
"type": "cdk:tree",
Expand Down Expand Up @@ -30,14 +30,23 @@
"/integ-distribution-lambda/Lambda/CurrentVersion/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "LambdaCurrentVersionDF706F6A9a632a294ae3a9cd4d550f1c4e26619d"
"data": "LambdaCurrentVersionDF706F6A1ee13d0fa54e9f5621e8c7b616fc53fc"
}
],
"/integ-distribution-lambda/Dist/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "DistB3B78991"
}
],
"LambdaCurrentVersionDF706F6A9a632a294ae3a9cd4d550f1c4e26619d": [
{
"type": "aws:cdk:logicalId",
"data": "LambdaCurrentVersionDF706F6A9a632a294ae3a9cd4d550f1c4e26619d",
"trace": [
"!!DESTRUCTIVE_CHANGES: WILL_DESTROY"
]
}
]
},
"displayName": "integ-distribution-lambda"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@
"lambdaFunctionAssociations": [
{
"lambdaFunctionArn": {
"Ref": "LambdaCurrentVersionDF706F6A9a632a294ae3a9cd4d550f1c4e26619d"
"Ref": "LambdaCurrentVersionDF706F6A1ee13d0fa54e9f5621e8c7b616fc53fc"
},
"eventType": "origin-request"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"HandlerServiceRoleFCDC14AE"
]
},
"HandlerCurrentVersion93FB80BFb2a9ce598bf2730613c07e406cddb6b6": {
"HandlerCurrentVersion93FB80BFf2e6129c63154d1f37c0092df295ab51": {
"Type": "AWS::Lambda::Version",
"Properties": {
"FunctionName": {
Expand All @@ -101,7 +101,7 @@
},
"FunctionVersion": {
"Fn::GetAtt": [
"HandlerCurrentVersion93FB80BFb2a9ce598bf2730613c07e406cddb6b6",
"HandlerCurrentVersion93FB80BFf2e6129c63154d1f37c0092df295ab51",
"Version"
]
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":"17.0.0"}
{"version":"20.0.0"}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": "18.0.0",
"version": "20.0.0",
"testCases": {
"aws-codedeploy/test/lambda/integ.deployment-group": {
"lambda/integ.deployment-group": {
"stacks": [
"aws-cdk-codedeploy-lambda"
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "17.0.0",
"version": "20.0.0",
"artifacts": {
"Tree": {
"type": "cdk:tree",
Expand Down Expand Up @@ -68,7 +68,7 @@
"/aws-cdk-codedeploy-lambda/Handler/CurrentVersion/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "HandlerCurrentVersion93FB80BFb2a9ce598bf2730613c07e406cddb6b6"
"data": "HandlerCurrentVersion93FB80BFf2e6129c63154d1f37c0092df295ab51"
}
],
"/aws-cdk-codedeploy-lambda/AssetParameters/edb7466707eb899fbaee22c1e67f9443e9edcc2eeda0b58d8448f7c4157746b3/S3Bucket": [
Expand Down Expand Up @@ -202,6 +202,15 @@
"type": "aws:cdk:logicalId",
"data": "ServiceprincipalMap"
}
],
"HandlerCurrentVersion93FB80BFb2a9ce598bf2730613c07e406cddb6b6": [
{
"type": "aws:cdk:logicalId",
"data": "HandlerCurrentVersion93FB80BFb2a9ce598bf2730613c07e406cddb6b6",
"trace": [
"!!DESTRUCTIVE_CHANGES: WILL_DESTROY"
]
}
]
},
"displayName": "aws-cdk-codedeploy-lambda"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@
},
"functionVersion": {
"Fn::GetAtt": [
"HandlerCurrentVersion93FB80BFb2a9ce598bf2730613c07e406cddb6b6",
"HandlerCurrentVersion93FB80BFf2e6129c63154d1f37c0092df295ab51",
"Version"
]
},
Expand Down
43 changes: 39 additions & 4 deletions packages/@aws-cdk/aws-lambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,20 @@ This has been fixed in the AWS CDK but *existing* users need to opt-in via a
[feature flag]. Users who have run `cdk init` since this fix will be opted in,
by default.

Existing users will need to enable the [feature flag]
Otherwise, you will need to enable the [feature flag]
`@aws-cdk/aws-lambda:recognizeVersionProps`. Since CloudFormation does not
allow duplicate versions, they will also need to make some modification to
their function so that a new version can be created. Any trivial change such as
a whitespace change in the code or a no-op environment variable will suffice.
allow duplicate versions, you will also need to make some modification to
your function so that a new version can be created. To efficiently and trivially
modify all your lambda functions at once, you can attach the
`FunctionVersionUpgrade` aspect to the stack, which slightly alters the
function description. This aspect is intended for one-time use to upgrade the
version of all your functions at the same time, and can safely be removed after
deploying once.

```ts
const stack = new Stack();
Aspects.of(stack).add(new lambda.FunctionVersionUpgrade(LAMBDA_RECOGNIZE_VERSION_PROPS));
```

When the new logic is in effect, you may rarely come across the following error:
`The following properties are not recognized as version properties`. This will
Expand All @@ -304,6 +313,32 @@ record whether a new version should be generated when this property is changed.
This can be typically determined by checking whether the property can be
modified using the *[UpdateFunctionConfiguration]* API or not.

### `currentVersion`: Updated hashing logic for layer versions

An additional update to the hashing logic fixes two issues surrounding layers.
Prior to this change, updating the lambda layer version would have no effect on
the function version. Also, the order of lambda layers provided to the function
was unnecessarily baked into the hash.

This has been fixed in the AWS CDK starting with version 2.27. If you ran
`cdk init` with an earlier version, you will need to opt-in via a [feature flag].
If you run `cdk init` with v2.27 or later, this fix will be opted in, by default.

Existing users will need to enable the [feature flag]
`@aws-cdk/aws-lambda:recognizeLayerVersion`. Since CloudFormation does not
allow duplicate versions, they will also need to make some modification to
their function so that a new version can be created. To efficiently and trivially
modify all your lambda functions at once, users can attach the
`FunctionVersionUpgrade` aspect to the stack, which slightly alters the
function description. This aspect is intended for one-time use to upgrade the
version of all your functions at the same time, and can safely be removed after
deploying once.

```ts
const stack = new Stack();
Aspects.of(stack).add(new lambda.FunctionVersionUpgrade(LAMBDA_RECOGNIZE_LAYER_VERSION));
```

[feature flag]: https://docs.aws.amazon.com/cdk/latest/guide/featureflags.html
[property overrides]: https://docs.aws.amazon.com/cdk/latest/guide/cfn_layer.html#cfn_layer_raw
[UpdateFunctionConfiguration]: https://docs.aws.amazon.com/lambda/latest/dg/API_UpdateFunctionConfiguration.html
Expand Down
35 changes: 34 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,31 @@ 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;
// 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;
continue;
}
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');
}
48 changes: 40 additions & 8 deletions packages/@aws-cdk/aws-lambda/lib/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ 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, IAspect, IConstruct, Lazy, Names, Size, Stack, Token } from '@aws-cdk/core';
import { LAMBDA_RECOGNIZE_LAYER_VERSION } from '@aws-cdk/cx-api';
import { Construct } from 'constructs';
import { AliasOptions, Alias } from './alias';
import { Architecture } from './architecture';
import { Code, CodeConfig } from './code';
import { ICodeSigningConfig } from './code-signing-config';
Expand All @@ -26,7 +28,6 @@ import { Runtime } from './runtime';
// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
// eslint-disable-next-line
import { LogRetentionRetryOptions } from './log-retention';
import { AliasOptions, Alias } from './alias';
import { addAlias } from './util';

/**
Expand Down Expand Up @@ -637,10 +638,10 @@ export class Function extends FunctionBase {

public readonly permissionsNode = this.node;


protected readonly canCreatePermissions = true;

private readonly layers: ILayerVersion[] = [];
/** @internal */
public readonly _layers: ILayerVersion[] = [];

private _logGroup?: logs.ILogGroup;

Expand Down Expand Up @@ -777,7 +778,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 @@ -909,7 +910,7 @@ export class Function extends FunctionBase {
*/
public addLayers(...layers: ILayerVersion[]): void {
for (const layer of layers) {
if (this.layers.length === 5) {
if (this._layers.length === 5) {
throw new Error('Unable to add layer: this lambda function already uses 5 layers.');
}
if (layer.compatibleRuntimes && !layer.compatibleRuntimes.find(runtime => runtime.runtimeEquals(this.runtime))) {
Expand All @@ -920,8 +921,7 @@ 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);
this._layers.push(layer);
}
}

Expand Down Expand Up @@ -1050,6 +1050,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 Expand Up @@ -1269,3 +1281,23 @@ function undefinedIfNoKeys<A>(struct: A): A | undefined {
const allUndefined = Object.values(struct).every(val => val === undefined);
return allUndefined ? undefined : struct;
}

/**
* Aspect for upgrading function versions when the feature flag
* provided feature flag present. This can be necessary when the feature flag
* changes the function hash, as such changes must be associated with a new
* version. This aspect will change the function description in these cases,
* which "validates" the new function hash.
*/
export class FunctionVersionUpgrade implements IAspect {
constructor(private readonly featureFlag: string, private readonly enabled=true) {}

public visit(node: IConstruct): void {
if (node instanceof Function &&
this.enabled === FeatureFlags.of(node).isEnabled(this.featureFlag)) {
const cfnFunction = node.node.defaultChild as CfnFunction;
const desc = cfnFunction.description ? `${cfnFunction.description} ` : '';
cfnFunction.addPropertyOverride('Description', `${desc}version-hash:${calculateFunctionHash(node)}`);
}
};
}
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
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-lambda/rosetta/default.ts-fixture
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Fixture with packages imported, but nothing else
import * as path from 'path';
import { Construct } from 'constructs';
import { CfnOutput, DockerImage, Duration, RemovalPolicy, Stack } from '@aws-cdk/core';
import { Aspects, CfnOutput, DockerImage, Duration, RemovalPolicy, Stack } from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
import * as iam from '@aws-cdk/aws-iam';
import { LAMBDA_RECOGNIZE_VERSION_PROPS, LAMBDA_RECOGNIZE_LAYER_VERSION } from '@aws-cdk/cx-api';

class Fixture extends Stack {
constructor(scope: Construct, id: string) {
Expand Down
Loading

0 comments on commit f19ecef

Please sign in to comment.