Skip to content

Commit dfe2c5c

Browse files
authored
fix(lambda-nodejs): NodejsFunction construct incompatible with lambda@edge (#9562)
Check version and function compatibility when a Lambda is used for Lambda@Edge. Environment variables can be marked as "removable" when used for Lambda@Edge. Closes #9328 Closes #9453 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 85fdeb5 commit dfe2c5c

12 files changed

+376
-43
lines changed

packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts

+4-9
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,10 @@ export class CacheBehavior {
4848
smoothStreaming: this.props.smoothStreaming,
4949
viewerProtocolPolicy: this.props.viewerProtocolPolicy ?? ViewerProtocolPolicy.ALLOW_ALL,
5050
lambdaFunctionAssociations: this.props.edgeLambdas
51-
? this.props.edgeLambdas.map(edgeLambda => {
52-
if (edgeLambda.functionVersion.version === '$LATEST') {
53-
throw new Error('$LATEST function version cannot be used for Lambda@Edge');
54-
}
55-
return {
56-
lambdaFunctionArn: edgeLambda.functionVersion.functionArn,
57-
eventType: edgeLambda.eventType.toString(),
58-
};
59-
})
51+
? this.props.edgeLambdas.map(edgeLambda => ({
52+
lambdaFunctionArn: edgeLambda.functionVersion.edgeArn,
53+
eventType: edgeLambda.eventType.toString(),
54+
}))
6055
: undefined,
6156
};
6257
}

packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -896,7 +896,7 @@ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribu
896896
lambdaFunctionAssociations: input.lambdaFunctionAssociations
897897
.map(fna => ({
898898
eventType: fna.eventType,
899-
lambdaFunctionArn: fna.lambdaFunction && fna.lambdaFunction.functionArn,
899+
lambdaFunctionArn: fna.lambdaFunction && fna.lambdaFunction.edgeArn,
900900
})),
901901
});
902902

packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts

+53
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,59 @@ describe('with Lambda@Edge functions', () => {
538538
});
539539
}).toThrow(/\$LATEST function version cannot be used for Lambda@Edge/);
540540
});
541+
542+
test('with removable env vars', () => {
543+
const envLambdaFunction = new lambda.Function(stack, 'EnvFunction', {
544+
runtime: lambda.Runtime.NODEJS,
545+
code: lambda.Code.fromInline('whateverwithenv'),
546+
handler: 'index.handler',
547+
});
548+
envLambdaFunction.addEnvironment('KEY', 'value', { removeInEdge: true });
549+
550+
new Distribution(stack, 'MyDist', {
551+
defaultBehavior: {
552+
origin,
553+
edgeLambdas: [
554+
{
555+
functionVersion: envLambdaFunction.currentVersion,
556+
eventType: LambdaEdgeEventType.ORIGIN_REQUEST,
557+
},
558+
],
559+
},
560+
});
561+
562+
expect(stack).toHaveResource('AWS::Lambda::Function', {
563+
Environment: ABSENT,
564+
Code: {
565+
ZipFile: 'whateverwithenv',
566+
},
567+
});
568+
});
569+
570+
test('with incompatible env vars', () => {
571+
const envLambdaFunction = new lambda.Function(stack, 'EnvFunction', {
572+
runtime: lambda.Runtime.NODEJS,
573+
code: lambda.Code.fromInline('whateverwithenv'),
574+
handler: 'index.handler',
575+
environment: {
576+
KEY: 'value',
577+
},
578+
});
579+
580+
new Distribution(stack, 'MyDist', {
581+
defaultBehavior: {
582+
origin,
583+
edgeLambdas: [
584+
{
585+
functionVersion: envLambdaFunction.currentVersion,
586+
eventType: LambdaEdgeEventType.ORIGIN_REQUEST,
587+
},
588+
],
589+
},
590+
});
591+
592+
expect(() => app.synth()).toThrow(/KEY/);
593+
});
541594
});
542595

