Skip to content

Commit

Permalink
feat(cloudfront): support Lambda@Edge for behaviors (#9220)
Browse files Browse the repository at this point in the history
This change adds support for support for defining function associations when adding
both default and additional behaviors to the Distribution.

Fixes #9108

----

*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 Jul 24, 2020
1 parent e6dca52 commit d3e5533
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 25 deletions.
61 changes: 61 additions & 0 deletions packages/@aws-cdk/aws-cloudfront/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,67 @@ new cloudfront.Distribution(this, 'myDist', {
});
```

### Lambda@Edge

Lambda@Edge is an extension of AWS Lambda, a compute service that lets you execute functions that customize the content that CloudFront delivers.
You can author Node.js or Python functions in the US East (N. Virginia) region,
and then execute them in AWS locations globally that are closer to the viewer,
without provisioning or managing servers.
Lambda@Edge functions are associated with a specific behavior and event type.
Lambda@Edge can be used rewrite URLs,
alter responses based on headers or cookies,
or authorize requests based on headers or authorization tokens.

The following shows a Lambda@Edge function added to the default behavior and triggered on every request:

```typescript
const myFunc = new lambda.Function(...);
new cloudfront.Distribution(this, 'myDist', {
defaultBehavior: {
origin: new origins.S3Origin(myBucket),
edgeLambdas: [
{
functionVersion: myFunc.currentVersion,
eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
}
],
},
});
```

Lambda@Edge functions can also be associated with additional behaviors,
either at Distribution creation time,
or after.

```typescript
// assigning at Distribution creation
const myOrigin = new origins.S3Origin(myBucket);
new cloudfront.Distribution(this, 'myDist', {
defaultBehavior: { origin: myOrigin },
additionalBehaviors: {
'images/*': {
origin: myOrigin,
edgeLambdas: [
{
functionVersion: myFunc.currentVersion,
eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
},
],
},
},
});

// assigning after creation
myDistribution.addBehavior('images/*', myOrigin, {
edgeLambdas: [
{
functionVersion: myFunc.currentVersion,
eventType: cloudfront.LambdaEdgeEventType.VIEWER_RESPONSE,
},
],
});
```

## CloudFrontWebDistribution API - Stable

![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge)
Expand Down
52 changes: 52 additions & 0 deletions packages/@aws-cdk/aws-cloudfront/lib/distribution.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as acm from '@aws-cdk/aws-certificatemanager';
import * as lambda from '@aws-cdk/aws-lambda';
import { Construct, IResource, Lazy, Resource, Stack, Token, Duration } from '@aws-cdk/core';
import { CfnDistribution } from './cloudfront.generated';
import { Origin } from './origin';
Expand Down Expand Up @@ -351,6 +352,49 @@ export interface ErrorResponse {
readonly responsePagePath?: string;
}

/**
* The type of events that a Lambda@Edge function can be invoked in response to.
*/
export enum LambdaEdgeEventType {
/**
* The origin-request specifies the request to the
* origin location (e.g. S3)
*/
ORIGIN_REQUEST = 'origin-request',

/**
* The origin-response specifies the response from the
* origin location (e.g. S3)
*/
ORIGIN_RESPONSE = 'origin-response',

/**
* The viewer-request specifies the incoming request
*/
VIEWER_REQUEST = 'viewer-request',

/**
* The viewer-response specifies the outgoing reponse
*/
VIEWER_RESPONSE = 'viewer-response',
}

/**
* Represents a Lambda function version and event type when using Lambda@Edge.
* The type of the {@link AddBehaviorOptions.edgeLambdas} property.
*/
export interface EdgeLambda {
/**
* The version of the Lambda function that will be invoked.
*
* **Note**: it's not possible to use the '$LATEST' function version for Lambda@Edge!
*/
readonly functionVersion: lambda.IVersion;

/** The type of event in response to which should the function be invoked. */
readonly eventType: LambdaEdgeEventType;
}

/**
* Options for adding a new behavior to a Distribution.
*
Expand Down Expand Up @@ -380,6 +424,14 @@ export interface AddBehaviorOptions {
* @default []
*/
readonly forwardQueryStringCacheKeys?: string[];

/**
* The Lambda@Edge functions to invoke before serving the contents.
*
* @default - no Lambda functions will be invoked
* @see https://aws.amazon.com/lambda/edge
*/
readonly edgeLambdas?: EdgeLambda[];
}

/**
Expand Down
12 changes: 11 additions & 1 deletion packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,17 @@ export class CacheBehavior {
queryStringCacheKeys: this.props.forwardQueryStringCacheKeys,
},
viewerProtocolPolicy: ViewerProtocolPolicy.ALLOW_ALL,
lambdaFunctionAssociations: this.props.edgeLambdas
? this.props.edgeLambdas.map(edgeLambda => {
if (edgeLambda.functionVersion.version === '$LATEST') {
throw new Error('$LATEST function version cannot be used for Lambda@Edge');
}
return {
lambdaFunctionArn: edgeLambda.functionVersion.functionArn,
eventType: edgeLambda.eventType.toString(),
};
})
: undefined,
};
}

}
23 changes: 1 addition & 22 deletions packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as lambda from '@aws-cdk/aws-lambda';
import * as s3 from '@aws-cdk/aws-s3';
import * as cdk from '@aws-cdk/core';
import { CfnDistribution } from './cloudfront.generated';
import { IDistribution, OriginProtocolPolicy, PriceClass, ViewerProtocolPolicy, SSLMethod, SecurityPolicyProtocol } from './distribution';
import { IDistribution, LambdaEdgeEventType, OriginProtocolPolicy, PriceClass, ViewerProtocolPolicy, SSLMethod, SecurityPolicyProtocol } from './distribution';
import { IOriginAccessIdentity } from './origin_access_identity';

export enum HttpVersion {
Expand Down Expand Up @@ -430,27 +430,6 @@ export interface LambdaFunctionAssociation {
readonly lambdaFunction: lambda.IVersion;
}

export enum LambdaEdgeEventType {
/**
* The origin-request specifies the request to the
* origin location (e.g. S3)
*/
ORIGIN_REQUEST = 'origin-request',
/**
* The origin-response specifies the response from the
* origin location (e.g. S3)
*/
ORIGIN_RESPONSE = 'origin-response',
/**
* The viewer-request specifies the incoming request
*/
VIEWER_REQUEST = 'viewer-request',
/**
* The viewer-response specifies the outgoing reponse
*/
VIEWER_RESPONSE = 'viewer-response',
}

export interface ViewerCertificateOptions {
/**
* How CloudFront should serve HTTPS requests.
Expand Down
1 change: 0 additions & 1 deletion packages/@aws-cdk/aws-cloudfront/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@
"docs-public-apis:@aws-cdk/aws-cloudfront.HttpVersion",
"docs-public-apis:@aws-cdk/aws-cloudfront.HttpVersion.HTTP1_1",
"docs-public-apis:@aws-cdk/aws-cloudfront.HttpVersion.HTTP2",
"docs-public-apis:@aws-cdk/aws-cloudfront.LambdaEdgeEventType",
"docs-public-apis:@aws-cdk/aws-cloudfront.OriginSslPolicy",
"docs-public-apis:@aws-cdk/aws-cloudfront.OriginSslPolicy.SSL_V3",
"docs-public-apis:@aws-cdk/aws-cloudfront.OriginSslPolicy.TLS_V1",
Expand Down
98 changes: 97 additions & 1 deletion packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import '@aws-cdk/assert/jest';
import * as acm from '@aws-cdk/aws-certificatemanager';
import * as lambda from '@aws-cdk/aws-lambda';
import * as s3 from '@aws-cdk/aws-s3';
import { App, Duration, Stack } from '@aws-cdk/core';
import { Distribution, Origin, PriceClass, S3Origin } from '../lib';
import { Distribution, LambdaEdgeEventType, Origin, PriceClass, S3Origin } from '../lib';

let app: App;
let stack: Stack;
Expand Down Expand Up @@ -288,6 +289,101 @@ describe('custom error responses', () => {

});

describe('with Lambda@Edge functions', () => {
let lambdaFunction: lambda.Function;
let origin: Origin;

beforeEach(() => {
lambdaFunction = new lambda.Function(stack, 'Function', {
runtime: lambda.Runtime.NODEJS,
code: lambda.Code.fromInline('whatever'),
handler: 'index.handler',
});

origin = defaultS3Origin();
});

test('can add an edge lambdas to the default behavior', () => {
new Distribution(stack, 'MyDist', {
defaultBehavior: {
origin,
edgeLambdas: [
{
functionVersion: lambdaFunction.currentVersion,
eventType: LambdaEdgeEventType.ORIGIN_REQUEST,
},
],
},
});

expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', {
DistributionConfig: {
DefaultCacheBehavior: {
LambdaFunctionAssociations: [
{
EventType: 'origin-request',
LambdaFunctionARN: {
Ref: 'FunctionCurrentVersion4E2B22619c0305f954e58f25575548280c0a3629',
},
},
],
},
},
});
});

test('can add an edge lambdas to additional behaviors', () => {
new Distribution(stack, 'MyDist', {
defaultBehavior: { origin },
additionalBehaviors: {
'images/*': {
origin,
edgeLambdas: [
{
functionVersion: lambdaFunction.currentVersion,
eventType: LambdaEdgeEventType.VIEWER_REQUEST,
},
],
},
},
});

expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', {
DistributionConfig: {
CacheBehaviors: [
{
PathPattern: 'images/*',
LambdaFunctionAssociations: [
{
EventType: 'viewer-request',
LambdaFunctionARN: {
Ref: 'FunctionCurrentVersion4E2B22619c0305f954e58f25575548280c0a3629',
},
},
],
},
],
},
});
});

test('fails creation when attempting to add the $LATEST function version as an edge Lambda to the default behavior', () => {
expect(() => {
new Distribution(stack, 'MyDist', {
defaultBehavior: {
origin,
edgeLambdas: [
{
functionVersion: lambdaFunction.latestVersion,
eventType: LambdaEdgeEventType.ORIGIN_RESPONSE,
},
],
},
});
}).toThrow(/\$LATEST function version cannot be used for Lambda@Edge/);
});
});

test('price class is included if provided', () => {
const origin = defaultS3Origin();
new Distribution(stack, 'Dist', {
Expand Down

0 comments on commit d3e5533

Please sign in to comment.