From 1721ce6f40ec32c6db5ca3196242d9e4eb7e8bce Mon Sep 17 00:00:00 2001 From: Roman Bobrovskiy Date: Wed, 27 Nov 2024 21:11:08 +0100 Subject: [PATCH 1/2] feat: added cache fragment header to response --- src/cacheHandler/strategy/s3.ts | 3 ++ src/cdk/constructs/CloudFrontDistribution.ts | 15 +++++++- .../constructs/ViewerResponseLambdaEdge.ts | 35 +++++++++++++++++++ src/cdk/stacks/NextCloudfrontStack.ts | 8 +++++ src/lambdas/viewerResponse.ts | 28 +++++++++++++++ 5 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/cdk/constructs/ViewerResponseLambdaEdge.ts create mode 100644 src/lambdas/viewerResponse.ts diff --git a/src/cacheHandler/strategy/s3.ts b/src/cacheHandler/strategy/s3.ts index a4753a2..33f02d5 100644 --- a/src/cacheHandler/strategy/s3.ts +++ b/src/cacheHandler/strategy/s3.ts @@ -66,6 +66,9 @@ export class S3Cache implements CacheStrategy { const baseInput: PutObjectCommandInput = { Bucket: this.bucketName, Key: `${pageKey}/${cacheKey}`, + Metadata: { + 'Cache-Fragment-Key': cacheKey + }, ...(data.revalidate ? { CacheControl: `max-age=${data.revalidate}` } : undefined) } diff --git a/src/cdk/constructs/CloudFrontDistribution.ts b/src/cdk/constructs/CloudFrontDistribution.ts index c874427..6d09b06 100644 --- a/src/cdk/constructs/CloudFrontDistribution.ts +++ b/src/cdk/constructs/CloudFrontDistribution.ts @@ -12,6 +12,7 @@ interface CloudFrontPropsDistribution { renderServerDomain: string requestEdgeFunction: cloudfront.experimental.EdgeFunction responseEdgeFunction: cloudfront.experimental.EdgeFunction + viewerResponseEdgeFunction: cloudfront.experimental.EdgeFunction cacheConfig: CacheConfig imageTTL?: number } @@ -29,7 +30,15 @@ export class CloudFrontDistribution extends Construct { constructor(scope: Construct, id: string, props: CloudFrontPropsDistribution) { super(scope, id) - const { staticBucket, requestEdgeFunction, responseEdgeFunction, cacheConfig, renderServerDomain, imageTTL } = props + const { + staticBucket, + requestEdgeFunction, + responseEdgeFunction, + viewerResponseEdgeFunction, + cacheConfig, + renderServerDomain, + imageTTL + } = props const splitCachePolicy = new cloudfront.CachePolicy(this, 'SplitCachePolicy', { cachePolicyName: `${id}-SplitCachePolicy`, @@ -86,6 +95,10 @@ export class CloudFrontDistribution extends Construct { { functionVersion: responseEdgeFunction.currentVersion, eventType: cloudfront.LambdaEdgeEventType.ORIGIN_RESPONSE + }, + { + functionVersion: viewerResponseEdgeFunction.currentVersion, + eventType: cloudfront.LambdaEdgeEventType.VIEWER_RESPONSE } ], cachePolicy: splitCachePolicy diff --git a/src/cdk/constructs/ViewerResponseLambdaEdge.ts b/src/cdk/constructs/ViewerResponseLambdaEdge.ts new file mode 100644 index 0000000..a8d2ddc --- /dev/null +++ b/src/cdk/constructs/ViewerResponseLambdaEdge.ts @@ -0,0 +1,35 @@ +import { Construct } from 'constructs' +import * as lambda from 'aws-cdk-lib/aws-lambda' +import * as cloudfront from 'aws-cdk-lib/aws-cloudfront' +import path from 'node:path' +import { buildLambda } from '../../build/edge' + +const NodeJSEnvironmentMapping: Record = { + '18': lambda.Runtime.NODEJS_18_X, + '20': lambda.Runtime.NODEJS_20_X +} + +interface ViewerResponseLambdaEdgeProps { + nodejs?: string + buildOutputPath: string +} + +export class ViewerResponseLambdaEdge extends Construct { + public readonly lambdaEdge: cloudfront.experimental.EdgeFunction + + constructor(scope: Construct, id: string, props: ViewerResponseLambdaEdgeProps) { + const { nodejs, buildOutputPath } = props + super(scope, id) + + const nodeJSEnvironment = NodeJSEnvironmentMapping[nodejs ?? ''] ?? NodeJSEnvironmentMapping['20'] + const name = 'viewerResponse' + + buildLambda(name, buildOutputPath) + + this.lambdaEdge = new cloudfront.experimental.EdgeFunction(this, 'ViewerResponseLambdaEdge', { + runtime: nodeJSEnvironment, + code: lambda.Code.fromAsset(path.join(buildOutputPath, 'server-functions', name)), + handler: 'index.handler' + }) + } +} diff --git a/src/cdk/stacks/NextCloudfrontStack.ts b/src/cdk/stacks/NextCloudfrontStack.ts index a16c687..52b4042 100644 --- a/src/cdk/stacks/NextCloudfrontStack.ts +++ b/src/cdk/stacks/NextCloudfrontStack.ts @@ -5,6 +5,7 @@ import { RoutingLambdaEdge } from '../constructs/RoutingLambdaEdge' import { CloudFrontDistribution } from '../constructs/CloudFrontDistribution' import { CacheConfig } from '../../types' import { CheckExpirationLambdaEdge } from '../constructs/CheckExpirationLambdaEdge' +import { ViewerResponseLambdaEdge } from '../constructs/ViewerResponseLambdaEdge' export interface NextCloudfrontStackProps extends StackProps { nodejs?: string @@ -21,6 +22,7 @@ export interface NextCloudfrontStackProps extends StackProps { export class NextCloudfrontStack extends Stack { public readonly routingLambdaEdge: RoutingLambdaEdge public readonly checkExpLambdaEdge: CheckExpirationLambdaEdge + public readonly viewerResponseLambdaEdge: ViewerResponseLambdaEdge public readonly cloudfront: CloudFrontDistribution constructor(scope: Construct, id: string, props: NextCloudfrontStackProps) { @@ -55,6 +57,11 @@ export class NextCloudfrontStack extends Stack { region }) + this.viewerResponseLambdaEdge = new ViewerResponseLambdaEdge(this, `${id}-ViewerResponseLambdaEdge`, { + nodejs, + buildOutputPath + }) + const staticBucket = s3.Bucket.fromBucketAttributes(this, `${id}-StaticAssetsBucket`, { bucketName: staticBucketName, region @@ -65,6 +72,7 @@ export class NextCloudfrontStack extends Stack { renderServerDomain, requestEdgeFunction: this.routingLambdaEdge.lambdaEdge, responseEdgeFunction: this.checkExpLambdaEdge.lambdaEdge, + viewerResponseEdgeFunction: this.viewerResponseLambdaEdge.lambdaEdge, cacheConfig, imageTTL }) diff --git a/src/lambdas/viewerResponse.ts b/src/lambdas/viewerResponse.ts new file mode 100644 index 0000000..fb20079 --- /dev/null +++ b/src/lambdas/viewerResponse.ts @@ -0,0 +1,28 @@ +import type { CloudFrontRequestCallback, Context, CloudFrontResponseEvent } from 'aws-lambda' + +/** + * Lambda@Edge viewer response handler that processes CloudFront responses. + * This handler extracts the cache fragment key from x-amz-meta headers and + * sets it as a standard Cache-Fragment-Key header while removing the original + * x-amz-meta header. + * + * @param {CloudFrontResponseEvent} event - The CloudFront response event object + * @param {Context} _context - AWS Lambda context object (unused) + * @param {CloudFrontRequestCallback} callback - Callback to return the modified response + * @returns {Promise} - Returns nothing, uses callback to return response + */ +export const handler = async ( + event: CloudFrontResponseEvent, + _context: Context, + callback: CloudFrontRequestCallback +) => { + const response = event.Records[0].cf.response + const fileCacheKey = response.headers['x-amz-meta-cache-fragment-key']?.[0].value + + if (fileCacheKey) { + response.headers['cache-fragment-key'] = [{ key: 'Cache-Fragment-Key', value: fileCacheKey }] + response.headers['x-amz-meta-cache-fragment-key'] = [] + } + + callback(null, response) +} From 52ae047acd1b3c18745e030267a14d1fd236108e Mon Sep 17 00:00:00 2001 From: Roman Bobrovskiy Date: Thu, 28 Nov 2024 16:27:45 +0100 Subject: [PATCH 2/2] feat: updated naming for edge functions --- ...mbdaEdge.ts => OriginRequestLambdaEdge.ts} | 14 ++++++------- ...bdaEdge.ts => OriginResponseLambdaEdge.ts} | 14 ++++++------- src/cdk/stacks/NextCloudfrontStack.ts | 20 +++++++++---------- .../{edgeRouting.ts => originRequest.ts} | 0 .../{checkExpiration.ts => originResponse.ts} | 0 5 files changed, 24 insertions(+), 24 deletions(-) rename src/cdk/constructs/{RoutingLambdaEdge.ts => OriginRequestLambdaEdge.ts} (82%) rename src/cdk/constructs/{CheckExpirationLambdaEdge.ts => OriginResponseLambdaEdge.ts} (81%) rename src/lambdas/{edgeRouting.ts => originRequest.ts} (100%) rename src/lambdas/{checkExpiration.ts => originResponse.ts} (100%) diff --git a/src/cdk/constructs/RoutingLambdaEdge.ts b/src/cdk/constructs/OriginRequestLambdaEdge.ts similarity index 82% rename from src/cdk/constructs/RoutingLambdaEdge.ts rename to src/cdk/constructs/OriginRequestLambdaEdge.ts index 09e3623..4d4815f 100644 --- a/src/cdk/constructs/RoutingLambdaEdge.ts +++ b/src/cdk/constructs/OriginRequestLambdaEdge.ts @@ -8,7 +8,7 @@ import path from 'node:path' import { buildLambda } from '../../build/edge' import { CacheConfig } from '../../types' -interface RoutingLambdaEdgeProps extends cdk.StackProps { +interface OriginRequestLambdaEdgeProps extends cdk.StackProps { bucketName: string renderServerDomain: string buildOutputPath: string @@ -22,15 +22,15 @@ const NodeJSEnvironmentMapping: Record = { '20': lambda.Runtime.NODEJS_20_X } -export class RoutingLambdaEdge extends Construct { +export class OriginRequestLambdaEdge extends Construct { public readonly lambdaEdge: cloudfront.experimental.EdgeFunction - constructor(scope: Construct, id: string, props: RoutingLambdaEdgeProps) { + constructor(scope: Construct, id: string, props: OriginRequestLambdaEdgeProps) { const { bucketName, bucketRegion, renderServerDomain, nodejs, buildOutputPath, cacheConfig } = props super(scope, id) const nodeJSEnvironment = NodeJSEnvironmentMapping[nodejs ?? ''] ?? NodeJSEnvironmentMapping['20'] - const name = 'edgeRouting' + const name = 'originRequest' buildLambda(name, buildOutputPath, { define: { @@ -41,13 +41,13 @@ export class RoutingLambdaEdge extends Construct { } }) - const logGroup = new logs.LogGroup(this, 'RoutingLambdaEdgeLogGroup', { - logGroupName: `/aws/lambda/${id}-edgeRouting`, + const logGroup = new logs.LogGroup(this, 'OriginRequestLambdaEdgeLogGroup', { + logGroupName: `/aws/lambda/${id}-originRequest`, removalPolicy: cdk.RemovalPolicy.DESTROY, retention: logs.RetentionDays.ONE_DAY }) - this.lambdaEdge = new cloudfront.experimental.EdgeFunction(this, 'RoutingLambdaEdge', { + this.lambdaEdge = new cloudfront.experimental.EdgeFunction(this, 'OriginRequestLambdaEdge', { runtime: nodeJSEnvironment, code: lambda.Code.fromAsset(path.join(buildOutputPath, 'server-functions', name)), handler: 'index.handler', diff --git a/src/cdk/constructs/CheckExpirationLambdaEdge.ts b/src/cdk/constructs/OriginResponseLambdaEdge.ts similarity index 81% rename from src/cdk/constructs/CheckExpirationLambdaEdge.ts rename to src/cdk/constructs/OriginResponseLambdaEdge.ts index 4af49bc..1b4ac38 100644 --- a/src/cdk/constructs/CheckExpirationLambdaEdge.ts +++ b/src/cdk/constructs/OriginResponseLambdaEdge.ts @@ -8,7 +8,7 @@ import path from 'node:path' import { buildLambda } from '../../build/edge' import { CacheConfig } from '../../types' -interface CheckExpirationLambdaEdgeProps extends cdk.StackProps { +interface OriginResponseLambdaEdgeProps extends cdk.StackProps { renderWorkerQueueUrl: string renderWorkerQueueArn: string buildOutputPath: string @@ -22,15 +22,15 @@ const NodeJSEnvironmentMapping: Record = { '20': lambda.Runtime.NODEJS_20_X } -export class CheckExpirationLambdaEdge extends Construct { +export class OriginResponseLambdaEdge extends Construct { public readonly lambdaEdge: cloudfront.experimental.EdgeFunction - constructor(scope: Construct, id: string, props: CheckExpirationLambdaEdgeProps) { + constructor(scope: Construct, id: string, props: OriginResponseLambdaEdgeProps) { const { nodejs, buildOutputPath, cacheConfig, renderWorkerQueueUrl, renderWorkerQueueArn, region } = props super(scope, id) const nodeJSEnvironment = NodeJSEnvironmentMapping[nodejs ?? ''] ?? NodeJSEnvironmentMapping['20'] - const name = 'checkExpiration' + const name = 'originResponse' buildLambda(name, buildOutputPath, { define: { @@ -40,13 +40,13 @@ export class CheckExpirationLambdaEdge extends Construct { } }) - const logGroup = new logs.LogGroup(this, 'CheckExpirationLambdaEdgeLogGroup', { - logGroupName: `/aws/lambda/${id}-checkExpiration`, + const logGroup = new logs.LogGroup(this, 'OriginResponseLambdaEdgeLogGroup', { + logGroupName: `/aws/lambda/${id}-originResponse`, removalPolicy: cdk.RemovalPolicy.DESTROY, retention: logs.RetentionDays.ONE_DAY }) - this.lambdaEdge = new cloudfront.experimental.EdgeFunction(this, 'CheckExpirationLambdaEdge', { + this.lambdaEdge = new cloudfront.experimental.EdgeFunction(this, 'OriginResponseLambdaEdge', { runtime: nodeJSEnvironment, code: lambda.Code.fromAsset(path.join(buildOutputPath, 'server-functions', name)), handler: 'index.handler', diff --git a/src/cdk/stacks/NextCloudfrontStack.ts b/src/cdk/stacks/NextCloudfrontStack.ts index 52b4042..10a61d6 100644 --- a/src/cdk/stacks/NextCloudfrontStack.ts +++ b/src/cdk/stacks/NextCloudfrontStack.ts @@ -1,10 +1,10 @@ import { Stack, type StackProps } from 'aws-cdk-lib' import { Construct } from 'constructs' import * as s3 from 'aws-cdk-lib/aws-s3' -import { RoutingLambdaEdge } from '../constructs/RoutingLambdaEdge' +import { OriginRequestLambdaEdge } from '../constructs/OriginRequestLambdaEdge' import { CloudFrontDistribution } from '../constructs/CloudFrontDistribution' import { CacheConfig } from '../../types' -import { CheckExpirationLambdaEdge } from '../constructs/CheckExpirationLambdaEdge' +import { OriginResponseLambdaEdge } from '../constructs/OriginResponseLambdaEdge' import { ViewerResponseLambdaEdge } from '../constructs/ViewerResponseLambdaEdge' export interface NextCloudfrontStackProps extends StackProps { @@ -20,8 +20,8 @@ export interface NextCloudfrontStackProps extends StackProps { } export class NextCloudfrontStack extends Stack { - public readonly routingLambdaEdge: RoutingLambdaEdge - public readonly checkExpLambdaEdge: CheckExpirationLambdaEdge + public readonly originRequestLambdaEdge: OriginRequestLambdaEdge + public readonly originResponseLambdaEdge: OriginResponseLambdaEdge public readonly viewerResponseLambdaEdge: ViewerResponseLambdaEdge public readonly cloudfront: CloudFrontDistribution @@ -39,7 +39,7 @@ export class NextCloudfrontStack extends Stack { imageTTL } = props - this.routingLambdaEdge = new RoutingLambdaEdge(this, `${id}-RoutingLambdaEdge`, { + this.originRequestLambdaEdge = new OriginRequestLambdaEdge(this, `${id}-OriginRequestLambdaEdge`, { nodejs, bucketName: staticBucketName, renderServerDomain, @@ -48,7 +48,7 @@ export class NextCloudfrontStack extends Stack { bucketRegion: region }) - this.checkExpLambdaEdge = new CheckExpirationLambdaEdge(this, `${id}-CheckExpirationLambdaEdge`, { + this.originResponseLambdaEdge = new OriginResponseLambdaEdge(this, `${id}-OriginResponseLambdaEdge`, { nodejs, renderWorkerQueueUrl, buildOutputPath, @@ -70,14 +70,14 @@ export class NextCloudfrontStack extends Stack { this.cloudfront = new CloudFrontDistribution(this, `${id}-NextCloudFront`, { staticBucket, renderServerDomain, - requestEdgeFunction: this.routingLambdaEdge.lambdaEdge, - responseEdgeFunction: this.checkExpLambdaEdge.lambdaEdge, + requestEdgeFunction: this.originRequestLambdaEdge.lambdaEdge, + responseEdgeFunction: this.originResponseLambdaEdge.lambdaEdge, viewerResponseEdgeFunction: this.viewerResponseLambdaEdge.lambdaEdge, cacheConfig, imageTTL }) - staticBucket.grantRead(this.routingLambdaEdge.lambdaEdge) - staticBucket.grantRead(this.checkExpLambdaEdge.lambdaEdge) + staticBucket.grantRead(this.originRequestLambdaEdge.lambdaEdge) + staticBucket.grantRead(this.originResponseLambdaEdge.lambdaEdge) } } diff --git a/src/lambdas/edgeRouting.ts b/src/lambdas/originRequest.ts similarity index 100% rename from src/lambdas/edgeRouting.ts rename to src/lambdas/originRequest.ts diff --git a/src/lambdas/checkExpiration.ts b/src/lambdas/originResponse.ts similarity index 100% rename from src/lambdas/checkExpiration.ts rename to src/lambdas/originResponse.ts