Skip to content

Commit

Permalink
ref(cdk): removed unnecessary symlinks, improved naming, cleaned depe…
Browse files Browse the repository at this point in the history
…ndencies
  • Loading branch information
Bender committed Oct 13, 2022
1 parent 33a0509 commit a61854e
Show file tree
Hide file tree
Showing 7 changed files with 350 additions and 133 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ node_modules/**
!.env.example
dist/**
.webpack
.vscode

# Temporary build folder.
nodejs/**
Expand All @@ -19,3 +20,5 @@ cdk.json
.yarn/**

**/cdk.out

examples/*
10 changes: 4 additions & 6 deletions lib/cdk-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import packageJson from '../package.json'

import { HttpApi } from '@aws-cdk/aws-apigatewayv2-alpha'
import { HttpLambdaIntegration } from '@aws-cdk/aws-apigatewayv2-integrations-alpha'
import { App, CfnOutput, Duration, RemovalPolicy, Stack, StackProps, SymlinkFollowMode } from 'aws-cdk-lib'
import { App, CfnOutput, Duration, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'
import { CloudFrontAllowedMethods, CloudFrontWebDistribution, OriginAccessIdentity } from 'aws-cdk-lib/aws-cloudfront'
import { Function } from 'aws-cdk-lib/aws-lambda'
import { Code, LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'
Expand Down Expand Up @@ -41,9 +41,7 @@ class NextStandaloneStack extends Stack {
})

const serverLambda = new Function(this, 'DefaultNextJs', {
code: Code.fromAsset(config.codeZipPath, {
followSymlinks: SymlinkFollowMode.NEVER,
}),
code: Code.fromAsset(config.codeZipPath),
runtime: Runtime.NODEJS_16_X,
handler: config.customServerHandler,
layers: [depsLayer],
Expand Down Expand Up @@ -92,10 +90,10 @@ class NextStandaloneStack extends Stack {

assetsBucket.grantRead(s3AssetsIdentity)

const cfnDistro = new CloudFrontWebDistribution(this, 'TestApigwDistro', {
const cfnDistro = new CloudFrontWebDistribution(this, 'NextCfnProxy', {
// Must be set, because cloufront would use index.html which would not match in NextJS routes.
defaultRootObject: '',
comment: 'ApiGwLambda Proxy for NextJS',
comment: 'Cloudfront for NextJS app',
viewerCertificate: config.cfnViewerCertificate,
originConfigs: [
{
Expand Down
16 changes: 2 additions & 14 deletions lib/cli/pack.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdirSync, rmSync, symlinkSync, writeFileSync } from 'fs'
import { mkdirSync, rmSync, writeFileSync } from 'fs'
import { tmpdir } from 'os'
import path from 'path'
import { nextServerConfigRegex } from '../consts'
Expand Down Expand Up @@ -28,7 +28,6 @@ export const packHandler = async ({ handlerPath, outputFolder, publicFolder, sta
// Dependencies layer configuration
const nodeModulesFolderPath = path.resolve(standaloneFolder, staticNames.nodeFolder)
const depsLambdaFolder = 'nodejs/node_modules'
const lambdaNodeModulesPath = path.resolve('/opt', depsLambdaFolder)
const dependenciesOutputPath = path.resolve(outputFolder, staticNames.dependenciesZip)

// Assets bundle configuration
Expand Down Expand Up @@ -74,18 +73,13 @@ export const packHandler = async ({ handlerPath, outputFolder, publicFolder, sta
],
})

// Create a symlink for node_modules so they point to the separately packaged layer.
// We need to create symlink because we are not using NodejsFunction in CDK as bundling is custom.
const tmpFolder = tmpdir()

const symlinkPath = path.resolve(tmpFolder, `./node_modules_${Math.random()}`)
symlinkSync(lambdaNodeModulesPath, symlinkPath)

const nextConfig = findInFile(generatedNextServerPath, nextServerConfigRegex)
const configPath = path.resolve(tmpFolder, `./config.json_${Math.random()}`)
writeFileSync(configPath, nextConfig, 'utf-8')

// Zip codebase including symlinked node_modules and handler.
// Zip codebase including handler.
await zipMultipleFoldersOrFiles({
outputName: codeOutputPath,
inputDefinition: [
Expand All @@ -106,12 +100,6 @@ export const packHandler = async ({ handlerPath, outputFolder, publicFolder, sta
path: handlerPath,
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',
},
{
isFile: true,
path: configPath,
Expand Down
50 changes: 49 additions & 1 deletion lib/standalone/image-handler.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,59 @@
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from 'aws-lambda'
import { IncomingMessage, ServerResponse } from 'http'
import { defaultConfig, NextConfigComplete } from 'next/dist/server/config-shared'
import { imageOptimizer as nextImageOptimizer, ImageOptimizerCache } from 'next/dist/server/image-optimizer'
import { NextUrlWithParsedQuery } from 'next/dist/server/request-meta'
import { ImageConfigComplete } from 'next/dist/shared/lib/image-config'
import { normalizeHeaders, requestHandler } from '../utils'
import { Readable } from 'stream'

const sourceBucket = process.env.S3_SOURCE_BUCKET ?? undefined

// Handle fetching of S3 object before optimization happens in nextjs.
const requestHandler =
(bucketName: string) =>
async (req: IncomingMessage, res: ServerResponse, url?: NextUrlWithParsedQuery): Promise<void> => {
if (!url) {
throw new Error('URL is missing from request.')
}

// S3 expects keys without leading `/`
const trimmedKey = url.href.startsWith('/') ? url.href.substring(1) : url.href

const client = new S3Client({})
const response = await client.send(new GetObjectCommand({ Bucket: bucketName, Key: trimmedKey }))

if (!response.Body) {
throw new Error(`Could not fetch image ${trimmedKey} from bucket.`)
}

const stream = response.Body as Readable

const data = await new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = []
stream.on('data', (chunk) => chunks.push(chunk))
stream.once('end', () => resolve(Buffer.concat(chunks)))
stream.once('error', reject)
})

res.statusCode = 200

if (response.ContentType) {
res.setHeader('Content-Type', response.ContentType)
}

if (response.CacheControl) {
res.setHeader('Cache-Control', response.CacheControl)
}

res.write(data)
res.end()
}

// Make header keys lowercase to ensure integrity.
const normalizeHeaders = (headers: Record<string, any>) =>
Object.entries(headers).reduce((acc, [key, value]) => ({ ...acc, [key.toLowerCase()]: value }), {} as Record<string, string>)

// @TODO: Allow passing params as env vars.
const nextConfig = {
...(defaultConfig as NextConfigComplete),
Expand Down
73 changes: 1 addition & 72 deletions lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,8 @@
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'
import archiver from 'archiver'
import { exec } from 'child_process'
import crypto from 'crypto'
import { closeSync, createWriteStream, existsSync, openSync, readdirSync, readFileSync, readSync, symlinkSync } from 'fs'
import { createWriteStream, existsSync, readdirSync, readFileSync, symlinkSync } from 'fs'
import glob, { IOptions as GlobOptions } from 'glob'
import { IncomingMessage, ServerResponse } from 'http'
import { NextUrlWithParsedQuery } from 'next/dist/server/request-meta'
import { replaceInFileSync } from 'replace-in-file'
import { Readable } from 'stream'

// Make header keys lowercase to ensure integrity.
export const normalizeHeaders = (headers: Record<string, any>) =>
Object.entries(headers).reduce((acc, [key, value]) => ({ ...acc, [key.toLowerCase()]: value }), {} as Record<string, string>)

// Handle fetching of S3 object before optimization happens in nextjs.
export const requestHandler =
(bucketName: string) =>
async (req: IncomingMessage, res: ServerResponse, url?: NextUrlWithParsedQuery): Promise<void> => {
if (!url) {
throw new Error('URL is missing from request.')
}

// S3 expects keys without leading `/`
const trimmedKey = url.href.startsWith('/') ? url.href.substring(1) : url.href

const client = new S3Client({})
const response = await client.send(new GetObjectCommand({ Bucket: bucketName, Key: trimmedKey }))

if (!response.Body) {
throw new Error(`Could not fetch image ${trimmedKey} from bucket.`)
}

const stream = response.Body as Readable

const data = await new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = []
stream.on('data', (chunk) => chunks.push(chunk))
stream.once('end', () => resolve(Buffer.concat(chunks)))
stream.once('error', reject)
})

res.statusCode = 200

if (response.ContentType) {
res.setHeader('Content-Type', response.ContentType)
}

if (response.CacheControl) {
res.setHeader('Cache-Control', response.CacheControl)
}

res.write(data)
res.end()
}

export enum BumpType {
Patch = 'patch',
Expand Down Expand Up @@ -260,27 +210,6 @@ interface SymlinkProps {

export const createSymlink = ({ linkLocation, sourcePath }: SymlinkProps) => symlinkSync(sourcePath, linkLocation)

const BUFFER_SIZE = 8192

export const md5FileSync = (path: string) => {
const fd = openSync(path, 'r')
const hash = crypto.createHash('md5')
const buffer = Buffer.alloc(BUFFER_SIZE)

try {
let bytesRead

do {
bytesRead = readSync(fd, buffer, 0, BUFFER_SIZE, null)
hash.update(buffer.subarray(0, bytesRead))
} while (bytesRead === BUFFER_SIZE)
} finally {
closeSync(fd)
}

return hash.digest('hex')
}

interface CommandProps {
cmd: string
path?: string
Expand Down
Loading

0 comments on commit a61854e

Please sign in to comment.