From 0218f0ff413395c779cd7ccb76246c20fcb2c49c Mon Sep 17 00:00:00 2001
From: Kevin Mitchell <>
Date: Thu, 27 Jul 2023 22:02:29 -0400
Subject: [PATCH 01/16] https://github.com/jetbridge/cdk-nextjs/issues/119
Updates the server handler to include cache bucket configuration, **which is likely not correct as I'm just using the static asset bucket temporarily**. I need to look at how SST does this and what the spirit of this should be, e.g. if a separate bucket would be more appropriate, a folder, etc.
This also adds the ISR handling logic. I haven't actually tested this yet (am not 100% sure how to).
**This is a WIP**.
---
src/ImageOptimizationLambda.ts | 1 +
src/Nextjs.ts | 15 +++++++-
src/NextjsLambda.ts | 17 ++++++++-
src/NextjsRevaluation.ts | 63 ++++++++++++++++++++++++++++++++++
src/index.ts | 1 +
5 files changed, 95 insertions(+), 2 deletions(-)
create mode 100644 src/NextjsRevaluation.ts
diff --git a/src/ImageOptimizationLambda.ts b/src/ImageOptimizationLambda.ts
index 42a64508..93c19592 100644
--- a/src/ImageOptimizationLambda.ts
+++ b/src/ImageOptimizationLambda.ts
@@ -52,6 +52,7 @@ export class ImageOptimizationLambda extends Function {
handler: 'index.handler',
runtime: LAMBDA_RUNTIME,
architecture: Architecture.ARM_64,
+ description: 'Next.js Image Optimization Function',
// prevents "Resolution error: Cannot use resource in a cross-environment
// fashion, the resource's physical name must be explicit set or use
// PhysicalName.GENERATE_IF_NEEDED."
diff --git a/src/Nextjs.ts b/src/Nextjs.ts
index b5f5194d..aba47ce2 100644
--- a/src/Nextjs.ts
+++ b/src/Nextjs.ts
@@ -13,11 +13,12 @@ import { BaseSiteDomainProps, NextjsBaseProps } from './NextjsBase';
import { NextjsBuild } from './NextjsBuild';
import { NextjsDistribution, NextjsDistributionProps } from './NextjsDistribution';
import { NextJsLambda } from './NextjsLambda';
+import { NextjsRevaluation } from './NextjsRevaluation';
// contains server-side resolved environment vars in config bucket
export const CONFIG_ENV_JSON_PATH = 'next-env.json';
-export interface NextjsDomainProps extends BaseSiteDomainProps { }
+export interface NextjsDomainProps extends BaseSiteDomainProps {}
/**
* Defaults for created resources.
@@ -99,6 +100,11 @@ export class Nextjs extends Construct {
*/
public tempBuildDir: string;
+ /**
+ * Revalidation handler and queue.
+ */
+ public revalidation: NextjsRevaluation;
+
public configBucket?: s3.Bucket;
public lambdaFunctionUrl!: lambda.FunctionUrl;
public imageOptimizationLambdaFunctionUrl!: lambda.FunctionUrl;
@@ -134,6 +140,7 @@ export class Nextjs extends Construct {
tempBuildDir,
nextBuild: this.nextBuild,
lambda: props.defaults?.lambda,
+ cacheBucket: this.staticAssetBucket,
});
// build image optimization
this.imageOptimizationFunction = new ImageOptimizationLambda(this, 'ImgOptFn', {
@@ -160,6 +167,12 @@ export class Nextjs extends Construct {
this.serverFunction.lambdaFunction.node.addDependency(...this.assetsDeployment.deployments);
}
+ this.revalidation = new NextjsRevaluation(this, 'Revaluation', {
+ ...props,
+ nextBuild: this.nextBuild,
+ serverFunction: this.serverFunction,
+ });
+
this.distribution = new NextjsDistribution(this, 'Distribution', {
...props,
...props.defaults?.distribution,
diff --git a/src/NextjsLambda.ts b/src/NextjsLambda.ts
index 72ac1e1a..9bafc5a0 100644
--- a/src/NextjsLambda.ts
+++ b/src/NextjsLambda.ts
@@ -3,7 +3,7 @@ import * as path from 'path';
import { Duration, PhysicalName, RemovalPolicy, Stack, Token } from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Function, FunctionOptions } from 'aws-cdk-lib/aws-lambda';
-import { Bucket } from 'aws-cdk-lib/aws-s3';
+import { Bucket, IBucket } from 'aws-cdk-lib/aws-s3';
import * as s3Assets from 'aws-cdk-lib/aws-s3-assets';
import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
@@ -22,6 +22,13 @@ function getEnvironment(props: NextjsLambdaProps): { [name: string]: string } {
...props.environment,
...props.lambda?.environment,
...(props.nodeEnv ? { NODE_ENV: props.nodeEnv } : {}),
+ ...{
+ CACHE_BUCKET_NAME: props.cacheBucket?.bucketName || '',
+ CACHE_BUCKET_KEY_PREFIX: '_cache',
+ // @see: https://github.com/serverless-stack/sst/blob/master/packages/sst/src/constructs/NextjsSite.ts#L158
+ // TODO: refactor this or pass in the region
+ // CACHE_BUCKET_REGION: Stack.of(this).region,
+ },
};
return environmentVariables;
@@ -37,6 +44,11 @@ export interface NextjsLambdaProps extends NextjsBaseProps {
* Override function properties.
*/
readonly lambda?: FunctionOptions;
+
+ /**
+ * The S3 bucket holding application cache.
+ */
+ readonly cacheBucket: IBucket;
}
/**
@@ -90,6 +102,9 @@ export class NextJsLambda extends Construct {
});
this.lambdaFunction = fn;
+ // todo: once we figure out the correct S3 bucket, make sure permissions are appropriate.
+ props.cacheBucket.grantReadWrite(fn);
+
// rewrite env var placeholders in server code
const replacementParams = this._getReplacementParams(environment);
if (!isPlaceholder && Object.keys(replacementParams).length) {
diff --git a/src/NextjsRevaluation.ts b/src/NextjsRevaluation.ts
new file mode 100644
index 00000000..0c467aac
--- /dev/null
+++ b/src/NextjsRevaluation.ts
@@ -0,0 +1,63 @@
+import { Duration, Stack } from 'aws-cdk-lib';
+import { Code, Function, FunctionOptions, Runtime } from 'aws-cdk-lib/aws-lambda';
+import { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources';
+import { Queue } from 'aws-cdk-lib/aws-sqs';
+import { Construct } from 'constructs';
+import { NextjsBaseProps } from './NextjsBase';
+import { NextjsBuild } from './NextjsBuild';
+import { NextJsLambda } from './NextjsLambda';
+
+export interface RevaluationProps extends NextjsBaseProps {
+ /**
+ * Override function properties.
+ */
+ readonly lambdaOptions?: FunctionOptions;
+
+ /**
+ * The `NextjsBuild` instance representing the built Nextjs application.
+ */
+ readonly nextBuild: NextjsBuild;
+
+ /**
+ * The main NextJS server handler lambda function.
+ */
+ readonly serverFunction: NextJsLambda;
+}
+
+/**
+ * Builds the system for revaluating Next.js resources. This includes a Lambda function handler and queue system.
+ */
+export class NextjsRevaluation extends Construct {
+ constructor(scope: Construct, id: string, props: RevaluationProps) {
+ super(scope, id);
+
+ if (!props.nextBuild) return;
+
+ const code = props.isPlaceholder
+ ? Code.fromInline(
+ "module.exports.handler = async () => { return { statusCode: 200, body: 'SST placeholder site' } }"
+ )
+ : Code.fromAsset(props.nextBuild.nextImageFnDir);
+
+ const queue = new Queue(this, 'RevalidationQueue', {
+ fifo: true,
+ receiveMessageWaitTime: Duration.seconds(20),
+ });
+ const consumer = new Function(this, 'RevalidationFunction', {
+ description: 'Next.js revalidator',
+ handler: 'index.handler',
+ code,
+ runtime: Runtime.NODEJS_18_X,
+ timeout: Duration.seconds(30),
+ // This, I think, is supposed to be the VPC or VPC Subnet config (https://github.com/serverless-stack/sst/blob/master/packages/sst/src/constructs/NextjsSite.ts#L59C5-L59C17)
+ // ...this.revalidation,
+ });
+ consumer.addEventSource(new SqsEventSource(queue, { batchSize: 5 }));
+
+ // Allow server to send messages to the queue
+ const server = props.serverFunction.lambdaFunction;
+ server?.addEnvironment('REVALIDATION_QUEUE_URL', queue.queueUrl);
+ server?.addEnvironment('REVALIDATION_QUEUE_REGION', Stack.of(this).region);
+ queue.grantSendMessages(server?.role!);
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index 0a472f7c..c1ad14e5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -11,6 +11,7 @@ export {
NextjsAssetsDeploymentProps,
NextjsAssetsCachePolicyProps,
} from './NextjsAssetsDeployment';
+export { NextjsRevaluation, RevaluationProps } from './NextjsRevaluation';
export { NextjsBuild, NextjsBuildProps, CreateArchiveArgs } from './NextjsBuild';
export { EnvironmentVars, NextJsLambda, NextjsLambdaProps } from './NextjsLambda';
export { ImageOptimizationLambda, ImageOptimizationProps } from './ImageOptimizationLambda';
From 5365beeae8c2bedbdfb897e57cf22fd114afacc2 Mon Sep 17 00:00:00 2001
From: Kevin Mitchell <>
Date: Sat, 29 Jul 2023 09:33:48 -0400
Subject: [PATCH 02/16] feat: add cache deployment bucket
This isn't working 100% quite yet. I am testing with https://github.com/vercel/app-playground - with these changes I'm able to deploy but I need to populate the cache bucket name in another function env I believe. I also haven't really gotten into the details of looking to see if messages are actually being sent to the queue, etc.
The main difference here is some cleanup, and I added the separate bucket for the cache.
---
API.md | 356 ++++++++++++++++++
src/ImageOptimizationLambda.ts | 4 +-
src/Nextjs.ts | 40 +-
src/NextjsLambda.ts | 2 +-
...jsRevaluation.ts => NextjsRevalidation.ts} | 12 +-
src/index.ts | 2 +-
6 files changed, 393 insertions(+), 23 deletions(-)
rename src/{NextjsRevaluation.ts => NextjsRevalidation.ts} (79%)
diff --git a/API.md b/API.md
index c8d24a5c..d2ac943e 100644
--- a/API.md
+++ b/API.md
@@ -1302,6 +1302,7 @@ Any object.
| imageOptimizationLambdaFunctionUrl
| aws-cdk-lib.aws_lambda.FunctionUrl
| *No description.* |
| lambdaFunctionUrl
| aws-cdk-lib.aws_lambda.FunctionUrl
| *No description.* |
| nextBuild
| NextjsBuild
| Built NextJS project output. |
+| revalidation
| NextjsRevalidation
| Revalidation handler and queue. |
| serverFunction
| NextJsLambda
| The main NextJS server handler lambda function. |
| tempBuildDir
| string
| Where build-time assets for deployment are stored. |
| configBucket
| aws-cdk-lib.aws_s3.Bucket
| *No description.* |
@@ -1408,6 +1409,18 @@ Built NextJS project output.
---
+##### `revalidation`Required
+
+```typescript
+public readonly revalidation: NextjsRevalidation;
+```
+
+- *Type:* NextjsRevalidation
+
+Revalidation handler and queue.
+
+---
+
##### `serverFunction`Required
```typescript
@@ -1689,6 +1702,7 @@ Any object.
| **Name** | **Type** | **Description** |
| --- | --- | --- |
| node
| constructs.Node
| The tree node. |
+| nextCacheDir
| string
| Cache setup directory. |
| nextImageFnDir
| string
| Contains function for processessing image requests. |
| nextServerFnDir
| string
| Contains server code and dependencies. |
| nextStaticDir
| string
| Static files containing client-side code. |
@@ -1710,6 +1724,18 @@ The tree node.
---
+##### `nextCacheDir`Required
+
+```typescript
+public readonly nextCacheDir: string;
+```
+
+- *Type:* string
+
+Cache setup directory.
+
+---
+
##### `nextImageFnDir`Required
```typescript
@@ -2505,6 +2531,107 @@ The runtimes compatible with this Layer.
---
+### NextjsRevalidation
+
+Builds the system for revaluating Next.js resources. This includes a Lambda function handler and queue system.
+
+#### Initializers
+
+```typescript
+import { NextjsRevalidation } from 'cdk-nextjs-standalone'
+
+new NextjsRevalidation(scope: Construct, id: string, props: RevalidationProps)
+```
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| scope
| constructs.Construct
| *No description.* |
+| id
| string
| *No description.* |
+| props
| RevalidationProps
| *No description.* |
+
+---
+
+##### `scope`Required
+
+- *Type:* constructs.Construct
+
+---
+
+##### `id`Required
+
+- *Type:* string
+
+---
+
+##### `props`Required
+
+- *Type:* RevalidationProps
+
+---
+
+#### Methods
+
+| **Name** | **Description** |
+| --- | --- |
+| toString
| Returns a string representation of this construct. |
+
+---
+
+##### `toString`
+
+```typescript
+public toString(): string
+```
+
+Returns a string representation of this construct.
+
+#### Static Functions
+
+| **Name** | **Description** |
+| --- | --- |
+| isConstruct
| Checks if `x` is a construct. |
+
+---
+
+##### ~~`isConstruct`~~
+
+```typescript
+import { NextjsRevalidation } from 'cdk-nextjs-standalone'
+
+NextjsRevalidation.isConstruct(x: any)
+```
+
+Checks if `x` is a construct.
+
+###### `x`Required
+
+- *Type:* any
+
+Any object.
+
+---
+
+#### Properties
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| node
| constructs.Node
| The tree node. |
+
+---
+
+##### `node`Required
+
+```typescript
+public readonly node: Node;
+```
+
+- *Type:* constructs.Node
+
+The tree node.
+
+---
+
+
### NextjsS3EnvRewriter
Rewrites variables in S3 objects after a deployment happens to replace CloudFormation tokens with their values.
@@ -4466,6 +4593,7 @@ const nextjsLambdaProps: NextjsLambdaProps = { ... }
| quiet
| boolean
| Less build output. |
| sharpLayerArn
| string
| Optional arn for the sharp lambda layer. |
| tempBuildDir
| string
| Directory to store temporary build files in. |
+| cacheBucket
| aws-cdk-lib.aws_s3.IBucket
| The S3 bucket holding application cache. |
| nextBuild
| NextjsBuild
| Built nextJS application. |
| lambda
| aws-cdk-lib.aws_lambda.FunctionOptions
| Override function properties. |
@@ -4620,6 +4748,18 @@ Defaults to os.tmpdir().
---
+##### `cacheBucket`Required
+
+```typescript
+public readonly cacheBucket: IBucket;
+```
+
+- *Type:* aws-cdk-lib.aws_s3.IBucket
+
+The S3 bucket holding application cache.
+
+---
+
##### `nextBuild`Required
```typescript
@@ -5140,6 +5280,222 @@ public readonly debug: boolean;
---
+### RevalidationProps
+
+#### Initializer
+
+```typescript
+import { RevalidationProps } from 'cdk-nextjs-standalone'
+
+const revalidationProps: RevalidationProps = { ... }
+```
+
+#### Properties
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| nextjsPath
| string
| Relative path to the directory where the NextJS project is located. |
+| buildCommand
| string
| Optional value used to install NextJS node dependencies. |
+| buildPath
| string
| The directory to execute `npm run build` from. |
+| compressionLevel
| number
| 0 - no compression, fastest 9 - maximum compression, slowest. |
+| environment
| {[ key: string ]: string}
| Custom environment variables to pass to the NextJS build and runtime. |
+| isPlaceholder
| boolean
| Skip building app and deploy a placeholder. |
+| nodeEnv
| string
| Optional value for NODE_ENV during build and runtime. |
+| projectRoot
| string
| Root of your project, if different from `nextjsPath`. |
+| quiet
| boolean
| Less build output. |
+| sharpLayerArn
| string
| Optional arn for the sharp lambda layer. |
+| tempBuildDir
| string
| Directory to store temporary build files in. |
+| nextBuild
| NextjsBuild
| The `NextjsBuild` instance representing the built Nextjs application. |
+| serverFunction
| NextJsLambda
| The main NextJS server handler lambda function. |
+| lambdaOptions
| aws-cdk-lib.aws_lambda.FunctionOptions
| Override function properties. |
+
+---
+
+##### `nextjsPath`Required
+
+```typescript
+public readonly nextjsPath: string;
+```
+
+- *Type:* string
+
+Relative path to the directory where the NextJS project is located.
+
+Can be the root of your project (`.`) or a subdirectory (`packages/web`).
+
+---
+
+##### `buildCommand`Optional
+
+```typescript
+public readonly buildCommand: string;
+```
+
+- *Type:* string
+
+Optional value used to install NextJS node dependencies.
+
+It defaults to 'npx --yes open-next@latest build'
+
+---
+
+##### `buildPath`Optional
+
+```typescript
+public readonly buildPath: string;
+```
+
+- *Type:* string
+
+The directory to execute `npm run build` from.
+
+By default, it is `nextjsPath`.
+Can be overridden, particularly useful for monorepos where `build` is expected to run
+at the root of the project.
+
+---
+
+##### `compressionLevel`Optional
+
+```typescript
+public readonly compressionLevel: number;
+```
+
+- *Type:* number
+- *Default:* 1
+
+0 - no compression, fastest 9 - maximum compression, slowest.
+
+---
+
+##### `environment`Optional
+
+```typescript
+public readonly environment: {[ key: string ]: string};
+```
+
+- *Type:* {[ key: string ]: string}
+
+Custom environment variables to pass to the NextJS build and runtime.
+
+---
+
+##### `isPlaceholder`Optional
+
+```typescript
+public readonly isPlaceholder: boolean;
+```
+
+- *Type:* boolean
+
+Skip building app and deploy a placeholder.
+
+Useful when using `next dev` for local development.
+
+---
+
+##### `nodeEnv`Optional
+
+```typescript
+public readonly nodeEnv: string;
+```
+
+- *Type:* string
+
+Optional value for NODE_ENV during build and runtime.
+
+---
+
+##### `projectRoot`Optional
+
+```typescript
+public readonly projectRoot: string;
+```
+
+- *Type:* string
+
+Root of your project, if different from `nextjsPath`.
+
+Defaults to current working directory.
+
+---
+
+##### `quiet`Optional
+
+```typescript
+public readonly quiet: boolean;
+```
+
+- *Type:* boolean
+
+Less build output.
+
+---
+
+##### `sharpLayerArn`Optional
+
+```typescript
+public readonly sharpLayerArn: string;
+```
+
+- *Type:* string
+
+Optional arn for the sharp lambda layer.
+
+If omitted, the layer will be created.
+
+---
+
+##### `tempBuildDir`Optional
+
+```typescript
+public readonly tempBuildDir: string;
+```
+
+- *Type:* string
+
+Directory to store temporary build files in.
+
+Defaults to os.tmpdir().
+
+---
+
+##### `nextBuild`Required
+
+```typescript
+public readonly nextBuild: NextjsBuild;
+```
+
+- *Type:* NextjsBuild
+
+The `NextjsBuild` instance representing the built Nextjs application.
+
+---
+
+##### `serverFunction`Required
+
+```typescript
+public readonly serverFunction: NextJsLambda;
+```
+
+- *Type:* NextJsLambda
+
+The main NextJS server handler lambda function.
+
+---
+
+##### `lambdaOptions`Optional
+
+```typescript
+public readonly lambdaOptions: FunctionOptions;
+```
+
+- *Type:* aws-cdk-lib.aws_lambda.FunctionOptions
+
+Override function properties.
+
+---
+
### RewriteReplacementsConfig
#### Initializer
diff --git a/src/ImageOptimizationLambda.ts b/src/ImageOptimizationLambda.ts
index 93c19592..43d2ab65 100644
--- a/src/ImageOptimizationLambda.ts
+++ b/src/ImageOptimizationLambda.ts
@@ -43,8 +43,8 @@ export class ImageOptimizationLambda extends Function {
const code = isPlaceholder
? Code.fromInline(
- "module.exports.handler = async () => { return { statusCode: 200, body: 'SST placeholder site' } }"
- )
+ "module.exports.handler = async () => { return { statusCode: 200, body: 'cdk-nextjs placeholder site' } }"
+ )
: Code.fromAsset(props.nextBuild.nextImageFnDir);
super(scope, id, {
diff --git a/src/Nextjs.ts b/src/Nextjs.ts
index aba47ce2..588c5aa6 100644
--- a/src/Nextjs.ts
+++ b/src/Nextjs.ts
@@ -5,6 +5,7 @@ import { RemovalPolicy } from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { FunctionOptions } from 'aws-cdk-lib/aws-lambda';
import * as s3 from 'aws-cdk-lib/aws-s3';
+import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';
import { Construct } from 'constructs';
import * as fs from 'fs-extra';
import { ImageOptimizationLambda } from './ImageOptimizationLambda';
@@ -13,7 +14,7 @@ import { BaseSiteDomainProps, NextjsBaseProps } from './NextjsBase';
import { NextjsBuild } from './NextjsBuild';
import { NextjsDistribution, NextjsDistributionProps } from './NextjsDistribution';
import { NextJsLambda } from './NextjsLambda';
-import { NextjsRevaluation } from './NextjsRevaluation';
+import { NextjsRevalidation } from './NextjsRevalidation';
// contains server-side resolved environment vars in config bucket
export const CONFIG_ENV_JSON_PATH = 'next-env.json';
@@ -103,13 +104,14 @@ export class Nextjs extends Construct {
/**
* Revalidation handler and queue.
*/
- public revalidation: NextjsRevaluation;
+ public revalidation: NextjsRevalidation;
public configBucket?: s3.Bucket;
public lambdaFunctionUrl!: lambda.FunctionUrl;
public imageOptimizationLambdaFunctionUrl!: lambda.FunctionUrl;
protected staticAssetBucket: s3.IBucket;
+ protected cacheBucket: s3.IBucket;
constructor(scope: Construct, id: string, protected props: NextjsProps) {
super(scope, id);
@@ -119,8 +121,8 @@ export class Nextjs extends Construct {
// get dir to store temp build files in
const tempBuildDir = props.tempBuildDir
? path.resolve(
- path.join(props.tempBuildDir, `nextjs-cdk-build-${this.node.id}-${this.node.addr.substring(0, 4)}`)
- )
+ path.join(props.tempBuildDir, `nextjs-cdk-build-${this.node.id}-${this.node.addr.substring(0, 4)}`)
+ )
: fs.mkdtempSync(path.join(os.tmpdir(), 'nextjs-cdk-build-'));
this.tempBuildDir = tempBuildDir;
@@ -128,7 +130,15 @@ export class Nextjs extends Construct {
// create static asset bucket
this.staticAssetBucket =
props.defaults?.assetDeployment?.bucket ??
- new s3.Bucket(this, 'Bucket', {
+ new s3.Bucket(this, 'AssetBucket', {
+ removalPolicy: RemovalPolicy.DESTROY,
+ autoDeleteObjects: true,
+ });
+
+ // create cache bucket
+ this.cacheBucket =
+ props.defaults?.assetDeployment?.bucket ??
+ new s3.Bucket(this, 'CacheBucket', {
removalPolicy: RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
@@ -140,7 +150,7 @@ export class Nextjs extends Construct {
tempBuildDir,
nextBuild: this.nextBuild,
lambda: props.defaults?.lambda,
- cacheBucket: this.staticAssetBucket,
+ cacheBucket: this.cacheBucket,
});
// build image optimization
this.imageOptimizationFunction = new ImageOptimizationLambda(this, 'ImgOptFn', {
@@ -150,6 +160,18 @@ export class Nextjs extends Construct {
lambdaOptions: props.defaults?.lambda,
});
+ // build revalidation queue and handler function
+ this.revalidation = new NextjsRevalidation(this, 'Revaluation', {
+ ...props,
+ nextBuild: this.nextBuild,
+ serverFunction: this.serverFunction,
+ });
+
+ new BucketDeployment(this, 'DeployCacheFiles', {
+ sources: [Source.asset(this.nextBuild.nextCacheDir)],
+ destinationBucket: this.cacheBucket,
+ });
+
// deploy nextjs static assets to s3
this.assetsDeployment = new NextJsAssetsDeployment(this, 'AssetDeployment', {
...props,
@@ -167,12 +189,6 @@ export class Nextjs extends Construct {
this.serverFunction.lambdaFunction.node.addDependency(...this.assetsDeployment.deployments);
}
- this.revalidation = new NextjsRevaluation(this, 'Revaluation', {
- ...props,
- nextBuild: this.nextBuild,
- serverFunction: this.serverFunction,
- });
-
this.distribution = new NextjsDistribution(this, 'Distribution', {
...props,
...props.defaults?.distribution,
diff --git a/src/NextjsLambda.ts b/src/NextjsLambda.ts
index 9bafc5a0..7a7ea226 100644
--- a/src/NextjsLambda.ts
+++ b/src/NextjsLambda.ts
@@ -24,7 +24,7 @@ function getEnvironment(props: NextjsLambdaProps): { [name: string]: string } {
...(props.nodeEnv ? { NODE_ENV: props.nodeEnv } : {}),
...{
CACHE_BUCKET_NAME: props.cacheBucket?.bucketName || '',
- CACHE_BUCKET_KEY_PREFIX: '_cache',
+ // Note we don't need a CACHE_BUCKET_KEY_PREFIX because we're using a separate bucket for cache
// @see: https://github.com/serverless-stack/sst/blob/master/packages/sst/src/constructs/NextjsSite.ts#L158
// TODO: refactor this or pass in the region
// CACHE_BUCKET_REGION: Stack.of(this).region,
diff --git a/src/NextjsRevaluation.ts b/src/NextjsRevalidation.ts
similarity index 79%
rename from src/NextjsRevaluation.ts
rename to src/NextjsRevalidation.ts
index 0c467aac..249f52ed 100644
--- a/src/NextjsRevaluation.ts
+++ b/src/NextjsRevalidation.ts
@@ -7,7 +7,7 @@ import { NextjsBaseProps } from './NextjsBase';
import { NextjsBuild } from './NextjsBuild';
import { NextJsLambda } from './NextjsLambda';
-export interface RevaluationProps extends NextjsBaseProps {
+export interface RevalidationProps extends NextjsBaseProps {
/**
* Override function properties.
*/
@@ -27,15 +27,15 @@ export interface RevaluationProps extends NextjsBaseProps {
/**
* Builds the system for revaluating Next.js resources. This includes a Lambda function handler and queue system.
*/
-export class NextjsRevaluation extends Construct {
- constructor(scope: Construct, id: string, props: RevaluationProps) {
+export class NextjsRevalidation extends Construct {
+ constructor(scope: Construct, id: string, props: RevalidationProps) {
super(scope, id);
if (!props.nextBuild) return;
const code = props.isPlaceholder
? Code.fromInline(
- "module.exports.handler = async () => { return { statusCode: 200, body: 'SST placeholder site' } }"
+ "module.exports.handler = async () => { return { statusCode: 200, body: 'cdk-nextjs placeholder site' } }"
)
: Code.fromAsset(props.nextBuild.nextImageFnDir);
@@ -44,13 +44,11 @@ export class NextjsRevaluation extends Construct {
receiveMessageWaitTime: Duration.seconds(20),
});
const consumer = new Function(this, 'RevalidationFunction', {
- description: 'Next.js revalidator',
+ description: 'Next.js revalidation function',
handler: 'index.handler',
code,
runtime: Runtime.NODEJS_18_X,
timeout: Duration.seconds(30),
- // This, I think, is supposed to be the VPC or VPC Subnet config (https://github.com/serverless-stack/sst/blob/master/packages/sst/src/constructs/NextjsSite.ts#L59C5-L59C17)
- // ...this.revalidation,
});
consumer.addEventSource(new SqsEventSource(queue, { batchSize: 5 }));
diff --git a/src/index.ts b/src/index.ts
index c1ad14e5..3a3b8ec4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -11,7 +11,7 @@ export {
NextjsAssetsDeploymentProps,
NextjsAssetsCachePolicyProps,
} from './NextjsAssetsDeployment';
-export { NextjsRevaluation, RevaluationProps } from './NextjsRevaluation';
+export { NextjsRevalidation, RevalidationProps } from './NextjsRevalidation';
export { NextjsBuild, NextjsBuildProps, CreateArchiveArgs } from './NextjsBuild';
export { EnvironmentVars, NextJsLambda, NextjsLambdaProps } from './NextjsLambda';
export { ImageOptimizationLambda, ImageOptimizationProps } from './ImageOptimizationLambda';
From 6da84cc33375d87b2043249bd0ded71691307012 Mon Sep 17 00:00:00 2001
From: Kevin Mitchell <>
Date: Sat, 29 Jul 2023 12:00:21 -0400
Subject: [PATCH 03/16] fix: update default lambda memory and allow cache
bucket config
* added a new constant so we can keep the default memory size uniform across lambda functions
* Allow the cache bucket to be passed in as a construct property if user wants to supply their own s3 cache bucket
---
API.md | 13 +++++++++++++
src/ImageOptimizationLambda.ts | 4 ++--
src/Nextjs.ts | 15 ++++++++++-----
src/NextjsLambda.ts | 8 +++-----
src/NextjsS3EnvRewriter.ts | 3 ++-
src/constants.ts | 2 ++
6 files changed, 32 insertions(+), 13 deletions(-)
diff --git a/API.md b/API.md
index d2ac943e..8ed323ad 100644
--- a/API.md
+++ b/API.md
@@ -4038,6 +4038,7 @@ const nextjsDefaultsProps: NextjsDefaultsProps = { ... }
| **Name** | **Type** | **Description** |
| --- | --- | --- |
| assetDeployment
| any
| Override static file deployment settings. |
+| cacheBucket
| any
| Override cache bucket. |
| distribution
| any
| Override CloudFront distribution settings. |
| lambda
| aws-cdk-lib.aws_lambda.FunctionOptions
| Override server lambda function settings. |
@@ -4055,6 +4056,18 @@ Override static file deployment settings.
---
+##### `cacheBucket`Optional
+
+```typescript
+public readonly cacheBucket: any;
+```
+
+- *Type:* any
+
+Override cache bucket.
+
+---
+
##### `distribution`Optional
```typescript
diff --git a/src/ImageOptimizationLambda.ts b/src/ImageOptimizationLambda.ts
index 43d2ab65..8dab6fc0 100644
--- a/src/ImageOptimizationLambda.ts
+++ b/src/ImageOptimizationLambda.ts
@@ -4,7 +4,7 @@ import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { Architecture, Code, Function, FunctionOptions } from 'aws-cdk-lib/aws-lambda';
import { IBucket } from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';
-import { LAMBDA_RUNTIME } from './constants';
+import { LAMBDA_RUNTIME, DEFAULT_LAMBA_MEMORY } from './constants';
import { NextjsBaseProps } from './NextjsBase';
import type { NextjsBuild } from './NextjsBuild';
@@ -59,7 +59,7 @@ export class ImageOptimizationLambda extends Function {
functionName: Stack.of(scope).region !== 'us-east-1' ? PhysicalName.GENERATE_IF_NEEDED : undefined,
...lambdaOptions,
// defaults
- memorySize: lambdaOptions?.memorySize || 1024,
+ memorySize: lambdaOptions?.memorySize || DEFAULT_LAMBA_MEMORY,
timeout: lambdaOptions?.timeout ?? Duration.seconds(10),
environment: {
BUCKET_NAME: bucket.bucketName,
diff --git a/src/Nextjs.ts b/src/Nextjs.ts
index 588c5aa6..58c2fc76 100644
--- a/src/Nextjs.ts
+++ b/src/Nextjs.ts
@@ -31,6 +31,11 @@ export interface NextjsDefaultsProps {
*/
readonly assetDeployment?: NextjsAssetsDeploymentProps | any;
+ /**
+ * Override cache bucket.
+ */
+ readonly cacheBucket?: s3.IBucket | any;
+
/**
* Override server lambda function settings.
*/
@@ -130,22 +135,22 @@ export class Nextjs extends Construct {
// create static asset bucket
this.staticAssetBucket =
props.defaults?.assetDeployment?.bucket ??
- new s3.Bucket(this, 'AssetBucket', {
+ new s3.Bucket(this, 'Assets', {
removalPolicy: RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// create cache bucket
this.cacheBucket =
- props.defaults?.assetDeployment?.bucket ??
- new s3.Bucket(this, 'CacheBucket', {
+ props.defaults?.cacheBucket ??
+ new s3.Bucket(this, 'Cache', {
removalPolicy: RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// build nextjs app
this.nextBuild = new NextjsBuild(this, id, { ...props, tempBuildDir });
- this.serverFunction = new NextJsLambda(this, 'Fn', {
+ this.serverFunction = new NextJsLambda(this, 'ServerFn', {
...props,
tempBuildDir,
nextBuild: this.nextBuild,
@@ -161,7 +166,7 @@ export class Nextjs extends Construct {
});
// build revalidation queue and handler function
- this.revalidation = new NextjsRevalidation(this, 'Revaluation', {
+ this.revalidation = new NextjsRevalidation(this, 'Revalidation', {
...props,
nextBuild: this.nextBuild,
serverFunction: this.serverFunction,
diff --git a/src/NextjsLambda.ts b/src/NextjsLambda.ts
index 7a7ea226..718f30de 100644
--- a/src/NextjsLambda.ts
+++ b/src/NextjsLambda.ts
@@ -9,7 +9,7 @@ import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
import { Construct } from 'constructs';
import * as fs from 'fs-extra';
-import { LAMBDA_RUNTIME } from './constants';
+import { LAMBDA_RUNTIME, DEFAULT_LAMBA_MEMORY } from './constants';
import { CONFIG_ENV_JSON_PATH } from './Nextjs';
import { NextjsBaseProps } from './NextjsBase';
import { createArchive, NextjsBuild } from './NextjsBuild';
@@ -24,10 +24,9 @@ function getEnvironment(props: NextjsLambdaProps): { [name: string]: string } {
...(props.nodeEnv ? { NODE_ENV: props.nodeEnv } : {}),
...{
CACHE_BUCKET_NAME: props.cacheBucket?.bucketName || '',
+ CACHE_BUCKET_REGION: props.cacheBucket ? Stack.of(props.cacheBucket).region : '',
// Note we don't need a CACHE_BUCKET_KEY_PREFIX because we're using a separate bucket for cache
// @see: https://github.com/serverless-stack/sst/blob/master/packages/sst/src/constructs/NextjsSite.ts#L158
- // TODO: refactor this or pass in the region
- // CACHE_BUCKET_REGION: Stack.of(this).region,
},
};
@@ -88,7 +87,7 @@ export class NextJsLambda extends Construct {
// build the lambda function
const environment = getEnvironment(props);
const fn = new Function(scope, 'ServerHandler', {
- memorySize: functionOptions?.memorySize || 1024,
+ memorySize: functionOptions?.memorySize || DEFAULT_LAMBA_MEMORY,
timeout: functionOptions?.timeout ?? Duration.seconds(10),
runtime: LAMBDA_RUNTIME,
handler: path.join('index.handler'),
@@ -102,7 +101,6 @@ export class NextJsLambda extends Construct {
});
this.lambdaFunction = fn;
- // todo: once we figure out the correct S3 bucket, make sure permissions are appropriate.
props.cacheBucket.grantReadWrite(fn);
// rewrite env var placeholders in server code
diff --git a/src/NextjsS3EnvRewriter.ts b/src/NextjsS3EnvRewriter.ts
index f69a5931..488e367a 100644
--- a/src/NextjsS3EnvRewriter.ts
+++ b/src/NextjsS3EnvRewriter.ts
@@ -9,6 +9,7 @@ import { Bucket, IBucket } from 'aws-cdk-lib/aws-s3';
import * as cr from 'aws-cdk-lib/custom-resources';
import { Construct } from 'constructs';
import { bundleFunction } from './BundleFunction';
+import { DEFAULT_LAMBA_MEMORY } from './constants';
import { NextjsBaseProps } from './NextjsBase';
import { makeTokenPlaceholder } from './NextjsBuild';
@@ -73,7 +74,7 @@ export class NextjsS3EnvRewriter extends Construct {
// rewriter lambda function
const rewriteFn = new lambda.Function(this, 'RewriteOnEventHandler', {
runtime: Runtime.NODEJS_16_X,
- memorySize: 1024,
+ memorySize: DEFAULT_LAMBA_MEMORY,
timeout: Duration.minutes(5),
handler: 'S3EnvRewriter.handler',
code: lambda.Code.fromAsset(handlerDir),
diff --git a/src/constants.ts b/src/constants.ts
index aa926855..41cf0f3d 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -5,3 +5,5 @@ export const DEFAULT_STATIC_MAX_AGE = Duration.days(30).toSeconds();
export const DEFAULT_STATIC_STALE_WHILE_REVALIDATE = Duration.days(1).toSeconds();
export const LAMBDA_RUNTIME = Runtime.NODEJS_18_X;
+
+export const DEFAULT_LAMBA_MEMORY = 1536;
From ff9da82a44012149cacc7cd0c8ec631d1d983090 Mon Sep 17 00:00:00 2001
From: Kevin Mitchell <>
Date: Sat, 29 Jul 2023 13:30:21 -0400
Subject: [PATCH 04/16] chore: add comment for reason for default memory size
---
src/constants.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/constants.ts b/src/constants.ts
index 41cf0f3d..c239ac9c 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -6,4 +6,8 @@ export const DEFAULT_STATIC_STALE_WHILE_REVALIDATE = Duration.days(1).toSeconds(
export const LAMBDA_RUNTIME = Runtime.NODEJS_18_X;
+/**
+ * 1536mb costs 1.5x but runs twice as fast for most scenarios.
+ * @see {@link https://dev.to/dashbird/4-tips-for-aws-lambda-optimization-for-production-3if1}
+ */
export const DEFAULT_LAMBA_MEMORY = 1536;
From 348459c1535ab5fd680817c4f15b6b59c3afc6a4 Mon Sep 17 00:00:00 2001
From: Kevin Mitchell <>
Date: Sun, 30 Jul 2023 19:27:28 -0400
Subject: [PATCH 05/16] peg open-next in default build to major version
see: https://github.com/jetbridge/cdk-nextjs/issues/126
---
src/NextjsBuild.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/NextjsBuild.ts b/src/NextjsBuild.ts
index e5f8f1ab..6a558b84 100644
--- a/src/NextjsBuild.ts
+++ b/src/NextjsBuild.ts
@@ -100,7 +100,7 @@ export class NextjsBuild extends Construct {
};
const buildPath = this.props.buildPath ?? nextjsPath;
- const buildCommand = this.props.buildCommand ?? 'npx --yes open-next@1 build';
+ const buildCommand = this.props.buildCommand ?? 'npx --yes open-next@2 build';
// run build
console.debug(`├ Running "${buildCommand}" in`, buildPath);
const cmdParts = buildCommand.split(/\s+/);
From 7f5892f7804f5fc8c3f19552ee69fddb8e31417e Mon Sep 17 00:00:00 2001
From: Kevin Mitchell <>
Date: Sun, 30 Jul 2023 20:30:01 -0400
Subject: [PATCH 06/16] use correct function for revalidation queue handler
---
API.md | 13 +++++++++++++
src/NextjsBuild.ts | 6 ++++++
src/NextjsRevalidation.ts | 2 +-
3 files changed, 20 insertions(+), 1 deletion(-)
diff --git a/API.md b/API.md
index 8ed323ad..c95b5e7c 100644
--- a/API.md
+++ b/API.md
@@ -1704,6 +1704,7 @@ Any object.
| node
| constructs.Node
| The tree node. |
| nextCacheDir
| string
| Cache setup directory. |
| nextImageFnDir
| string
| Contains function for processessing image requests. |
+| nextRevalidateFnDir
| string
| Contains function for processing items from revalidation queue. |
| nextServerFnDir
| string
| Contains server code and dependencies. |
| nextStaticDir
| string
| Static files containing client-side code. |
| projectRoot
| string
| *No description.* |
@@ -1750,6 +1751,18 @@ Should be arm64.
---
+##### `nextRevalidateFnDir`Required
+
+```typescript
+public readonly nextRevalidateFnDir: string;
+```
+
+- *Type:* string
+
+Contains function for processing items from revalidation queue.
+
+---
+
##### `nextServerFnDir`Required
```typescript
diff --git a/src/NextjsBuild.ts b/src/NextjsBuild.ts
index 6a558b84..b3663e26 100644
--- a/src/NextjsBuild.ts
+++ b/src/NextjsBuild.ts
@@ -9,6 +9,7 @@ import { CompressionLevel, NextjsBaseProps } from './NextjsBase';
const NEXTJS_BUILD_DIR = '.open-next';
const NEXTJS_STATIC_DIR = 'assets';
const NEXTJS_BUILD_MIDDLEWARE_FN_DIR = 'middleware-function';
+const NEXTJS_BUILD_REVALIDATE_FN_DIR = 'revalidation-function';
const NEXTJS_BUILD_IMAGE_FN_DIR = 'image-optimization-function';
const NEXTJS_BUILD_SERVER_FN_DIR = 'server-function';
@@ -34,6 +35,10 @@ export class NextjsBuild extends Construct {
* Should be arm64.
*/
public nextImageFnDir: string;
+ /**
+ * Contains function for processing items from revalidation queue.
+ */
+ public nextRevalidateFnDir: string;
/**
* Static files containing client-side code.
*/
@@ -66,6 +71,7 @@ export class NextjsBuild extends Construct {
// our outputs
this.nextStaticDir = this._getNextStaticDir();
this.nextImageFnDir = this._getOutputDir(NEXTJS_BUILD_IMAGE_FN_DIR);
+ this.nextRevalidateFnDir = this._getOutputDir(NEXTJS_BUILD_REVALIDATE_FN_DIR);
this.nextServerFnDir = this._getOutputDir(NEXTJS_BUILD_SERVER_FN_DIR);
this.nextMiddlewareFnDir = this._getOutputDir(NEXTJS_BUILD_MIDDLEWARE_FN_DIR, true);
// this.nextDir = this._getNextDir();
diff --git a/src/NextjsRevalidation.ts b/src/NextjsRevalidation.ts
index 249f52ed..210f3aaa 100644
--- a/src/NextjsRevalidation.ts
+++ b/src/NextjsRevalidation.ts
@@ -37,7 +37,7 @@ export class NextjsRevalidation extends Construct {
? Code.fromInline(
"module.exports.handler = async () => { return { statusCode: 200, body: 'cdk-nextjs placeholder site' } }"
)
- : Code.fromAsset(props.nextBuild.nextImageFnDir);
+ : Code.fromAsset(props.nextBuild.nextRevalidateFnDir);
const queue = new Queue(this, 'RevalidationQueue', {
fifo: true,
From 64bb29dd6bda17834aeeedbc6b949933ed8c35f8 Mon Sep 17 00:00:00 2001
From: Kevin Mitchell <>
Date: Sun, 30 Jul 2023 20:44:10 -0400
Subject: [PATCH 07/16] fix: remove default origin fallback group for direct
lambda invocation
I'm not at all confident this is the "right" thing to do here, but without this I was having issues with S3 fallback serving 403s. It's entirely likely this is a shortsighted change and in reality I need to fix something on the S3 bucket in terms of how it handles 404 / 403s.
See SST implementation here: https://github.com/serverless-stack/sst/blob/b56c2ea021290211c72841c605cec58579ef3591/packages/sst/src/constructs/SsrSite.ts#L1053-L1058
---
src/NextjsDistribution.ts | 20 ++++++--------------
1 file changed, 6 insertions(+), 14 deletions(-)
diff --git a/src/NextjsDistribution.ts b/src/NextjsDistribution.ts
index cfc8dbe8..e9d45f7f 100644
--- a/src/NextjsDistribution.ts
+++ b/src/NextjsDistribution.ts
@@ -378,17 +378,6 @@ export class NextjsDistribution extends Construct {
},
];
- // default handler for requests that don't match any other path:
- // - try lambda handler first (/some-page, etc...)
- // - if 403, fall back to S3
- // - if 404, fall back to lambda handler
- // - if 503, fall back to lambda handler
- const fallbackOriginGroup = new origins.OriginGroup({
- primaryOrigin: serverFunctionOrigin,
- fallbackOrigin: s3Origin,
- fallbackStatusCodes: [403, 404, 503],
- });
-
const lambdaCachePolicy = cachePolicies?.lambdaCachePolicy ?? this.createCloudFrontLambdaCachePolicy();
// requests for static objects
@@ -461,15 +450,18 @@ export class NextjsDistribution extends Construct {
domainNames,
certificate: this.certificate,
defaultBehavior: {
- origin: fallbackOriginGroup, // try S3 first, then lambda
+ origin: new origins.HttpOrigin(Fn.parseDomainName(fnUrl.url), {
+ // todo: decide what an appropriate way to allow this to be configured is
+ readTimeout: Duration.seconds(10),
+ }),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
compress: true,
-
// what goes here? static or lambda?
cachePolicy: lambdaCachePolicy,
originRequestPolicy: fallbackOriginRequestPolicy,
-
edgeLambdas: lambdaOriginEdgeFns,
+ allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
+ cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
},
additionalBehaviors: {
From f7526557f0f787da5da7dad1bd3a8824618275e2 Mon Sep 17 00:00:00 2001
From: Kevin Mitchell <>
Date: Mon, 31 Jul 2023 17:26:26 -0400
Subject: [PATCH 08/16] revert default origin to original behavior
I now see why this was in place originally. It still doesn't work, but I'd rather leave it as it is for now.
SST creates additional behaviors in CloudFormation at deploy time, basically iterating through all of the files in the root of `/public` and creating a rule that routes to the static assets bucket for these matches:
https://github.com/serverless-stack/sst/blob/b56c2ea021290211c72841c605cec58579ef3591/packages/sst/src/constructs/SsrSite.ts#L1113
```
protected addStaticFileBehaviors() {
const { cdk } = this.props;
// Create a template for statics behaviours
const publicDir = path.join(
this.props.path,
this.buildConfig.clientBuildOutputDir
);
for (const item of fs.readdirSync(publicDir)) {
const isDir = fs.statSync(path.join(publicDir, item)).isDirectory();
this.distribution.addBehavior(isDir ? `${item}/*` : item, this.s3Origin, {
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
allowedMethods: AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
cachedMethods: CachedMethods.CACHE_GET_HEAD_OPTIONS,
compress: true,
cachePolicy: CachePolicy.CACHING_OPTIMIZED,
responseHeadersPolicy: cdk?.responseHeadersPolicy,
});
}
}
```
This is a bit more complicated with the pitfall of hitting the maximum number of behaviors (25 apparently).
> First do no evil.
---
src/NextjsDistribution.ts | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/src/NextjsDistribution.ts b/src/NextjsDistribution.ts
index e9d45f7f..d585e41e 100644
--- a/src/NextjsDistribution.ts
+++ b/src/NextjsDistribution.ts
@@ -378,6 +378,17 @@ export class NextjsDistribution extends Construct {
},
];
+ // default handler for requests that don't match any other path:
+ // - try lambda handler first (/some-page, etc...)
+ // - if 403, fall back to S3
+ // - if 404, fall back to lambda handler
+ // - if 503, fall back to lambda handler
+ const fallbackOriginGroup = new origins.OriginGroup({
+ primaryOrigin: serverFunctionOrigin,
+ fallbackOrigin: s3Origin,
+ fallbackStatusCodes: [403, 404, 503],
+ });
+
const lambdaCachePolicy = cachePolicies?.lambdaCachePolicy ?? this.createCloudFrontLambdaCachePolicy();
// requests for static objects
@@ -450,18 +461,13 @@ export class NextjsDistribution extends Construct {
domainNames,
certificate: this.certificate,
defaultBehavior: {
- origin: new origins.HttpOrigin(Fn.parseDomainName(fnUrl.url), {
- // todo: decide what an appropriate way to allow this to be configured is
- readTimeout: Duration.seconds(10),
- }),
+ origin: fallbackOriginGroup, // try S3 first, then lambda
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
compress: true,
// what goes here? static or lambda?
cachePolicy: lambdaCachePolicy,
originRequestPolicy: fallbackOriginRequestPolicy,
edgeLambdas: lambdaOriginEdgeFns,
- allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
- cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
},
additionalBehaviors: {
From 6f030503c5eaed8b11984e94aa1ecae4e0654f22 Mon Sep 17 00:00:00 2001
From: Kevin Mitchell <>
Date: Wed, 2 Aug 2023 11:57:32 -0400
Subject: [PATCH 09/16] setting the primary origin to the S3 bucket fixes asset
404 issue
There was quite a bit of discussion about this, and it's still not the best solution for all situations. For now it should fix an issue where direct requests to static assets (e.g. files in the `/public` directory) cause a 404.
https://github.com/jetbridge/cdk-nextjs/pull/125#discussion_r1279212678
---
src/NextjsDistribution.ts | 11 +++++------
1 file changed, 5 insertions(+), 6 deletions(-)
diff --git a/src/NextjsDistribution.ts b/src/NextjsDistribution.ts
index d585e41e..8d8669e3 100644
--- a/src/NextjsDistribution.ts
+++ b/src/NextjsDistribution.ts
@@ -379,13 +379,12 @@ export class NextjsDistribution extends Construct {
];
// default handler for requests that don't match any other path:
- // - try lambda handler first (/some-page, etc...)
- // - if 403, fall back to S3
- // - if 404, fall back to lambda handler
- // - if 503, fall back to lambda handler
+ // - try S3 bucket first
+ // - if 403, 404, or 503 fall back to Lambda handler
+ // see discussion here: https://github.com/jetbridge/cdk-nextjs/pull/125#discussion_r1279212678
const fallbackOriginGroup = new origins.OriginGroup({
- primaryOrigin: serverFunctionOrigin,
- fallbackOrigin: s3Origin,
+ primaryOrigin: s3Origin,
+ fallbackOrigin: serverFunctionOrigin,
fallbackStatusCodes: [403, 404, 503],
});
From 501e31337368bd7f872ce11ebf5ffcdfd4520653 Mon Sep 17 00:00:00 2001
From: Kevin Mitchell <>
Date: Wed, 2 Aug 2023 14:08:18 -0400
Subject: [PATCH 10/16] remove unnecessary runtime check and simplify cdk grant
---
src/NextjsRevalidation.ts | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/NextjsRevalidation.ts b/src/NextjsRevalidation.ts
index 210f3aaa..d53e11a3 100644
--- a/src/NextjsRevalidation.ts
+++ b/src/NextjsRevalidation.ts
@@ -31,8 +31,6 @@ export class NextjsRevalidation extends Construct {
constructor(scope: Construct, id: string, props: RevalidationProps) {
super(scope, id);
- if (!props.nextBuild) return;
-
const code = props.isPlaceholder
? Code.fromInline(
"module.exports.handler = async () => { return { statusCode: 200, body: 'cdk-nextjs placeholder site' } }"
@@ -56,6 +54,6 @@ export class NextjsRevalidation extends Construct {
const server = props.serverFunction.lambdaFunction;
server?.addEnvironment('REVALIDATION_QUEUE_URL', queue.queueUrl);
server?.addEnvironment('REVALIDATION_QUEUE_REGION', Stack.of(this).region);
- queue.grantSendMessages(server?.role!);
+ queue.grantSendMessages(server);
}
}
From 542e52e74dcf394e99ba4a2c32fcb32faa7af700 Mon Sep 17 00:00:00 2001
From: Kevin Mitchell <>
Date: Wed, 2 Aug 2023 16:55:13 -0400
Subject: [PATCH 11/16] comment to explain origin of code for revalidation and
image function
---
src/ImageOptimizationLambda.ts | 2 ++
src/NextjsRevalidation.ts | 5 +++++
2 files changed, 7 insertions(+)
diff --git a/src/ImageOptimizationLambda.ts b/src/ImageOptimizationLambda.ts
index 8dab6fc0..8b400742 100644
--- a/src/ImageOptimizationLambda.ts
+++ b/src/ImageOptimizationLambda.ts
@@ -48,6 +48,8 @@ export class ImageOptimizationLambda extends Function {
: Code.fromAsset(props.nextBuild.nextImageFnDir);
super(scope, id, {
+ // open-next image-optimization-function
+ // see: https://github.com/serverless-stack/open-next/blob/274d446ed7e940cfbe7ce05a21108f4c854ee37a/README.md?plain=1#L66
code,
handler: 'index.handler',
runtime: LAMBDA_RUNTIME,
diff --git a/src/NextjsRevalidation.ts b/src/NextjsRevalidation.ts
index d53e11a3..778a1755 100644
--- a/src/NextjsRevalidation.ts
+++ b/src/NextjsRevalidation.ts
@@ -26,6 +26,9 @@ export interface RevalidationProps extends NextjsBaseProps {
/**
* Builds the system for revaluating Next.js resources. This includes a Lambda function handler and queue system.
+ *
+ * @see {@link https://github.com/serverless-stack/open-next/blob/main/README.md?plain=1#L65}
+ *
*/
export class NextjsRevalidation extends Construct {
constructor(scope: Construct, id: string, props: RevalidationProps) {
@@ -44,6 +47,8 @@ export class NextjsRevalidation extends Construct {
const consumer = new Function(this, 'RevalidationFunction', {
description: 'Next.js revalidation function',
handler: 'index.handler',
+ // open-next revalidation-function
+ // see: https://github.com/serverless-stack/open-next/blob/274d446ed7e940cfbe7ce05a21108f4c854ee37a/README.md?plain=1#L65
code,
runtime: Runtime.NODEJS_18_X,
timeout: Duration.seconds(30),
From 94490cf93799e8a7ca7bc78127a5b2ec227a2de1 Mon Sep 17 00:00:00 2001
From: Kevin Mitchell <>
Date: Thu, 3 Aug 2023 16:45:42 -0400
Subject: [PATCH 12/16] add cloudfront invalidation for all paths with optional
skip
---
API.md | 146 ++++++++++++++++++++++++++++++++++++++++++++++
src/Nextjs.ts | 20 +++++--
src/NextjsBase.ts | 7 +++
3 files changed, 168 insertions(+), 5 deletions(-)
diff --git a/API.md b/API.md
index c95b5e7c..c40afcf1 100644
--- a/API.md
+++ b/API.md
@@ -2548,6 +2548,8 @@ The runtimes compatible with this Layer.
Builds the system for revaluating Next.js resources. This includes a Lambda function handler and queue system.
+> [{@link https://github.com/serverless-stack/open-next/blob/main/README.md?plain=1#L65}]({@link https://github.com/serverless-stack/open-next/blob/main/README.md?plain=1#L65})
+
#### Initializers
```typescript
@@ -3076,6 +3078,7 @@ const imageOptimizationProps: ImageOptimizationProps = { ... }
| projectRoot
| string
| Root of your project, if different from `nextjsPath`. |
| quiet
| boolean
| Less build output. |
| sharpLayerArn
| string
| Optional arn for the sharp lambda layer. |
+| skipFullInvalidation
| boolean
| By default all CloudFront cache will be invalidated on deployment. |
| tempBuildDir
| string
| Directory to store temporary build files in. |
| bucket
| aws-cdk-lib.aws_s3.IBucket
| The S3 bucket holding application images. |
| nextBuild
| NextjsBuild
| The `NextjsBuild` instance representing the built Nextjs application. |
@@ -3218,6 +3221,21 @@ If omitted, the layer will be created.
---
+##### `skipFullInvalidation`Optional
+
+```typescript
+public readonly skipFullInvalidation: boolean;
+```
+
+- *Type:* boolean
+
+By default all CloudFront cache will be invalidated on deployment.
+
+This can be set to true to skip the full cache invalidation, which
+could be important for some users.
+
+---
+
##### `tempBuildDir`Optional
```typescript
@@ -3339,6 +3357,7 @@ const nextjsAssetsDeploymentProps: NextjsAssetsDeploymentProps = { ... }
| projectRoot
| string
| Root of your project, if different from `nextjsPath`. |
| quiet
| boolean
| Less build output. |
| sharpLayerArn
| string
| Optional arn for the sharp lambda layer. |
+| skipFullInvalidation
| boolean
| By default all CloudFront cache will be invalidated on deployment. |
| tempBuildDir
| string
| Directory to store temporary build files in. |
| bucket
| aws-cdk-lib.aws_s3.IBucket
| Properties for the S3 bucket containing the NextJS assets. |
| nextBuild
| NextjsBuild
| The `NextjsBuild` instance representing the built Nextjs application. |
@@ -3487,6 +3506,21 @@ If omitted, the layer will be created.
---
+##### `skipFullInvalidation`Optional
+
+```typescript
+public readonly skipFullInvalidation: boolean;
+```
+
+- *Type:* boolean
+
+By default all CloudFront cache will be invalidated on deployment.
+
+This can be set to true to skip the full cache invalidation, which
+could be important for some users.
+
+---
+
##### `tempBuildDir`Optional
```typescript
@@ -3637,6 +3671,7 @@ const nextjsBaseProps: NextjsBaseProps = { ... }
| projectRoot
| string
| Root of your project, if different from `nextjsPath`. |
| quiet
| boolean
| Less build output. |
| sharpLayerArn
| string
| Optional arn for the sharp lambda layer. |
+| skipFullInvalidation
| boolean
| By default all CloudFront cache will be invalidated on deployment. |
| tempBuildDir
| string
| Directory to store temporary build files in. |
---
@@ -3776,6 +3811,21 @@ If omitted, the layer will be created.
---
+##### `skipFullInvalidation`Optional
+
+```typescript
+public readonly skipFullInvalidation: boolean;
+```
+
+- *Type:* boolean
+
+By default all CloudFront cache will be invalidated on deployment.
+
+This can be set to true to skip the full cache invalidation, which
+could be important for some users.
+
+---
+
##### `tempBuildDir`Optional
```typescript
@@ -3814,6 +3864,7 @@ const nextjsBuildProps: NextjsBuildProps = { ... }
| projectRoot
| string
| Root of your project, if different from `nextjsPath`. |
| quiet
| boolean
| Less build output. |
| sharpLayerArn
| string
| Optional arn for the sharp lambda layer. |
+| skipFullInvalidation
| boolean
| By default all CloudFront cache will be invalidated on deployment. |
| tempBuildDir
| string
| Directory to store temporary build files in. |
---
@@ -3953,6 +4004,21 @@ If omitted, the layer will be created.
---
+##### `skipFullInvalidation`Optional
+
+```typescript
+public readonly skipFullInvalidation: boolean;
+```
+
+- *Type:* boolean
+
+By default all CloudFront cache will be invalidated on deployment.
+
+This can be set to true to skip the full cache invalidation, which
+could be important for some users.
+
+---
+
##### `tempBuildDir`Optional
```typescript
@@ -4161,6 +4227,7 @@ const nextjsDistributionProps: NextjsDistributionProps = { ... }
| projectRoot
| string
| Root of your project, if different from `nextjsPath`. |
| quiet
| boolean
| Less build output. |
| sharpLayerArn
| string
| Optional arn for the sharp lambda layer. |
+| skipFullInvalidation
| boolean
| By default all CloudFront cache will be invalidated on deployment. |
| tempBuildDir
| string
| Directory to store temporary build files in. |
| imageOptFunction
| aws-cdk-lib.aws_lambda.IFunction
| Lambda function to optimize images. |
| nextBuild
| NextjsBuild
| Built NextJS app. |
@@ -4311,6 +4378,21 @@ If omitted, the layer will be created.
---
+##### `skipFullInvalidation`Optional
+
+```typescript
+public readonly skipFullInvalidation: boolean;
+```
+
+- *Type:* boolean
+
+By default all CloudFront cache will be invalidated on deployment.
+
+This can be set to true to skip the full cache invalidation, which
+could be important for some users.
+
+---
+
##### `tempBuildDir`Optional
```typescript
@@ -4618,6 +4700,7 @@ const nextjsLambdaProps: NextjsLambdaProps = { ... }
| projectRoot
| string
| Root of your project, if different from `nextjsPath`. |
| quiet
| boolean
| Less build output. |
| sharpLayerArn
| string
| Optional arn for the sharp lambda layer. |
+| skipFullInvalidation
| boolean
| By default all CloudFront cache will be invalidated on deployment. |
| tempBuildDir
| string
| Directory to store temporary build files in. |
| cacheBucket
| aws-cdk-lib.aws_s3.IBucket
| The S3 bucket holding application cache. |
| nextBuild
| NextjsBuild
| Built nextJS application. |
@@ -4760,6 +4843,21 @@ If omitted, the layer will be created.
---
+##### `skipFullInvalidation`Optional
+
+```typescript
+public readonly skipFullInvalidation: boolean;
+```
+
+- *Type:* boolean
+
+By default all CloudFront cache will be invalidated on deployment.
+
+This can be set to true to skip the full cache invalidation, which
+could be important for some users.
+
+---
+
##### `tempBuildDir`Optional
```typescript
@@ -4895,6 +4993,7 @@ const nextjsProps: NextjsProps = { ... }
| projectRoot
| string
| Root of your project, if different from `nextjsPath`. |
| quiet
| boolean
| Less build output. |
| sharpLayerArn
| string
| Optional arn for the sharp lambda layer. |
+| skipFullInvalidation
| boolean
| By default all CloudFront cache will be invalidated on deployment. |
| tempBuildDir
| string
| Directory to store temporary build files in. |
| defaults
| NextjsDefaultsProps
| Allows you to override defaults for the resources created by this construct. |
| imageOptimizationBucket
| aws-cdk-lib.aws_s3.IBucket
| Optional S3 Bucket to use, defaults to assets bucket. |
@@ -5036,6 +5135,21 @@ If omitted, the layer will be created.
---
+##### `skipFullInvalidation`Optional
+
+```typescript
+public readonly skipFullInvalidation: boolean;
+```
+
+- *Type:* boolean
+
+By default all CloudFront cache will be invalidated on deployment.
+
+This can be set to true to skip the full cache invalidation, which
+could be important for some users.
+
+---
+
##### `tempBuildDir`Optional
```typescript
@@ -5098,6 +5212,7 @@ const nextjsS3EnvRewriterProps: NextjsS3EnvRewriterProps = { ... }
| projectRoot
| string
| Root of your project, if different from `nextjsPath`. |
| quiet
| boolean
| Less build output. |
| sharpLayerArn
| string
| Optional arn for the sharp lambda layer. |
+| skipFullInvalidation
| boolean
| By default all CloudFront cache will be invalidated on deployment. |
| tempBuildDir
| string
| Directory to store temporary build files in. |
| replacementConfig
| RewriteReplacementsConfig
| *No description.* |
| s3Bucket
| aws-cdk-lib.aws_s3.IBucket
| *No description.* |
@@ -5242,6 +5357,21 @@ If omitted, the layer will be created.
---
+##### `skipFullInvalidation`Optional
+
+```typescript
+public readonly skipFullInvalidation: boolean;
+```
+
+- *Type:* boolean
+
+By default all CloudFront cache will be invalidated on deployment.
+
+This can be set to true to skip the full cache invalidation, which
+could be important for some users.
+
+---
+
##### `tempBuildDir`Optional
```typescript
@@ -5330,6 +5460,7 @@ const revalidationProps: RevalidationProps = { ... }
| projectRoot
| string
| Root of your project, if different from `nextjsPath`. |
| quiet
| boolean
| Less build output. |
| sharpLayerArn
| string
| Optional arn for the sharp lambda layer. |
+| skipFullInvalidation
| boolean
| By default all CloudFront cache will be invalidated on deployment. |
| tempBuildDir
| string
| Directory to store temporary build files in. |
| nextBuild
| NextjsBuild
| The `NextjsBuild` instance representing the built Nextjs application. |
| serverFunction
| NextJsLambda
| The main NextJS server handler lambda function. |
@@ -5472,6 +5603,21 @@ If omitted, the layer will be created.
---
+##### `skipFullInvalidation`Optional
+
+```typescript
+public readonly skipFullInvalidation: boolean;
+```
+
+- *Type:* boolean
+
+By default all CloudFront cache will be invalidated on deployment.
+
+This can be set to true to skip the full cache invalidation, which
+could be important for some users.
+
+---
+
##### `tempBuildDir`Optional
```typescript
diff --git a/src/Nextjs.ts b/src/Nextjs.ts
index 58c2fc76..2ce64fce 100644
--- a/src/Nextjs.ts
+++ b/src/Nextjs.ts
@@ -172,11 +172,6 @@ export class Nextjs extends Construct {
serverFunction: this.serverFunction,
});
- new BucketDeployment(this, 'DeployCacheFiles', {
- sources: [Source.asset(this.nextBuild.nextCacheDir)],
- destinationBucket: this.cacheBucket,
- });
-
// deploy nextjs static assets to s3
this.assetsDeployment = new NextJsAssetsDeployment(this, 'AssetDeployment', {
...props,
@@ -204,6 +199,21 @@ export class Nextjs extends Construct {
imageOptFunction: this.imageOptimizationFunction,
});
+ // We only want to provide the distribution options below if
+ // we are keep to invalidate the cache
+ const invalidationOptions = this.props.skipFullInvalidation
+ ? {}
+ : {
+ distribution: this.distribution.distribution,
+ distributionPaths: ['/*'],
+ };
+
+ new BucketDeployment(this, 'DeployCacheFiles', {
+ sources: [Source.asset(this.nextBuild.nextCacheDir)],
+ destinationBucket: this.cacheBucket,
+ ...invalidationOptions,
+ });
+
if (!props.quiet) console.debug('└ Finished preparing NextJS app for deployment');
}
diff --git a/src/NextjsBase.ts b/src/NextjsBase.ts
index eca0bac6..7bcae24b 100644
--- a/src/NextjsBase.ts
+++ b/src/NextjsBase.ts
@@ -72,6 +72,13 @@ export interface NextjsBaseProps {
* If omitted, the layer will be created.
*/
readonly sharpLayerArn?: string;
+
+ /**
+ * By default all CloudFront cache will be invalidated on deployment.
+ * This can be set to true to skip the full cache invalidation, which
+ * could be important for some users.
+ */
+ readonly skipFullInvalidation?: boolean;
}
///// stuff below taken from https://github.com/serverless-stack/sst/blob/8d377e941467ced81d8cc31ee67d5a06550f04d4/packages/resources/src/BaseSite.ts
From a6fbcfafeca9d2ce4dfb18ad7cf5ed6460d2cf83 Mon Sep 17 00:00:00 2001
From: Kevin Mitchell <>
Date: Thu, 3 Aug 2023 21:29:06 -0400
Subject: [PATCH 13/16] fix typo and a rebase issue
I rebased on main to prep for a potential landing but somehow lost the changes in NextjsBuild. Also a typo fix.
---
API.md | 24 ++++++++++++------------
src/NextjsBase.ts | 2 +-
src/NextjsBuild.ts | 11 +++++++++++
src/NextjsRevalidation.ts | 2 +-
4 files changed, 25 insertions(+), 14 deletions(-)
diff --git a/API.md b/API.md
index c40afcf1..30ed9cad 100644
--- a/API.md
+++ b/API.md
@@ -1702,7 +1702,7 @@ Any object.
| **Name** | **Type** | **Description** |
| --- | --- | --- |
| node
| constructs.Node
| The tree node. |
-| nextCacheDir
| string
| Cache setup directory. |
+| nextCacheDir
| string
| Cache directory for generated data. |
| nextImageFnDir
| string
| Contains function for processessing image requests. |
| nextRevalidateFnDir
| string
| Contains function for processing items from revalidation queue. |
| nextServerFnDir
| string
| Contains server code and dependencies. |
@@ -1733,7 +1733,7 @@ public readonly nextCacheDir: string;
- *Type:* string
-Cache setup directory.
+Cache directory for generated data.
---
@@ -2546,7 +2546,7 @@ The runtimes compatible with this Layer.
### NextjsRevalidation
-Builds the system for revaluating Next.js resources. This includes a Lambda function handler and queue system.
+Builds the system for revalidating Next.js resources. This includes a Lambda function handler and queue system.
> [{@link https://github.com/serverless-stack/open-next/blob/main/README.md?plain=1#L65}]({@link https://github.com/serverless-stack/open-next/blob/main/README.md?plain=1#L65})
@@ -3110,7 +3110,7 @@ public readonly buildCommand: string;
Optional value used to install NextJS node dependencies.
-It defaults to 'npx --yes open-next@latest build'
+It defaults to 'npx --yes open-next@2 build'
---
@@ -3395,7 +3395,7 @@ public readonly buildCommand: string;
Optional value used to install NextJS node dependencies.
-It defaults to 'npx --yes open-next@latest build'
+It defaults to 'npx --yes open-next@2 build'
---
@@ -3700,7 +3700,7 @@ public readonly buildCommand: string;
Optional value used to install NextJS node dependencies.
-It defaults to 'npx --yes open-next@latest build'
+It defaults to 'npx --yes open-next@2 build'
---
@@ -3893,7 +3893,7 @@ public readonly buildCommand: string;
Optional value used to install NextJS node dependencies.
-It defaults to 'npx --yes open-next@latest build'
+It defaults to 'npx --yes open-next@2 build'
---
@@ -4267,7 +4267,7 @@ public readonly buildCommand: string;
Optional value used to install NextJS node dependencies.
-It defaults to 'npx --yes open-next@latest build'
+It defaults to 'npx --yes open-next@2 build'
---
@@ -4732,7 +4732,7 @@ public readonly buildCommand: string;
Optional value used to install NextJS node dependencies.
-It defaults to 'npx --yes open-next@latest build'
+It defaults to 'npx --yes open-next@2 build'
---
@@ -5024,7 +5024,7 @@ public readonly buildCommand: string;
Optional value used to install NextJS node dependencies.
-It defaults to 'npx --yes open-next@latest build'
+It defaults to 'npx --yes open-next@2 build'
---
@@ -5246,7 +5246,7 @@ public readonly buildCommand: string;
Optional value used to install NextJS node dependencies.
-It defaults to 'npx --yes open-next@latest build'
+It defaults to 'npx --yes open-next@2 build'
---
@@ -5492,7 +5492,7 @@ public readonly buildCommand: string;
Optional value used to install NextJS node dependencies.
-It defaults to 'npx --yes open-next@latest build'
+It defaults to 'npx --yes open-next@2 build'
---
diff --git a/src/NextjsBase.ts b/src/NextjsBase.ts
index 7bcae24b..83def26e 100644
--- a/src/NextjsBase.ts
+++ b/src/NextjsBase.ts
@@ -51,7 +51,7 @@ export interface NextjsBaseProps {
/**
* Optional value used to install NextJS node dependencies.
- * It defaults to 'npx --yes open-next@1 build'
+ * It defaults to 'npx --yes open-next@2 build'
*/
readonly buildCommand?: string;
diff --git a/src/NextjsBuild.ts b/src/NextjsBuild.ts
index b3663e26..5a107a2f 100644
--- a/src/NextjsBuild.ts
+++ b/src/NextjsBuild.ts
@@ -8,6 +8,7 @@ import { CompressionLevel, NextjsBaseProps } from './NextjsBase';
const NEXTJS_BUILD_DIR = '.open-next';
const NEXTJS_STATIC_DIR = 'assets';
+const NEXTJS_CACHE_DIR = 'cache';
const NEXTJS_BUILD_MIDDLEWARE_FN_DIR = 'middleware-function';
const NEXTJS_BUILD_REVALIDATE_FN_DIR = 'revalidation-function';
const NEXTJS_BUILD_IMAGE_FN_DIR = 'image-optimization-function';
@@ -43,6 +44,10 @@ export class NextjsBuild extends Construct {
* Static files containing client-side code.
*/
public nextStaticDir: string;
+ /**
+ * Cache directory for generated data.
+ */
+ public nextCacheDir: string;
public props: NextjsBuildProps;
@@ -70,6 +75,7 @@ export class NextjsBuild extends Construct {
// our outputs
this.nextStaticDir = this._getNextStaticDir();
+ this.nextCacheDir = this._getNextCacheDir();
this.nextImageFnDir = this._getOutputDir(NEXTJS_BUILD_IMAGE_FN_DIR);
this.nextRevalidateFnDir = this._getOutputDir(NEXTJS_BUILD_REVALIDATE_FN_DIR);
this.nextServerFnDir = this._getOutputDir(NEXTJS_BUILD_SERVER_FN_DIR);
@@ -161,6 +167,11 @@ export class NextjsBuild extends Construct {
private _getNextStaticDir() {
return path.join(this._getNextBuildDir(), NEXTJS_STATIC_DIR);
}
+
+ // contains cache files
+ private _getNextCacheDir() {
+ return path.join(this._getNextBuildDir(), NEXTJS_CACHE_DIR);
+ }
}
export interface CreateArchiveArgs {
diff --git a/src/NextjsRevalidation.ts b/src/NextjsRevalidation.ts
index 778a1755..1e6c61dc 100644
--- a/src/NextjsRevalidation.ts
+++ b/src/NextjsRevalidation.ts
@@ -25,7 +25,7 @@ export interface RevalidationProps extends NextjsBaseProps {
}
/**
- * Builds the system for revaluating Next.js resources. This includes a Lambda function handler and queue system.
+ * Builds the system for revalidating Next.js resources. This includes a Lambda function handler and queue system.
*
* @see {@link https://github.com/serverless-stack/open-next/blob/main/README.md?plain=1#L65}
*
From e6a76bc80d8036883f7f7d8036423e58cfbfb5cb Mon Sep 17 00:00:00 2001
From: Ben Stickley <35735118+bestickley@users.noreply.github.com>
Date: Fri, 4 Aug 2023 09:46:25 -0400
Subject: [PATCH 14/16] Update order of environment in NextjsLambda.ts
---
src/NextjsLambda.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/NextjsLambda.ts b/src/NextjsLambda.ts
index 718f30de..96c53d56 100644
--- a/src/NextjsLambda.ts
+++ b/src/NextjsLambda.ts
@@ -92,12 +92,15 @@ export class NextJsLambda extends Construct {
runtime: LAMBDA_RUNTIME,
handler: path.join('index.handler'),
code,
- environment,
// prevents "Resolution error: Cannot use resource in a cross-environment
// fashion, the resource's physical name must be explicit set or use
// PhysicalName.GENERATE_IF_NEEDED."
functionName: Stack.of(this).region !== 'us-east-1' ? PhysicalName.GENERATE_IF_NEEDED : undefined,
...functionOptions,
+ // `environment` needs to go after `functionOptions` b/c if
+ // `functionOptions.environment` is defined, it will override
+ // CACHE_* environment variables which are required
+ environment,
});
this.lambdaFunction = fn;
From e49b5aa33c42c304f32a9316c11c201b6696a17a Mon Sep 17 00:00:00 2001
From: Ben Stickley
Date: Fri, 4 Aug 2023 14:02:02 -0400
Subject: [PATCH 15/16] feat: use static asset bucket for cache
---
API.md | 20 +++++++++++---------
src/Nextjs.ts | 15 ++++-----------
src/NextjsLambda.ts | 15 +++++++--------
src/constants.ts | 2 ++
4 files changed, 24 insertions(+), 28 deletions(-)
diff --git a/API.md b/API.md
index 30ed9cad..854579ae 100644
--- a/API.md
+++ b/API.md
@@ -4702,8 +4702,8 @@ const nextjsLambdaProps: NextjsLambdaProps = { ... }
| sharpLayerArn
| string
| Optional arn for the sharp lambda layer. |
| skipFullInvalidation
| boolean
| By default all CloudFront cache will be invalidated on deployment. |
| tempBuildDir
| string
| Directory to store temporary build files in. |
-| cacheBucket
| aws-cdk-lib.aws_s3.IBucket
| The S3 bucket holding application cache. |
| nextBuild
| NextjsBuild
| Built nextJS application. |
+| staticAssetBucket
| aws-cdk-lib.aws_s3.IBucket
| Static asset bucket. |
| lambda
| aws-cdk-lib.aws_lambda.FunctionOptions
| Override function properties. |
---
@@ -4872,27 +4872,29 @@ Defaults to os.tmpdir().
---
-##### `cacheBucket`Required
+##### `nextBuild`Required
```typescript
-public readonly cacheBucket: IBucket;
+public readonly nextBuild: NextjsBuild;
```
-- *Type:* aws-cdk-lib.aws_s3.IBucket
+- *Type:* NextjsBuild
-The S3 bucket holding application cache.
+Built nextJS application.
---
-##### `nextBuild`Required
+##### `staticAssetBucket`Required
```typescript
-public readonly nextBuild: NextjsBuild;
+public readonly staticAssetBucket: IBucket;
```
-- *Type:* NextjsBuild
+- *Type:* aws-cdk-lib.aws_s3.IBucket
-Built nextJS application.
+Static asset bucket.
+
+Function needs bucket to read from cache.
---
diff --git a/src/Nextjs.ts b/src/Nextjs.ts
index 2ce64fce..206f5c2d 100644
--- a/src/Nextjs.ts
+++ b/src/Nextjs.ts
@@ -8,6 +8,7 @@ import * as s3 from 'aws-cdk-lib/aws-s3';
import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';
import { Construct } from 'constructs';
import * as fs from 'fs-extra';
+import { CACHE_BUCKET_KEY_PREFIX } from './constants';
import { ImageOptimizationLambda } from './ImageOptimizationLambda';
import { NextJsAssetsDeployment, NextjsAssetsDeploymentProps } from './NextjsAssetsDeployment';
import { BaseSiteDomainProps, NextjsBaseProps } from './NextjsBase';
@@ -116,7 +117,6 @@ export class Nextjs extends Construct {
public imageOptimizationLambdaFunctionUrl!: lambda.FunctionUrl;
protected staticAssetBucket: s3.IBucket;
- protected cacheBucket: s3.IBucket;
constructor(scope: Construct, id: string, protected props: NextjsProps) {
super(scope, id);
@@ -140,14 +140,6 @@ export class Nextjs extends Construct {
autoDeleteObjects: true,
});
- // create cache bucket
- this.cacheBucket =
- props.defaults?.cacheBucket ??
- new s3.Bucket(this, 'Cache', {
- removalPolicy: RemovalPolicy.DESTROY,
- autoDeleteObjects: true,
- });
-
// build nextjs app
this.nextBuild = new NextjsBuild(this, id, { ...props, tempBuildDir });
this.serverFunction = new NextJsLambda(this, 'ServerFn', {
@@ -155,7 +147,7 @@ export class Nextjs extends Construct {
tempBuildDir,
nextBuild: this.nextBuild,
lambda: props.defaults?.lambda,
- cacheBucket: this.cacheBucket,
+ staticAssetBucket: this.staticAssetBucket,
});
// build image optimization
this.imageOptimizationFunction = new ImageOptimizationLambda(this, 'ImgOptFn', {
@@ -210,7 +202,8 @@ export class Nextjs extends Construct {
new BucketDeployment(this, 'DeployCacheFiles', {
sources: [Source.asset(this.nextBuild.nextCacheDir)],
- destinationBucket: this.cacheBucket,
+ destinationBucket: this.staticAssetBucket,
+ destinationKeyPrefix: CACHE_BUCKET_KEY_PREFIX,
...invalidationOptions,
});
diff --git a/src/NextjsLambda.ts b/src/NextjsLambda.ts
index 96c53d56..b1167727 100644
--- a/src/NextjsLambda.ts
+++ b/src/NextjsLambda.ts
@@ -9,7 +9,7 @@ import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
import { Construct } from 'constructs';
import * as fs from 'fs-extra';
-import { LAMBDA_RUNTIME, DEFAULT_LAMBA_MEMORY } from './constants';
+import { LAMBDA_RUNTIME, DEFAULT_LAMBA_MEMORY, CACHE_BUCKET_KEY_PREFIX } from './constants';
import { CONFIG_ENV_JSON_PATH } from './Nextjs';
import { NextjsBaseProps } from './NextjsBase';
import { createArchive, NextjsBuild } from './NextjsBuild';
@@ -23,10 +23,9 @@ function getEnvironment(props: NextjsLambdaProps): { [name: string]: string } {
...props.lambda?.environment,
...(props.nodeEnv ? { NODE_ENV: props.nodeEnv } : {}),
...{
- CACHE_BUCKET_NAME: props.cacheBucket?.bucketName || '',
- CACHE_BUCKET_REGION: props.cacheBucket ? Stack.of(props.cacheBucket).region : '',
- // Note we don't need a CACHE_BUCKET_KEY_PREFIX because we're using a separate bucket for cache
- // @see: https://github.com/serverless-stack/sst/blob/master/packages/sst/src/constructs/NextjsSite.ts#L158
+ CACHE_BUCKET_NAME: props.staticAssetBucket?.bucketName || '',
+ CACHE_BUCKET_REGION: Stack.of(props.staticAssetBucket).region,
+ CACHE_BUCKET_KEY_PREFIX,
},
};
@@ -45,9 +44,9 @@ export interface NextjsLambdaProps extends NextjsBaseProps {
readonly lambda?: FunctionOptions;
/**
- * The S3 bucket holding application cache.
+ * Static asset bucket. Function needs bucket to read from cache.
*/
- readonly cacheBucket: IBucket;
+ readonly staticAssetBucket: IBucket;
}
/**
@@ -104,7 +103,7 @@ export class NextJsLambda extends Construct {
});
this.lambdaFunction = fn;
- props.cacheBucket.grantReadWrite(fn);
+ props.staticAssetBucket.grantReadWrite(fn);
// rewrite env var placeholders in server code
const replacementParams = this._getReplacementParams(environment);
diff --git a/src/constants.ts b/src/constants.ts
index c239ac9c..f6be3ab9 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -11,3 +11,5 @@ export const LAMBDA_RUNTIME = Runtime.NODEJS_18_X;
* @see {@link https://dev.to/dashbird/4-tips-for-aws-lambda-optimization-for-production-3if1}
*/
export const DEFAULT_LAMBA_MEMORY = 1536;
+
+export const CACHE_BUCKET_KEY_PREFIX = '_cache';
From 09fadd99e4ead59a084b1f00a8ec2a539ebde072 Mon Sep 17 00:00:00 2001
From: Ben Stickley
Date: Fri, 4 Aug 2023 14:13:47 -0400
Subject: [PATCH 16/16] build: use beta prerelease
---
.projenrc.js | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/.projenrc.js b/.projenrc.js
index f6783792..434bcbfe 100644
--- a/.projenrc.js
+++ b/.projenrc.js
@@ -15,7 +15,7 @@ const project = new awscdk.AwsCdkConstructLibrary({
// ignorePatterns: ['assets/**/*']
},
majorVersion: 3,
- // prerelease: 'pre',
+ prerelease: 'beta',
tsconfig: { compilerOptions: { noUnusedLocals: false }, include: ['assets/**/*.ts'] },
tsconfigDev: { compilerOptions: { noUnusedLocals: false } },
@@ -44,6 +44,7 @@ const project = new awscdk.AwsCdkConstructLibrary({
// do not generate sample test files
sampleCode: false,
});
+
const packageJson = project.tryFindObjectFile('package.json');
if (packageJson) {
packageJson.patch(
@@ -53,8 +54,5 @@ if (packageJson) {
])
);
}
-// project.eslint.addOverride({
-// rules: {},
-// });
-// project.tsconfig.addInclude('assets/**/*.ts');
+
project.synth();