Skip to content

Commit

Permalink
feat(cloudfront): add L2 support for CloudFront functions (#14511)
Browse files Browse the repository at this point in the history
Add support for CloudFront functions

/cc @njlynch 

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
hoegertn authored Jun 1, 2021
1 parent 882508c commit 40d2ff9
Show file tree
Hide file tree
Showing 11 changed files with 537 additions and 14 deletions.
22 changes: 22 additions & 0 deletions packages/@aws-cdk/aws-cloudfront/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,28 @@ new cloudfront.Distribution(this, 'distro', {
});
```

### CloudFront Function

You can also deploy CloudFront functions and add them to a CloudFront distribution.

```ts
const cfFunction = new cloudfront.Function(stack, 'Function', {
code: cloudfront.FunctionCode.fromInline('function handler(event) { return event.request }'),
});

new cloudfront.Distribution(stack, 'distro', {
defaultBehavior: {
origin: new origins.S3Origin(s3Bucket),
functionAssociations: [{
function: cfFunction,
eventType: cloudfront.FunctionEventType.VIEWER_REQUEST,
}],
},
});
```

It will auto-generate the name of the function and deploy it to the `live` stage.

### Logging

You can configure CloudFront to create log files that contain detailed information about every user request that CloudFront receives.
Expand Down
10 changes: 9 additions & 1 deletion packages/@aws-cdk/aws-cloudfront/lib/distribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { IResource, Lazy, Resource, Stack, Token, Duration, Names } from '@aws-c
import { Construct } from 'constructs';
import { ICachePolicy } from './cache-policy';
import { CfnDistribution } from './cloudfront.generated';
import { FunctionAssociation } from './function';
import { GeoRestriction } from './geo-restriction';
import { IKeyGroup } from './key-group';
import { IOrigin, OriginBindConfig, OriginBindOptions } from './origin';
Expand Down Expand Up @@ -445,7 +446,7 @@ export class Distribution extends Resource implements IDistribution {
}

private renderViewerCertificate(certificate: acm.ICertificate,
minimumProtocolVersion: SecurityPolicyProtocol = SecurityPolicyProtocol.TLS_V1_2_2019) : CfnDistribution.ViewerCertificateProperty {
minimumProtocolVersion: SecurityPolicyProtocol = SecurityPolicyProtocol.TLS_V1_2_2019): CfnDistribution.ViewerCertificateProperty {
return {
acmCertificateArn: certificate.certificateArn,
sslSupportMethod: SSLMethod.SNI,
Expand Down Expand Up @@ -706,6 +707,13 @@ export interface AddBehaviorOptions {
*/
readonly viewerProtocolPolicy?: ViewerProtocolPolicy;

/**
* The CloudFront functions to invoke before serving the contents.
*
* @default - no functions will be invoked
*/
readonly functionAssociations?: FunctionAssociation[];

/**
* The Lambda@Edge functions to invoke before serving the contents.
*
Expand Down
180 changes: 180 additions & 0 deletions packages/@aws-cdk/aws-cloudfront/lib/function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { IResource, Names, Resource, Stack } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnFunction } from './cloudfront.generated';

/**
* Represents the function's source code
*/
export abstract class FunctionCode {

/**
* Inline code for function
* @returns `InlineCode` with inline code.
* @param code The actual function code
*/
public static fromInline(code: string): FunctionCode {
return new InlineCode(code);
}

/**
* renders the function code
*/
public abstract render(): string;
}

/**
* Represents the function's source code as inline code
*/
class InlineCode extends FunctionCode {

constructor(private code: string) {
super();
}

public render(): string {
return this.code;
}
}

/**
* Represents a CloudFront Function
*/
export interface IFunction extends IResource {
/**
* The name of the function.
* @attribute
*/
readonly functionName: string;

/**
* The ARN of the function.
* @attribute
*/
readonly functionArn: string;
}

/**
* Attributes of an existing CloudFront Function to import it
*/
export interface FunctionAttributes {
/**
* The name of the function.
*/
readonly functionName: string;

/**
* The ARN of the function.
*/
readonly functionArn: string;
}

/**
* Properties for creating a CloudFront Function
*/
export interface FunctionProps {
/**
* A name to identify the function.
* @default - generated from the `id`
*/
readonly functionName?: string;

/**
* A comment to describe the function.
* @default - same as `functionName`
*/
readonly comment?: string;

/**
* The source code of the function.
*/
readonly code: FunctionCode;
}

/**
* A CloudFront Function
*
* @resource AWS::CloudFront::Function
*/
export class Function extends Resource implements IFunction {

/** Imports a function by its name and ARN */
public static fromFunctionAttributes(scope: Construct, id: string, attrs: FunctionAttributes): IFunction {
return new class extends Resource implements IFunction {
public readonly functionName = attrs.functionName;
public readonly functionArn = attrs.functionArn;
}(scope, id);
}

/**
* the name of the CloudFront function
* @attribute
*/
public readonly functionName: string;
/**
* the ARN of the CloudFront function
* @attribute
*/
public readonly functionArn: string;
/**
* the deployment stage of the CloudFront function
* @attribute
*/
public readonly functionStage: string;

constructor(scope: Construct, id: string, props: FunctionProps) {
super(scope, id);

this.functionName = props.functionName ?? this.generateName();

const resource = new CfnFunction(this, 'Resource', {
autoPublish: true,
functionCode: props.code.render(),
functionConfig: {
comment: props.comment ?? this.functionName,
runtime: 'cloudfront-js-1.0',
},
name: this.functionName,
});

this.functionArn = resource.attrFunctionArn;
this.functionStage = resource.attrStage;
}

private generateName(): string {
const name = Stack.of(this).region + Names.uniqueId(this);
if (name.length > 64) {
return name.substring(0, 32) + name.substring(name.length - 32);
}
return name;
}
}

/**
* The type of events that a CloudFront function can be invoked in response to.
*/
export enum FunctionEventType {

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

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

/**
* Represents a CloudFront function and event type when using CF Functions.
* The type of the {@link AddBehaviorOptions.functionAssociations} property.
*/
export interface FunctionAssociation {
/**
* The CloudFront function that will be invoked.
*/
readonly function: IFunction;

/** The type of event which should invoke the function. */
readonly eventType: FunctionEventType;
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-cloudfront/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './cache-policy';
export * from './distribution';
export * from './function';
export * from './geo-restriction';
export * from './key-group';
export * from './origin';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export class CacheBehavior {
originRequestPolicyId: this.props.originRequestPolicy?.originRequestPolicyId,
smoothStreaming: this.props.smoothStreaming,
viewerProtocolPolicy: this.props.viewerProtocolPolicy ?? ViewerProtocolPolicy.ALLOW_ALL,
functionAssociations: this.props.functionAssociations?.map(association => ({
functionArn: association.function.functionArn,
eventType: association.eventType.toString(),
})),
lambdaFunctionAssociations: this.props.edgeLambdas?.map(edgeLambda => ({
lambdaFunctionArn: edgeLambda.functionVersion.edgeArn,
eventType: edgeLambda.eventType.toString(),
Expand Down
40 changes: 28 additions & 12 deletions packages/@aws-cdk/aws-cloudfront/lib/web-distribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as cdk from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnDistribution } from './cloudfront.generated';
import { HttpVersion, IDistribution, LambdaEdgeEventType, OriginProtocolPolicy, PriceClass, ViewerProtocolPolicy, SSLMethod, SecurityPolicyProtocol } from './distribution';
import { FunctionAssociation } from './function';
import { GeoRestriction } from './geo-restriction';
import { IKeyGroup } from './key-group';
import { IOriginAccessIdentity } from './origin-access-identity';
Expand Down Expand Up @@ -422,6 +423,13 @@ export interface Behavior {
*/
readonly lambdaFunctionAssociations?: LambdaFunctionAssociation[];

/**
* The CloudFront functions to invoke before serving the contents.
*
* @default - no functions will be invoked
*/
readonly functionAssociations?: FunctionAssociation[];

}

export interface LambdaFunctionAssociation {
Expand Down Expand Up @@ -771,9 +779,9 @@ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribu

// Comments have an undocumented limit of 128 characters
const trimmedComment =
props.comment && props.comment.length > 128
? `${props.comment.substr(0, 128 - 3)}...`
: props.comment;
props.comment && props.comment.length > 128
? `${props.comment.substr(0, 128 - 3)}...`
: props.comment;

let distributionConfig: CfnDistribution.DistributionConfigProperty = {
comment: trimmedComment,
Expand Down Expand Up @@ -957,6 +965,14 @@ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribu
if (!input.isDefaultBehavior) {
toReturn = Object.assign(toReturn, { pathPattern: input.pathPattern });
}
if (input.functionAssociations) {
toReturn = Object.assign(toReturn, {
functionAssociations: input.functionAssociations.map(association => ({
functionArn: association.function.functionArn,
eventType: association.eventType.toString(),
})),
});
}
if (input.lambdaFunctionAssociations) {
const includeBodyEventTypes = [LambdaEdgeEventType.ORIGIN_REQUEST, LambdaEdgeEventType.VIEWER_REQUEST];
if (input.lambdaFunctionAssociations.some(fna => fna.includeBody && !includeBodyEventTypes.includes(fna.eventType))) {
Expand Down Expand Up @@ -1069,23 +1085,23 @@ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribu
: originConfig.customOriginSource!.domainName,
originPath: originConfig.originPath ?? originConfig.customOriginSource?.originPath ?? originConfig.s3OriginSource?.originPath,
originCustomHeaders:
originHeaders.length > 0 ? originHeaders : undefined,
originHeaders.length > 0 ? originHeaders : undefined,
s3OriginConfig,
customOriginConfig: originConfig.customOriginSource
? {
httpPort: originConfig.customOriginSource.httpPort || 80,
httpsPort: originConfig.customOriginSource.httpsPort || 443,
originKeepaliveTimeout:
(originConfig.customOriginSource.originKeepaliveTimeout &&
originConfig.customOriginSource.originKeepaliveTimeout.toSeconds()) ||
5,
(originConfig.customOriginSource.originKeepaliveTimeout &&
originConfig.customOriginSource.originKeepaliveTimeout.toSeconds()) ||
5,
originReadTimeout:
(originConfig.customOriginSource.originReadTimeout &&
originConfig.customOriginSource.originReadTimeout.toSeconds()) ||
30,
(originConfig.customOriginSource.originReadTimeout &&
originConfig.customOriginSource.originReadTimeout.toSeconds()) ||
30,
originProtocolPolicy:
originConfig.customOriginSource.originProtocolPolicy ||
OriginProtocolPolicy.HTTPS_ONLY,
originConfig.customOriginSource.originProtocolPolicy ||
OriginProtocolPolicy.HTTPS_ONLY,
originSslProtocols: originConfig.customOriginSource
.allowedOriginSSLVersions || [OriginSslPolicy.TLS_V1_2],
}
Expand Down
40 changes: 39 additions & 1 deletion packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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 { CfnDistribution, Distribution, GeoRestriction, HttpVersion, IOrigin, LambdaEdgeEventType, PriceClass, SecurityPolicyProtocol } from '../lib';
import { CfnDistribution, Distribution, Function, FunctionCode, FunctionEventType, GeoRestriction, HttpVersion, IOrigin, LambdaEdgeEventType, PriceClass, SecurityPolicyProtocol } from '../lib';
import { defaultOrigin, defaultOriginGroup } from './test-origin';

let app: App;
Expand Down Expand Up @@ -730,6 +730,44 @@ describe('with Lambda@Edge functions', () => {
});
});

describe('with CloudFront functions', () => {

test('can add a CloudFront function to the default behavior', () => {
new Distribution(stack, 'MyDist', {
defaultBehavior: {
origin: defaultOrigin(),
functionAssociations: [
{
eventType: FunctionEventType.VIEWER_REQUEST,
function: new Function(stack, 'TestFunction', {
code: FunctionCode.fromInline('foo'),
}),
},
],
},
});

expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', {
DistributionConfig: {
DefaultCacheBehavior: {
FunctionAssociations: [
{
EventType: 'viewer-request',
FunctionARN: {
'Fn::GetAtt': [
'TestFunction22AD90FC',
'FunctionARN',
],
},
},
],
},
},
});
});

});

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

0 comments on commit 40d2ff9

Please sign in to comment.