|
| 1 | +#!/usr/bin/env node |
| 2 | +import 'source-map-support/register' |
| 3 | + |
| 4 | +import { HttpApi } from '@aws-cdk/aws-apigatewayv2-alpha' |
| 5 | +import { HttpLambdaIntegration } from '@aws-cdk/aws-apigatewayv2-integrations-alpha' |
| 6 | +import { App, CfnOutput, Duration, RemovalPolicy, Stack, StackProps, SymlinkFollowMode } from 'aws-cdk-lib' |
| 7 | +import { CloudFrontAllowedMethods, CloudFrontWebDistribution, OriginAccessIdentity } from 'aws-cdk-lib/aws-cloudfront' |
| 8 | +import { Function } from 'aws-cdk-lib/aws-lambda' |
| 9 | +import { Code, LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda' |
| 10 | +import { Bucket } from 'aws-cdk-lib/aws-s3' |
| 11 | +import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment' |
| 12 | + |
| 13 | +const app = new App() |
| 14 | + |
| 15 | +class NextStandaloneStack extends Stack { |
| 16 | + constructor(scope: App, id: string, props?: StackProps) { |
| 17 | + super(scope, id, props) |
| 18 | + |
| 19 | + const config = { |
| 20 | + assetsZipPath: './next.out/assetsLayer.zip', |
| 21 | + codeZipPath: './next.out/code.zip', |
| 22 | + dependenciesZipPath: './next.out/dependenciesLayer.zip', |
| 23 | + customServerHandler: 'handler.handler', |
| 24 | + customImageHandler: 'index.handler', |
| 25 | + cfnViewerCertificate: undefined, |
| 26 | + sharpLayerZipPath: './dist/sharp-layer.zip', |
| 27 | + nextLayerZipPath: './dist/next-layer.zip', |
| 28 | + imageHandlerZipPath: './dist/image-handler.zip', |
| 29 | + ...props, |
| 30 | + } |
| 31 | + |
| 32 | + const depsLayer = new LayerVersion(this, 'DepsLayer', { |
| 33 | + code: Code.fromAsset(config.dependenciesZipPath), |
| 34 | + }) |
| 35 | + |
| 36 | + const sharpLayer = new LayerVersion(this, 'SharpLayer', { |
| 37 | + code: Code.fromAsset(config.sharpLayerZipPath), |
| 38 | + }) |
| 39 | + |
| 40 | + const nextLayer = new LayerVersion(this, 'NextLayer', { |
| 41 | + code: Code.fromAsset(config.nextLayerZipPath), |
| 42 | + }) |
| 43 | + |
| 44 | + const serverLambda = new Function(this, 'DefaultNextJs', { |
| 45 | + code: Code.fromAsset(config.codeZipPath, { |
| 46 | + followSymlinks: SymlinkFollowMode.NEVER, |
| 47 | + }), |
| 48 | + runtime: Runtime.NODEJS_16_X, |
| 49 | + handler: config.customServerHandler, |
| 50 | + layers: [depsLayer, nextLayer], |
| 51 | + // No need for big memory as image handling is done elsewhere. |
| 52 | + memorySize: 512, |
| 53 | + timeout: Duration.seconds(15), |
| 54 | + }) |
| 55 | + |
| 56 | + const assetsBucket = new Bucket(this, 'NextAssetsBucket', { |
| 57 | + // Those settings are necessary for bucket to be removed on stack removal. |
| 58 | + removalPolicy: RemovalPolicy.DESTROY, |
| 59 | + autoDeleteObjects: true, |
| 60 | + publicReadAccess: false, |
| 61 | + }) |
| 62 | + |
| 63 | + const imageLambda = new Function(this, 'ImageOptimizationNextJs', { |
| 64 | + code: Code.fromAsset(config.imageHandlerZipPath), |
| 65 | + runtime: Runtime.NODEJS_16_X, |
| 66 | + handler: config.customImageHandler, |
| 67 | + layers: [sharpLayer, nextLayer], |
| 68 | + memorySize: 1024, |
| 69 | + timeout: Duration.seconds(10), |
| 70 | + environment: { |
| 71 | + S3_SOURCE_BUCKET: assetsBucket.bucketName, |
| 72 | + }, |
| 73 | + }) |
| 74 | + |
| 75 | + assetsBucket.grantRead(imageLambda) |
| 76 | + |
| 77 | + const serverApigatewayProxy = new HttpApi(this, 'ServerProxy', { |
| 78 | + createDefaultStage: true, |
| 79 | + defaultIntegration: new HttpLambdaIntegration('LambdaApigwIntegration', serverLambda), |
| 80 | + }) |
| 81 | + |
| 82 | + const imageApigatewayProxy = new HttpApi(this, 'ImagesProxy', { |
| 83 | + createDefaultStage: true, |
| 84 | + defaultIntegration: new HttpLambdaIntegration('ImagesApigwIntegration', imageLambda), |
| 85 | + }) |
| 86 | + |
| 87 | + const s3AssetsIdentity = new OriginAccessIdentity(this, 'OAICfnDistroS3', { |
| 88 | + comment: 'Allows CloudFront to access S3 bucket with assets', |
| 89 | + }) |
| 90 | + |
| 91 | + assetsBucket.grantRead(s3AssetsIdentity) |
| 92 | + |
| 93 | + const cfnDistro = new CloudFrontWebDistribution(this, 'TestApigwDistro', { |
| 94 | + // Must be set, because cloufront would use index.html which would not match in NextJS routes. |
| 95 | + defaultRootObject: '', |
| 96 | + comment: 'ApiGwLambda Proxy for NextJS', |
| 97 | + viewerCertificate: config.cfnViewerCertificate, |
| 98 | + originConfigs: [ |
| 99 | + { |
| 100 | + // Default behaviour, lambda handles. |
| 101 | + behaviors: [ |
| 102 | + { |
| 103 | + allowedMethods: CloudFrontAllowedMethods.ALL, |
| 104 | + isDefaultBehavior: true, |
| 105 | + forwardedValues: { queryString: true }, |
| 106 | + }, |
| 107 | + { |
| 108 | + allowedMethods: CloudFrontAllowedMethods.ALL, |
| 109 | + pathPattern: '_next/data/*', |
| 110 | + }, |
| 111 | + ], |
| 112 | + customOriginSource: { |
| 113 | + domainName: `${serverApigatewayProxy.apiId}.execute-api.${this.region}.amazonaws.com`, |
| 114 | + }, |
| 115 | + }, |
| 116 | + { |
| 117 | + // Our implementation of image optimization, we are tapping into Next's default route to avoid need for next.config.js changes. |
| 118 | + behaviors: [ |
| 119 | + { |
| 120 | + // Should use caching based on query params. |
| 121 | + allowedMethods: CloudFrontAllowedMethods.ALL, |
| 122 | + pathPattern: '_next/image*', |
| 123 | + forwardedValues: { queryString: true }, |
| 124 | + }, |
| 125 | + ], |
| 126 | + customOriginSource: { |
| 127 | + domainName: `${imageApigatewayProxy.apiId}.execute-api.${this.region}.amazonaws.com`, |
| 128 | + }, |
| 129 | + }, |
| 130 | + { |
| 131 | + // Remaining next files (safe-catch) and our assets that are not imported via `next/image` |
| 132 | + behaviors: [ |
| 133 | + { |
| 134 | + allowedMethods: CloudFrontAllowedMethods.GET_HEAD_OPTIONS, |
| 135 | + pathPattern: '_next/*', |
| 136 | + }, |
| 137 | + { |
| 138 | + allowedMethods: CloudFrontAllowedMethods.GET_HEAD_OPTIONS, |
| 139 | + pathPattern: 'assets/*', |
| 140 | + }, |
| 141 | + ], |
| 142 | + s3OriginSource: { |
| 143 | + s3BucketSource: assetsBucket, |
| 144 | + originAccessIdentity: s3AssetsIdentity, |
| 145 | + }, |
| 146 | + }, |
| 147 | + ], |
| 148 | + }) |
| 149 | + |
| 150 | + // This can be handled by `aws s3 sync` but we need to ensure invalidation of Cfn after deploy. |
| 151 | + new BucketDeployment(this, 'PublicFilesDeployment', { |
| 152 | + destinationBucket: assetsBucket, |
| 153 | + sources: [Source.asset('./next.out/assetsLayer.zip')], |
| 154 | + // Invalidate all paths after deployment. |
| 155 | + distribution: cfnDistro, |
| 156 | + distributionPaths: ['/*'], |
| 157 | + }) |
| 158 | + |
| 159 | + new CfnOutput(this, 'cfnDistroUrl', { value: cfnDistro.distributionDomainName }) |
| 160 | + new CfnOutput(this, 'cfnDistroId', { value: cfnDistro.distributionId }) |
| 161 | + new CfnOutput(this, 'defaultApiGwUrl', { value: serverApigatewayProxy.apiEndpoint }) |
| 162 | + new CfnOutput(this, 'imagesApiGwUrl', { value: imageApigatewayProxy.apiEndpoint }) |
| 163 | + new CfnOutput(this, 'assetsBucketUrl', { value: assetsBucket.bucketDomainName }) |
| 164 | + } |
| 165 | +} |
| 166 | + |
| 167 | +new NextStandaloneStack(app, 'StandaloneNextjsStack-Temporary') |
| 168 | + |
| 169 | +app.synth() |
0 commit comments