Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,205 changes: 1,205 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"@aws-sdk/client-cloudfront": "3.590.0",
"@aws-sdk/client-dynamodb": "3.709.0",
"@aws-sdk/client-elastic-beanstalk": "3.590.0",
"@aws-sdk/client-lambda": "3.787.0",
"@aws-sdk/client-s3": "3.591.0",
"@aws-sdk/client-secrets-manager": "3.758.0",
"@aws-sdk/client-sqs": "3.682.0",
Expand Down
37 changes: 29 additions & 8 deletions src/build/next.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import childProcess from 'node:child_process'
import fs from 'fs/promises'
import fsPromises from 'fs/promises'
import fs from 'node:fs'
import path from 'node:path'
import type { PrerenderManifest, RoutesManifest } from 'next/dist/build'
import { type ProjectPackager, type ProjectSettings } from '../common/project'
Expand All @@ -16,6 +17,22 @@ interface BuildAppOptions {

export const OUTPUT_FOLDER = 'serverless-next'

export const cleanOutputFolder = () => {
const outputFolderPath = path.join(process.cwd(), OUTPUT_FOLDER)

fs.rmSync(outputFolderPath, { recursive: true, force: true })
}

export const createOutputFolder = () => {
const outputFolderPath = path.join(process.cwd(), OUTPUT_FOLDER)
// clean folder before creating new build output.
cleanOutputFolder()

fs.mkdirSync(outputFolderPath)

return outputFolderPath
}

const setNextEnvs = () => {
process.env.NEXT_SERVERLESS_DEPLOYING_PHASE = 'true'
}
Expand All @@ -29,19 +46,23 @@ export const buildNext = async (options: BuildOptions) => {

const copyAssets = async (outputPath: string, appPath: string, appRelativePath: string) => {
// Copying static assets (like js, css, images, .etc)
await fs.cp(path.join(appPath, '.next'), path.join(outputPath, '.next'), {
await fsPromises.cp(path.join(appPath, '.next'), path.join(outputPath, '.next'), {
recursive: true
})
await fs.cp(
await fsPromises.cp(
path.join(appPath, '.next', 'static'),
path.join(outputPath, '.next', 'standalone', appRelativePath, '.next', 'static'),
{
recursive: true
}
)
await fs.cp(path.join(appPath, 'public'), path.join(outputPath, '.next', 'standalone', appRelativePath, 'public'), {
recursive: true
})
await fsPromises.cp(
path.join(appPath, 'public'),
path.join(outputPath, '.next', 'standalone', appRelativePath, 'public'),
{
recursive: true
}
)
}

const getRewritesConfig = (manifestRules: RoutesManifest['rewrites']): NextRewrites => {
Expand Down Expand Up @@ -86,11 +107,11 @@ export const getNextCachedRoutesConfig = async (
outputPath: string,
appRelativePath: string
): Promise<{ cachedRoutesMatchers: string[]; rewritesConfig: NextRewrites; redirectsConfig: NextRedirects }> => {
const prerenderManifestJSON = await fs.readFile(
const prerenderManifestJSON = await fsPromises.readFile(
path.join(outputPath, '.next', 'standalone', appRelativePath, '.next', 'prerender-manifest.json'),
'utf-8'
)
const routesManifestJSON = await fs.readFile(
const routesManifestJSON = await fsPromises.readFile(
path.join(outputPath, '.next', 'standalone', appRelativePath, '.next', 'routes-manifest.json'),
'utf-8'
)
Expand Down
24 changes: 6 additions & 18 deletions src/cdk/constructs/OriginRequestLambdaEdge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,12 @@ import * as logs from 'aws-cdk-lib/aws-logs'
import * as iam from 'aws-cdk-lib/aws-iam'
import path from 'node:path'
import { buildLambda } from '../../common/esbuild'
import { CacheConfig } from '../../types'
import { addOutput } from '../../common/cdk'

interface OriginRequestLambdaEdgeProps extends cdk.StackProps {
bucketName: string
renderServerDomain: string
buildOutputPath: string
nodejs?: string
cacheConfig: CacheConfig
bucketRegion?: string
cachedRoutesMatchers: string[]
}

const NodeJSEnvironmentMapping: Record<string, lambda.Runtime> = {
Expand All @@ -27,22 +23,12 @@ export class OriginRequestLambdaEdge extends Construct {
public readonly lambdaEdge: cloudfront.experimental.EdgeFunction

constructor(scope: Construct, id: string, props: OriginRequestLambdaEdgeProps) {
const { bucketName, bucketRegion, renderServerDomain, nodejs, buildOutputPath, cacheConfig, cachedRoutesMatchers } =
props
const { bucketName, nodejs, buildOutputPath } = props
super(scope, id)

const nodeJSEnvironment = NodeJSEnvironmentMapping[nodejs ?? ''] ?? NodeJSEnvironmentMapping['20']
const name = 'originRequest'

buildLambda(name, buildOutputPath, {
define: {
'process.env.S3_BUCKET': JSON.stringify(bucketName),
'process.env.S3_BUCKET_REGION': JSON.stringify(bucketRegion ?? ''),
'process.env.EB_APP_URL': JSON.stringify(renderServerDomain),
'process.env.CACHE_CONFIG': JSON.stringify(cacheConfig),
'process.env.NEXT_CACHED_ROUTES_MATCHERS': JSON.stringify(cachedRoutesMatchers ?? [])
}
})
buildLambda('defaultLambdaMock', buildOutputPath)

const logGroup = new logs.LogGroup(this, 'OriginRequestLambdaEdgeLogGroup', {
logGroupName: `/aws/lambda/${id}-originRequest`,
Expand All @@ -52,7 +38,7 @@ export class OriginRequestLambdaEdge extends Construct {

this.lambdaEdge = new cloudfront.experimental.EdgeFunction(this, 'OriginRequestLambdaEdge', {
runtime: nodeJSEnvironment,
code: lambda.Code.fromAsset(path.join(buildOutputPath, 'server-functions', name)),
code: lambda.Code.fromAsset(path.join(buildOutputPath, 'server-functions', 'defaultLambdaMock')),
handler: 'index.handler',
logGroup
})
Expand All @@ -65,5 +51,7 @@ export class OriginRequestLambdaEdge extends Construct {
})

this.lambdaEdge.addToRolePolicy(policyStatement)

addOutput(this, `${id}-OriginRequestLambdaEdgeName`, this.lambdaEdge.functionName)
}
}
6 changes: 3 additions & 3 deletions src/cdk/constructs/RenderServerDistribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export class RenderServerDistribution extends Construct {
applicationName: `${id}-eb-app`
})

this.ebInstanceProfileRole = new iam.Role(this, 'EbInstanceProfileRole', {
this.ebInstanceProfileRole = new iam.Role(this, 'ProfileRole', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AWSElasticBeanstalkWebTier')]
})
Expand All @@ -98,7 +98,7 @@ export class RenderServerDistribution extends Construct {
})
)

this.ebInstanceProfile = new iam.CfnInstanceProfile(this, 'EbInstanceProfile', {
this.ebInstanceProfile = new iam.CfnInstanceProfile(this, 'Profile', {
roles: [this.ebInstanceProfileRole.roleName]
})

Expand All @@ -111,7 +111,7 @@ export class RenderServerDistribution extends Construct {
const privateSubnets = this.vpc.selectSubnets({ subnetType: SubnetType.PRIVATE_WITH_EGRESS }).subnetIds

this.ebEnv = new elasticbeanstalk.CfnEnvironment(this, 'EbEnv', {
environmentName: `${appName}-eb-env`,
environmentName: `${appName}-env`,
applicationName: this.ebApp.applicationName!,
solutionStackName: nodeJSEnvironment,
optionSettings: [
Expand Down
6 changes: 3 additions & 3 deletions src/cdk/constructs/RenderWorkerDistribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export class RenderWorkerDistribution extends Construct {
* Create IAM role for EC2 instances
* Includes required permissions for Elastic Beanstalk Worker tier
*/
this.instanceRole = new iam.Role(this, 'WorkerInstanceRole', {
this.instanceRole = new iam.Role(this, 'InstanceRole', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AWSElasticBeanstalkWorkerTier')]
})
Expand Down Expand Up @@ -187,7 +187,7 @@ export class RenderWorkerDistribution extends Construct {
/**
* Create Instance Profile for EC2 instances
*/
this.instanceProfile = new iam.CfnInstanceProfile(this, 'WorkerInstanceProfile', {
this.instanceProfile = new iam.CfnInstanceProfile(this, 'Profile', {
roles: [this.instanceRole.roleName]
})

Expand All @@ -210,7 +210,7 @@ export class RenderWorkerDistribution extends Construct {
* Create Elastic Beanstalk environment with worker configuration
*/
this.environment = new elasticbeanstalk.CfnEnvironment(this, 'WorkerEnvironment', {
environmentName: `${appName}-worker-env`,
environmentName: appName,
applicationName: this.application.applicationName!,
solutionStackName: NODE_VERSIONS[nodejs],
tier: {
Expand Down
5 changes: 3 additions & 2 deletions src/cdk/constructs/RevalidateLambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface RevalidateLambdaProps extends cdk.StackProps {
nodejs?: string
sqsRegion: string
sqsQueueUrl: string
secretName: string
}

const NodeJSEnvironmentMapping: Record<string, lambda.Runtime> = {
Expand All @@ -24,7 +25,7 @@ export class RevalidateLambda extends Construct {
public readonly lambdaHttpUrl: lambda.FunctionUrl

constructor(scope: Construct, id: string, props: RevalidateLambdaProps) {
const { nodejs, buildOutputPath, sqsRegion, sqsQueueUrl } = props
const { nodejs, buildOutputPath, sqsRegion, sqsQueueUrl, secretName } = props
super(scope, id)

const nodeJSEnvironment = NodeJSEnvironmentMapping[nodejs ?? ''] ?? NodeJSEnvironmentMapping['20']
Expand All @@ -45,7 +46,7 @@ export class RevalidateLambda extends Construct {
logGroup,
environment: {
SQS_AWS_REGION: sqsRegion,
SECRET_ID: 'x-api-key',
SECRET_ID: secretName,
SQS_QUEUE_URL: sqsQueueUrl
}
})
Expand Down
10 changes: 8 additions & 2 deletions src/cdk/constructs/SecretManagerDistribution.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { Construct } from 'constructs'
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'

interface SecretManagerDistributionProps {
secretName: string
}

export class SecretManagerDistribution extends Construct {
public readonly xApiKey: secretsmanager.Secret

constructor(scope: Construct, id: string) {
constructor(scope: Construct, id: string, props: SecretManagerDistributionProps) {
super(scope, id)

const { secretName } = props

this.xApiKey = new secretsmanager.Secret(this, 'XApiKey', {
secretName: 'x-api-key'
secretName: secretName
})
}
}
22 changes: 6 additions & 16 deletions src/cdk/constructs/ViewerRequestLambdaEdge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,11 @@ import * as logs from 'aws-cdk-lib/aws-logs'
import * as iam from 'aws-cdk-lib/aws-iam'
import path from 'node:path'
import { buildLambda } from '../../common/esbuild'
import { NextRedirects, NextI18nConfig, NextRewrites } from '../../types'
import { addOutput } from '../../common/cdk'

interface ViewerRequestLambdaEdgeProps extends cdk.StackProps {
buildOutputPath: string
nodejs?: string
redirectsConfig?: NextRedirects
nextI18nConfig?: NextI18nConfig
isTrailingSlashEnabled: boolean
rewritesConfig: NextRewrites
}

const NodeJSEnvironmentMapping: Record<string, lambda.Runtime> = {
Expand All @@ -26,20 +22,12 @@ export class ViewerRequestLambdaEdge extends Construct {
public readonly lambdaEdge: cloudfront.experimental.EdgeFunction

constructor(scope: Construct, id: string, props: ViewerRequestLambdaEdgeProps) {
const { nodejs, buildOutputPath, redirectsConfig, nextI18nConfig, isTrailingSlashEnabled, rewritesConfig } = props
const { nodejs, buildOutputPath } = props
super(scope, id)

const nodeJSEnvironment = NodeJSEnvironmentMapping[nodejs ?? ''] ?? NodeJSEnvironmentMapping['20']
const name = 'viewerRequest'

buildLambda(name, buildOutputPath, {
define: {
'process.env.REDIRECTS': JSON.stringify(redirectsConfig ?? []),
'process.env.LOCALES_CONFIG': JSON.stringify(nextI18nConfig ?? null),
'process.env.IS_TRAILING_SLASH_ENABLED': JSON.stringify(isTrailingSlashEnabled),
'process.env.NEXT_REWRITES_CONFIG': JSON.stringify(rewritesConfig ?? [])
}
})
buildLambda('defaultLambdaMock', buildOutputPath)

const logGroup = new logs.LogGroup(this, 'ViewerRequestLambdaEdgeLogGroup', {
logGroupName: `/aws/lambda/${id}-viewerRequest`,
Expand All @@ -49,7 +37,7 @@ export class ViewerRequestLambdaEdge extends Construct {

this.lambdaEdge = new cloudfront.experimental.EdgeFunction(this, 'ViewerRequestLambdaEdge', {
runtime: nodeJSEnvironment,
code: lambda.Code.fromAsset(path.join(buildOutputPath, 'server-functions', name)),
code: lambda.Code.fromAsset(path.join(buildOutputPath, 'server-functions', 'defaultLambdaMock')),
handler: 'index.handler',
logGroup
})
Expand All @@ -62,5 +50,7 @@ export class ViewerRequestLambdaEdge extends Construct {
})

this.lambdaEdge.addToRolePolicy(policyStatement)

addOutput(this, `${id}-ViewerRequestLambdaEdgeName`, this.lambdaEdge.functionName)
}
}
37 changes: 13 additions & 24 deletions src/cdk/stacks/NextCloudfrontStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ViewerResponseLambdaEdge } from '../constructs/ViewerResponseLambdaEdge
import { ViewerRequestLambdaEdge } from '../constructs/ViewerRequestLambdaEdge'
import { RevalidateLambda } from '../constructs/RevalidateLambda'
import { SecretManagerDistribution } from '../constructs/SecretManagerDistribution'
import { DeployConfig, NextRedirects, NextI18nConfig, NextRewrites } from '../../types'
import { DeployConfig } from '../../types'
import * as iam from 'aws-cdk-lib/aws-iam'

export interface NextCloudfrontStackProps extends StackProps {
Expand All @@ -18,13 +18,9 @@ export interface NextCloudfrontStackProps extends StackProps {
buildOutputPath: string
deployConfig: DeployConfig
imageTTL?: number
redirectsConfig?: NextRedirects
nextI18nConfig?: NextI18nConfig
cachedRoutesMatchers: string[]
rewritesConfig: NextRewrites
isTrailingSlashEnabled: boolean
sqsQueueUrl: string
sqsQueueArn: string
appName: string
}

export class NextCloudfrontStack extends Stack {
Expand All @@ -43,32 +39,22 @@ export class NextCloudfrontStack extends Stack {
region,
deployConfig,
imageTTL,
redirectsConfig,
cachedRoutesMatchers,
nextI18nConfig,
rewritesConfig,
isTrailingSlashEnabled,
sqsQueueUrl,
sqsQueueArn
sqsQueueArn,
appName
} = props

const secretName = `${appName}-x-api-key`

this.originRequestLambdaEdge = new OriginRequestLambdaEdge(this, `${id}-OriginRequestLambdaEdge`, {
nodejs,
bucketName: staticBucketName,
renderServerDomain,
buildOutputPath,
cacheConfig: deployConfig.cache,
bucketRegion: region,
cachedRoutesMatchers
buildOutputPath
})

this.viewerRequestLambdaEdge = new ViewerRequestLambdaEdge(this, `${id}-ViewerRequestLambdaEdge`, {
buildOutputPath,
nodejs,
redirectsConfig,
rewritesConfig,
nextI18nConfig,
isTrailingSlashEnabled
nodejs
})

this.viewerResponseLambdaEdge = new ViewerResponseLambdaEdge(this, `${id}-ViewerResponseLambdaEdge`, {
Expand All @@ -80,15 +66,18 @@ export class NextCloudfrontStack extends Stack {
nodejs,
buildOutputPath,
sqsRegion: region,
sqsQueueUrl
sqsQueueUrl,
secretName
})

const staticBucket = s3.Bucket.fromBucketAttributes(this, `${id}-StaticAssetsBucket`, {
bucketName: staticBucketName,
region
})

const secretManager = new SecretManagerDistribution(this, `${id}-SecretManagerDistribution`)
const secretManager = new SecretManagerDistribution(this, `${id}-SecretManagerDistribution`, {
secretName
})

secretManager.xApiKey.grantRead(this.revalidateLambda.lambda)

Expand Down
Loading