Skip to content

Commit

Permalink
ref(cdk, images): cdk simplified to single APIGW, image handler bundl…
Browse files Browse the repository at this point in the history
…ing fixed and simplified to not include layers
  • Loading branch information
Bender committed Oct 8, 2022
1 parent 5477aa9 commit f0beeec
Show file tree
Hide file tree
Showing 12 changed files with 3,793 additions and 214 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ node_modules/**
.env*
!.env.example
dist/**
.webpack

# Temporary build folder.
nodejs/**
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
16
16.15
16 changes: 7 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,22 @@ This library uses Cloudfront, S3, ApiGateway and Lambdas to deploy easily in sec
- Run `npx --package @sladg/nextjs-lambda next-utils deploy` (will deploy to AWS).
- Profit 🎉



At this point, advanced features were not tested with this setup. This includes:
---

- [x] Render frontfacing pages in Lambda
- [x] Render API routes in Lambda
- [x] Image optimization
- [x] NextJS headers (next.config.js)
- [x] [GetStaticPaths](https://nextjs.org/docs/basic-features/data-fetching/get-static-paths)
- [ ] next-intl (i18n)
- [ ] [GetServerSideProps](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props)
- [ ] [GetStaticProps](https://nextjs.org/docs/basic-features/data-fetching/get-static-props)
- [ ] [Middleware](https://nextjs.org/docs/advanced-features/middleware)
- [x] next-intl (i18n)
- [x] [Middleware](https://nextjs.org/docs/advanced-features/middleware)
- [x] [GetServerSideProps](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props)
- [x] [GetStaticProps](https://nextjs.org/docs/basic-features/data-fetching/get-static-props)
- [x] NextJS rewrites (next.config.js)
- [ ] [ISR and fallbacks](https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration)
- [ ] [Streaming](https://nextjs.org/docs/advanced-features/react-18/streaming)
- [ ] Custom babel configuration

I am looking for advanced projects implementing those features, so we can test them out! Reach out to me!


## Usage

Expand Down
46 changes: 17 additions & 29 deletions cdk/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ class NextStandaloneStack extends Stack {
super(scope, id, props)

const config = {
apigwServerPath: '/_server',
apigwImagePath: '/_image',
assetsZipPath: path.resolve(commandCwd, './next.out/assetsLayer.zip'),
codeZipPath: path.resolve(commandCwd, './next.out/code.zip'),
dependenciesZipPath: path.resolve(commandCwd, './next.out/dependenciesLayer.zip'),
sharpLayerZipPath: path.resolve(cdkFolder, '../dist/sharp-layer.zip'),
nextLayerZipPath: path.resolve(cdkFolder, '../dist/next-layer.zip'),
imageHandlerZipPath: path.resolve(cdkFolder, '../dist/image-handler.zip'),
customServerHandler: 'handler.handler',
customImageHandler: 'index.handler',
customImageHandler: 'handler.handler',
cfnViewerCertificate: undefined,
...props,
}
Expand All @@ -42,28 +42,19 @@ class NextStandaloneStack extends Stack {
description: `${depsPrefix}-deps`,
})

// @TODO: Load Sharp version from source package.json so we respect it.
const sharpLayer = new LayerVersion(this, 'SharpLayer', {
code: Code.fromAsset(config.sharpLayerZipPath, { assetHashType: AssetHashType.CUSTOM, assetHash: depsPrefix }),
description: `${depsPrefix}-sharp`,
})

// @TODO: Load Next version from source package.json so we respect it.
const nextLayer = new LayerVersion(this, 'NextLayer', {
code: Code.fromAsset(config.nextLayerZipPath, { assetHashType: AssetHashType.CUSTOM, assetHash: depsPrefix }),
description: `${depsPrefix}-next`,
})

const serverLambda = new Function(this, 'DefaultNextJs', {
code: Code.fromAsset(config.codeZipPath, {
followSymlinks: SymlinkFollowMode.NEVER,
}),
runtime: Runtime.NODEJS_16_X,
handler: config.customServerHandler,
layers: [depsLayer, sharpLayer, nextLayer],
layers: [depsLayer],
// No need for big memory as image handling is done elsewhere.
memorySize: 512,
timeout: Duration.seconds(15),
environment: {
NEXTJS_LAMBDA_BASE_PATH: config.apigwServerPath,
},
})

const assetsBucket = new Bucket(this, 'NextAssetsBucket', {
Expand All @@ -77,7 +68,6 @@ class NextStandaloneStack extends Stack {
code: Code.fromAsset(config.imageHandlerZipPath),
runtime: Runtime.NODEJS_16_X,
handler: config.customImageHandler,
layers: [sharpLayer, nextLayer],
memorySize: 1024,
timeout: Duration.seconds(10),
environment: {
Expand All @@ -87,15 +77,12 @@ class NextStandaloneStack extends Stack {

assetsBucket.grantRead(imageLambda)

const serverApigatewayProxy = new HttpApi(this, 'ServerProxy', {
createDefaultStage: true,
defaultIntegration: new HttpLambdaIntegration('LambdaApigwIntegration', serverLambda),
})
const apigatewayProxy = new HttpApi(this, 'ServerProxy')

const imageApigatewayProxy = new HttpApi(this, 'ImagesProxy', {
createDefaultStage: true,
defaultIntegration: new HttpLambdaIntegration('ImagesApigwIntegration', imageLambda),
})
// We could do parameter mapping here and remove prefix from path.
// However passing env var (basePath) is easier to use, understand and integrate to other solutions.
apigatewayProxy.addRoutes({ path: `${config.apigwServerPath}/{proxy+}`, integration: new HttpLambdaIntegration('LambdaApigwIntegration', serverLambda) })
apigatewayProxy.addRoutes({ path: `${config.apigwImagePath}/{proxy+}`, integration: new HttpLambdaIntegration('ImagesApigwIntegration', imageLambda) })

const s3AssetsIdentity = new OriginAccessIdentity(this, 'OAICfnDistroS3', {
comment: 'Allows CloudFront to access S3 bucket with assets',
Expand Down Expand Up @@ -123,7 +110,8 @@ class NextStandaloneStack extends Stack {
},
],
customOriginSource: {
domainName: `${serverApigatewayProxy.apiId}.execute-api.${this.region}.amazonaws.com`,
originPath: config.apigwServerPath,
domainName: `${apigatewayProxy.apiId}.execute-api.${this.region}.amazonaws.com`,
},
},
{
Expand All @@ -137,7 +125,8 @@ class NextStandaloneStack extends Stack {
},
],
customOriginSource: {
domainName: `${imageApigatewayProxy.apiId}.execute-api.${this.region}.amazonaws.com`,
originPath: config.apigwImagePath,
domainName: `${apigatewayProxy.apiId}.execute-api.${this.region}.amazonaws.com`,
},
},
{
Expand Down Expand Up @@ -171,8 +160,7 @@ class NextStandaloneStack extends Stack {

new CfnOutput(this, 'cfnDistroUrl', { value: cfnDistro.distributionDomainName })
new CfnOutput(this, 'cfnDistroId', { value: cfnDistro.distributionId })
new CfnOutput(this, 'defaultApiGwUrl', { value: serverApigatewayProxy.apiEndpoint })
new CfnOutput(this, 'imagesApiGwUrl', { value: imageApigatewayProxy.apiEndpoint })
new CfnOutput(this, 'apiGwUrl', { value: apigatewayProxy.apiEndpoint })
new CfnOutput(this, 'assetsBucketUrl', { value: assetsBucket.bucketDomainName })
}
}
Expand Down
2 changes: 2 additions & 0 deletions lib/cli/pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const packHandler = async ({ handlerPath, outputFolder, publicFolder, sta
ignore: ['**/node_modules/**', '*.zip'],
},
{
// @TODO: use {dot:true} configuration in archiver and remove this.
// Ensure hidden files are included.
isGlob: true,
cwd: standaloneFolder,
Expand All @@ -96,6 +97,7 @@ export const packHandler = async ({ handlerPath, outputFolder, publicFolder, sta
name: 'handler.js',
},
{
// @TODO: Verify this as it seems like symlink is not needed when layer is in /opt/nodejs/node_modules
isFile: true,
path: symlinkPath,
name: 'node_modules',
Expand Down
6 changes: 2 additions & 4 deletions lib/standalone/image-handler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
// ! Make sure this comes before the fist import
process.env.NEXT_SHARP_PATH = require.resolve('sharp')
process.env.NODE_ENV = 'production'

import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from 'aws-lambda'
import { defaultConfig, NextConfigComplete } from 'next/dist/server/config-shared'
import { imageOptimizer as nextImageOptimizer, ImageOptimizerCache } from 'next/dist/server/image-optimizer'
Expand All @@ -24,6 +20,8 @@ const nextConfig = {
},
}

// We don't need serverless-http neither basePath configuration as endpoint works as single route API.
// Images are handled via header and query param information.
const optimizer = async (event: APIGatewayProxyEventV2): Promise<APIGatewayProxyStructuredResultV2> => {
try {
if (!sourceBucket) {
Expand Down
3 changes: 2 additions & 1 deletion lib/standalone/server-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ const server = slsHttp(
{
// We have separate function for handling images. Assets are handled by S3.
binary: false,
basePath: process.env.NEXTJS_LAMBDA_BASE_PATH
provider: 'aws',
basePath: process.env.NEXTJS_LAMBDA_BASE_PATH,
},
)

Expand Down
Loading

0 comments on commit f0beeec

Please sign in to comment.