543596
test('price class is included if provided', () => {

packages/@aws-cdk/aws-cloudfront/test/web_distribution.test.ts

+80-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert';
1+
import { ABSENT, expect, haveResource, haveResourceLike } from '@aws-cdk/assert';
22
import * as certificatemanager from '@aws-cdk/aws-certificatemanager';
33
import * as lambda from '@aws-cdk/aws-lambda';
44
import * as s3 from '@aws-cdk/aws-s3';
@@ -426,8 +426,7 @@ nodeunitShim({
426426
const stack = new cdk.Stack();
427427
const sourceBucket = new s3.Bucket(stack, 'Bucket');
428428

429-
const lambdaFunction = new lambda.SingletonFunction(stack, 'Lambda', {
430-
uuid: 'xxxx-xxxx-xxxx-xxxx',
429+
const lambdaFunction = new lambda.Function(stack, 'Lambda', {
431430
code: lambda.Code.inline('foo'),
432431
handler: 'index.handler',
433432
runtime: lambda.Runtime.NODEJS_10_X,
@@ -444,7 +443,7 @@ nodeunitShim({
444443
isDefaultBehavior: true,
445444
lambdaFunctionAssociations: [{
446445
eventType: LambdaEdgeEventType.ORIGIN_REQUEST,
447-
lambdaFunction: lambdaFunction.latestVersion,
446+
lambdaFunction: lambdaFunction.addVersion('1'),
448447
}],
449448
},
450449
],
@@ -459,13 +458,7 @@ nodeunitShim({
459458
{
460459
'EventType': 'origin-request',
461460
'LambdaFunctionARN': {
462-
'Fn::Join': [
463-
'',
464-
[
465-
{ 'Fn::GetAtt': ['SingletonLambdaxxxxxxxxxxxxxxxx69D4268A', 'Arn'] },
466-
':$LATEST',
467-
],
468-
],
461+
'Ref': 'LambdaVersion1BB7548E1',
469462
},
470463
},
471464
],
@@ -476,6 +469,82 @@ nodeunitShim({
476469
test.done();
477470
},
478471

472+
'associate a lambda with removable env vars'(test: Test) {
473+
const app = new cdk.App();
474+
const stack = new cdk.Stack(app, 'Stack');
475+
const sourceBucket = new s3.Bucket(stack, 'Bucket');
476+
477+
const lambdaFunction = new lambda.Function(stack, 'Lambda', {
478+
code: lambda.Code.inline('foo'),
479+
handler: 'index.handler',
480+
runtime: lambda.Runtime.NODEJS_10_X,
481+
});
482+
lambdaFunction.addEnvironment('KEY', 'value', { removeInEdge: true });
483+
484+
new CloudFrontWebDistribution(stack, 'AnAmazingWebsiteProbably', {
485+
originConfigs: [
486+
{
487+
s3OriginSource: {
488+
s3BucketSource: sourceBucket,
489+
},
490+
behaviors: [
491+
{
492+
isDefaultBehavior: true,
493+
lambdaFunctionAssociations: [{
494+
eventType: LambdaEdgeEventType.ORIGIN_REQUEST,
495+
lambdaFunction: lambdaFunction.addVersion('1'),
496+
}],
497+
},
498+
],
499+
},
500+
],
501+
});
502+
503+
expect(stack).to(haveResource('AWS::Lambda::Function', {
504+
Environment: ABSENT,
505+
}));
506+
507+
test.done();
508+
},
509+
510+
'throws when associating a lambda with incompatible env vars'(test: Test) {
511+
const app = new cdk.App();
512+
const stack = new cdk.Stack(app, 'Stack');
513+
const sourceBucket = new s3.Bucket(stack, 'Bucket');
514+
515+
const lambdaFunction = new lambda.Function(stack, 'Lambda', {
516+
code: lambda.Code.inline('foo'),
517+
handler: 'index.handler',
518+
runtime: lambda.Runtime.NODEJS_10_X,
519+
environment: {
520+
KEY: 'value',
521+
},
522+
});
523+
524+
new CloudFrontWebDistribution(stack, 'AnAmazingWebsiteProbably', {
525+
originConfigs: [
526+
{
527+
s3OriginSource: {
528+
s3BucketSource: sourceBucket,
529+
},
530+
behaviors: [
531+
{
532+
isDefaultBehavior: true,
533+
lambdaFunctionAssociations: [{
534+
eventType: LambdaEdgeEventType.ORIGIN_REQUEST,
535+
lambdaFunction: lambdaFunction.addVersion('1'),
536+
}],
537+
},
538+
],
539+
},
540+
],
541+
});
542+
543+
test.throws(() => app.synth(), /KEY/);
544+
545+
test.done();
546+
},
547+
479548
'distribution has a defaultChild'(test: Test) {
480549
const stack = new cdk.Stack();
481550
const sourceBucket = new s3.Bucket(stack, 'Bucket');

packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export class NodejsFunction extends lambda.Function {
8585

8686
// Enable connection reuse for aws-sdk
8787
if (props.awsSdkConnectionReuse ?? true) {
88-
this.addEnvironment('AWS_NODEJS_CONNECTION_REUSE_ENABLED', '1');
88+
this.addEnvironment('AWS_NODEJS_CONNECTION_REUSE_ENABLED', '1', { removeInEdge: true });
8989
}
9090
} finally {
9191
// We can only restore after the code has been bound to the function

packages/@aws-cdk/aws-lambda/lib/function-base.ts

+13
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,15 @@ export abstract class FunctionBase extends Resource implements IFunction {
318318
});
319319
}
320320

321+
/**
322+
* Checks whether this function is compatible for Lambda@Edge.
323+
*
324+
* @internal
325+
*/
326+
public _checkEdgeCompatibility(): void {
327+
return;
328+
}
329+
321330
/**
322331
* Returns the construct tree node that corresponds to the lambda function.
323332
* For use internally for constructs, when the tree is set up in non-standard ways. Ex: SingletonFunction.
@@ -417,4 +426,8 @@ class LatestVersion extends FunctionBase implements IVersion {
417426
public addAlias(aliasName: string, options: AliasOptions = {}) {
418427
return addAlias(this, this, aliasName, options);
419428
}
429+
430+
public get edgeArn(): never {
431+
throw new Error('$LATEST function version cannot be used for Lambda@Edge');
432+
}
420433
}

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

+59-18
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ export class Function extends FunctionBase {
515515
/**
516516
* Environment variables for this function
517517
*/
518-
private readonly environment: { [key: string]: string };
518+
private environment: { [key: string]: EnvironmentConfig } = {};
519519

520520
private readonly currentVersionOptions?: VersionOptions;
521521
private _currentVersion?: Version;
@@ -558,7 +558,7 @@ export class Function extends FunctionBase {
558558
const code = props.code.bind(this);
559559
verifyCodeConfig(code, props.runtime);
560560

561-
let profilingGroupEnvironmentVariables = {};
561+
let profilingGroupEnvironmentVariables: { [key: string]: string } = {};
562562
if (props.profilingGroup && props.profiling !== false) {
563563
this.validateProfilingEnvironmentVariables(props);
564564
props.profilingGroup.grantPublish(this.role);
@@ -582,7 +582,10 @@ export class Function extends FunctionBase {
582582
};
583583
}
584584

585-
this.environment = { ...profilingGroupEnvironmentVariables, ...(props.environment || {}) };
585+
const env = { ...profilingGroupEnvironmentVariables, ...props.environment };
586+
for (const [key, value] of Object.entries(env)) {
587+
this.addEnvironment(key, value);
588+
}
586589

587590
this.deadLetterQueue = this.buildDeadLetterQueue(props);
588591

@@ -675,9 +678,10 @@ export class Function extends FunctionBase {
675678
* If this is a ref to a Lambda function, this operation results in a no-op.
676679
* @param key The environment variable key.
677680
* @param value The environment variable's value.
681+
* @param options Environment variable options.
678682
*/
679-
public addEnvironment(key: string, value: string): this {
680-
this.environment[key] = value;
683+
public addEnvironment(key: string, value: string, options?: EnvironmentOptions): this {
684+
this.environment[key] = { value, ...options };
681685
return this;
682686
}
683687

@@ -764,27 +768,43 @@ export class Function extends FunctionBase {
764768
return this._logGroup;
765769
}
766770

771+
/** @internal */
772+
public _checkEdgeCompatibility(): void {
773+
// Check env vars
774+
const envEntries = Object.entries(this.environment);
775+
for (const [key, config] of envEntries) {
776+
if (config.removeInEdge) {
777+
delete this.environment[key];
778+
this.node.addInfo(`Removed ${key} environment variable for Lambda@Edge compatibility`);
779+
}
780+
}
781+
const envKeys = Object.keys(this.environment);
782+
if (envKeys.length !== 0) {
783+
throw new Error(`The function ${this.node.path} contains environment variables [${envKeys}] and is not compatible with Lambda@Edge. \
784+
Environment variables can be marked for removal when used in Lambda@Edge by setting the \'removeInEdge\' property in the \'addEnvironment()\' API.`);
785+
}
786+
787+
return;
788+
}
789+
767790
private renderEnvironment() {
768791
if (!this.environment || Object.keys(this.environment).length === 0) {
769792
return undefined;
770793
}
771794

772-
// for backwards compatibility we do not sort environment variables in case
773-
// _currentVersion is not defined. otherwise, this would have invalidated
795+
const variables: { [key: string]: string } = {};
796+
// Sort environment so the hash of the function used to create
797+
// `currentVersion` is not affected by key order (this is how lambda does
798+
// it). For backwards compatibility we do not sort environment variables in case
799+
// _currentVersion is not defined. Otherwise, this would have invalidated
774800
// the template, and for example, may cause unneeded updates for nested
775801
// stacks.
776-
if (!this._currentVersion) {
777-
return {
778-
variables: this.environment,
779-
};
780-
}
802+
const keys = this._currentVersion
803+
? Object.keys(this.environment).sort()
804+
: Object.keys(this.environment);
781805

782-
// sort environment so the hash of the function used to create
783-
// `currentVersion` is not affected by key order (this is how lambda does
784-
// it).
785-
const variables: { [key: string]: string } = {};
786-
for (const key of Object.keys(this.environment).sort()) {
787-
variables[key] = this.environment[key];
806+
for (const key of keys) {
807+
variables[key] = this.environment[key].value;
788808
}
789809

790810
return { variables };
@@ -905,6 +925,27 @@ export class Function extends FunctionBase {
905925
}
906926
}
907927

928+
/**
929+
* Environment variables options
930+
*/
931+
export interface EnvironmentOptions {
932+
/**
933+
* When used in Lambda@Edge via edgeArn() API, these environment
934+
* variables will be removed. If not set, an error will be thrown.
935+
* @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html#lambda-requirements-lambda-function-configuration
936+
*
937+
* @default false - using the function in Lambda@Edge will throw
938+
*/
939+
readonly removeInEdge?: boolean
940+
}
941+
942+
/**
943+
* Configuration for an environment variable
944+
*/
945+
interface EnvironmentConfig extends EnvironmentOptions {
946+
readonly value: string;
947+
}
948+
908949
/**
909950
* Given an opaque (token) ARN, returns a CloudFormation expression that extracts the function
910951
* name from the ARN.

0 commit comments

Comments
 (0)