Skip to content
Open
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Creates AWS resources for NextJS application if they were not created. Bundles N
| profile | string | none | AWS profile to use for credentials. If parameter is empty going to read credentials from:<br>process.env.AWS_ACCESS_KEY_ID and process.env.AWS_SECRET_ACCESS_KEY |
| nodejs | string | 20 | Supports nodejs v18 and v20 |
| production | boolean | false | Identifies if you want to create production AWS resources. So they are going to have different delete policies to keep data in safe. |
| cloudFrontId | string | none | Existing cloud front distribution id. Useful for when new cloudfront distribution isn't needed |

## Architecture

Expand Down
2 changes: 2 additions & 0 deletions src/cdk/constructs/CheckExpirationLambdaEdge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as iam from 'aws-cdk-lib/aws-iam'
import path from 'node:path'
import { buildLambda } from '../../build/edge'
import { CacheConfig } from '../../types'
import { addOutput } from '../../common/cdk'

interface CheckExpirationLambdaEdgeProps extends cdk.StackProps {
bucketName: string
Expand Down Expand Up @@ -62,5 +63,6 @@ export class CheckExpirationLambdaEdge extends Construct {
})

this.lambdaEdge.addToRolePolicy(policyStatement)
addOutput(this, `${id}-CheckExpirationFunctionArn`, this.lambdaEdge.functionArn)
}
}
69 changes: 44 additions & 25 deletions src/cdk/constructs/CloudFrontDistribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,28 @@ interface CloudFrontPropsDistribution {
requestEdgeFunction: cloudfront.experimental.EdgeFunction
responseEdgeFunction: cloudfront.experimental.EdgeFunction
cacheConfig: CacheConfig
customCloudFrontId?: string
customCloudFrontDomainName?: string
}

const OneMonthCache = Duration.days(30)
const NoCache = Duration.seconds(0)
const defaultNextQueries = ['_rsc']
const defaultNextHeaders = ['Cache-Control']
export class CloudFrontDistribution extends Construct {
public readonly cf: cloudfront.Distribution
public readonly cf: cloudfront.IDistribution

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

const { staticBucket, requestEdgeFunction, responseEdgeFunction, cacheConfig } = props
const {
staticBucket,
requestEdgeFunction,
responseEdgeFunction,
cacheConfig,
customCloudFrontId,
customCloudFrontDomainName
} = props

const splitCachePolicy = new cloudfront.CachePolicy(this, 'SplitCachePolicy', {
cachePolicyName: `${id}-SplitCachePolicy`,
Expand Down Expand Up @@ -55,40 +64,50 @@ export class CloudFrontDistribution extends Construct {

const s3Origin = new origins.S3Origin(staticBucket)

this.cf = new cloudfront.Distribution(this, id, {
defaultBehavior: {
origin: s3Origin,
edgeLambdas: [
{
functionVersion: requestEdgeFunction.currentVersion,
eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST
},
{
functionVersion: responseEdgeFunction.currentVersion,
eventType: cloudfront.LambdaEdgeEventType.ORIGIN_RESPONSE
}
],
cachePolicy: splitCachePolicy
},
defaultRootObject: '',
additionalBehaviors: {
['/_next/data/*']: {
if (customCloudFrontId && customCloudFrontDomainName) {
this.cf = cloudfront.Distribution.fromDistributionAttributes(this, id, {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we also need to add new rules and lambda to existing cloudfront

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can call this.cf.addBehaviour

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, this isn't supported by the cloudformation stack. we can only add existing id to the cloud formation template (which is what I did here by setting it to the output).
aws/aws-cdk#12524

domainName: customCloudFrontId,
distributionId: customCloudFrontId
})
} else {
this.cf = new cloudfront.Distribution(this, id, {
defaultBehavior: {
origin: s3Origin,
edgeLambdas: [
{
functionVersion: requestEdgeFunction.currentVersion,
eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST
},
{
functionVersion: responseEdgeFunction.currentVersion,
eventType: cloudfront.LambdaEdgeEventType.ORIGIN_RESPONSE
}
],
cachePolicy: splitCachePolicy
},
'/_next/*': {
origin: s3Origin,
cachePolicy: longCachePolicy
defaultRootObject: '',
additionalBehaviors: {
['/_next/data/*']: {
origin: s3Origin,
edgeLambdas: [
{
functionVersion: requestEdgeFunction.currentVersion,
eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST
}
],
cachePolicy: splitCachePolicy
},
'/_next/*': {
origin: s3Origin,
cachePolicy: longCachePolicy
}
}
}
})
})
}

addOutput(this, `${id}-CloudfrontDistributionId`, this.cf.distributionId)
addOutput(this, `${id}-SplitCachePolicyId`, splitCachePolicy.cachePolicyId)
addOutput(this, `${id}-LongCachePolicyId`, longCachePolicy.cachePolicyId)
addOutput(this, `${id}-StaticBucketRegionalDomainName`, staticBucket.bucketRegionalDomainName)
}
}
2 changes: 2 additions & 0 deletions src/cdk/constructs/RoutingLambdaEdge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as iam from 'aws-cdk-lib/aws-iam'
import path from 'node:path'
import { buildLambda } from '../../build/edge'
import { CacheConfig } from '../../types'
import { addOutput } from '../../common/cdk'

interface RoutingLambdaEdgeProps extends cdk.StackProps {
bucketName: string
Expand Down Expand Up @@ -62,5 +63,6 @@ export class RoutingLambdaEdge extends Construct {
})

this.lambdaEdge.addToRolePolicy(policyStatement)
addOutput(this, `${id}-RoutingFunctionArn`, this.lambdaEdge.functionArn)
}
}
16 changes: 14 additions & 2 deletions src/cdk/stacks/NextCloudfrontStack.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Stack, type StackProps } from 'aws-cdk-lib'
import { Construct } from 'constructs'
import * as s3 from 'aws-cdk-lib/aws-s3'
import * as cloudfront from '@aws-sdk/client-cloudfront'
import { RoutingLambdaEdge } from '../constructs/RoutingLambdaEdge'
import { CloudFrontDistribution } from '../constructs/CloudFrontDistribution'
import { CacheConfig } from '../../types'
Expand All @@ -13,6 +14,7 @@ export interface NextCloudfrontStackProps extends StackProps {
ebAppDomain: string
buildOutputPath: string
cacheConfig: CacheConfig
customCloudFrontDistribution?: cloudfront.Distribution
}

export class NextCloudfrontStack extends Stack {
Expand All @@ -22,7 +24,15 @@ export class NextCloudfrontStack extends Stack {

constructor(scope: Construct, id: string, props: NextCloudfrontStackProps) {
super(scope, id, props)
const { nodejs, buildOutputPath, staticBucketName, ebAppDomain, region, cacheConfig } = props
const {
nodejs,
buildOutputPath,
staticBucketName,
ebAppDomain,
region,
cacheConfig,
customCloudFrontDistribution
} = props

this.routingLambdaEdge = new RoutingLambdaEdge(this, `${id}-RoutingLambdaEdge`, {
nodejs,
Expand Down Expand Up @@ -52,7 +62,9 @@ export class NextCloudfrontStack extends Stack {
ebAppDomain,
requestEdgeFunction: this.routingLambdaEdge.lambdaEdge,
responseEdgeFunction: this.checkExpLambdaEdge.lambdaEdge,
cacheConfig
cacheConfig,
customCloudFrontId: customCloudFrontDistribution?.Id,
customCloudFrontDomainName: customCloudFrontDistribution?.DomainName
})

staticBucket.grantRead(this.routingLambdaEdge.lambdaEdge)
Expand Down
37 changes: 33 additions & 4 deletions src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { ElasticBeanstalk } from '@aws-sdk/client-elastic-beanstalk'
import { S3 } from '@aws-sdk/client-s3'
import { CloudFront } from '@aws-sdk/client-cloudfront'
import { CloudFront, GetDistributionCommandOutput } from '@aws-sdk/client-cloudfront'
import fs from 'node:fs'
import childProcess from 'node:child_process'
import path from 'node:path'
import { buildApp, OUTPUT_FOLDER } from '../build/next'
import { NextRenderServerStack, type NextRenderServerStackProps } from '../cdk/stacks/NextRenderServerStack'
import { NextCloudfrontStack, type NextCloudfrontStackProps } from '../cdk/stacks/NextCloudfrontStack'
import { getAWSCredentials, uploadFolderToS3, uploadFileToS3, AWS_EDGE_REGION, emptyBucket } from '../common/aws'
import {
getAWSCredentials,
uploadFolderToS3,
uploadFileToS3,
AWS_EDGE_REGION,
emptyBucket,
updateDistribution,
getCloudFrontDistribution
} from '../common/aws'
import { AppStack } from '../common/cdk'
import { getProjectSettings } from '../common/project'
import loadConfig from './helpers/loadConfig'
Expand All @@ -21,6 +29,8 @@ export interface DeployConfig {
region?: string
profile?: string
}
cloudFrontId?: string
skipDefaultBehavior?: boolean
}

export interface DeployStackProps {
Expand Down Expand Up @@ -53,9 +63,10 @@ const createOutputFolder = () => {
export const deploy = async (config: DeployConfig) => {
let cleanNextApp
try {
const { siteName, stage = 'development', aws } = config
const { siteName, stage = 'development', aws, cloudFrontId, skipDefaultBehavior } = config
const credentials = await getAWSCredentials({ region: config.aws.region, profile: config.aws.profile })
const region = aws.region || process.env.REGION
let customCFDistribution: GetDistributionCommandOutput | undefined

if (!credentials.accessKeyId || !credentials.secretAccessKey) {
throw new Error('AWS Credentials are required.')
Expand Down Expand Up @@ -97,6 +108,10 @@ export const deploy = async (config: DeployConfig) => {
}
const siteNameLowerCased = siteName.toLowerCase()

if (cloudFrontId) {
customCFDistribution = await getCloudFrontDistribution(cloudfrontClient, cloudFrontId)
}

const nextRenderServerStack = new AppStack<NextRenderServerStack, NextRenderServerStackProps>(
`${siteNameLowerCased}-server`,
NextRenderServerStack,
Expand Down Expand Up @@ -132,7 +147,8 @@ export const deploy = async (config: DeployConfig) => {
cacheConfig,
env: {
region: AWS_EDGE_REGION // required since Edge can be deployed only here.
}
},
customCloudFrontDistribution: customCFDistribution?.Distribution
}
)
const nextCloudfrontStackOutput = await nextCloudfrontStack.deployStack()
Expand Down Expand Up @@ -189,6 +205,19 @@ export const deploy = async (config: DeployConfig) => {
VersionLabel: versionLabel
})

// if custom cf distribution, update it
if (customCFDistribution) {
await updateDistribution(cloudfrontClient, customCFDistribution, {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should do that inside stack, otherwise we will keep changing cloudfront with each deployment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as the above comment. I searched and there is no way to update the distribution inside the stack.
aws/aws-cdk#12524

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we check at least that those origins and rules were already added, so we can skip that step?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will create a function to check this then.

longCachePolicyId: nextCloudfrontStackOutput.LongCachePolicyId!,
splitCachePolicyId: nextCloudfrontStackOutput.SplitCachePolicyId!,
routingFunctionArn: nextCloudfrontStackOutput.RoutingFunctionArn!,
checkExpirationFunctionArn: nextCloudfrontStackOutput.CheckExpirationFunctionArn!,
staticBucketName: nextCloudfrontStackOutput.StaticBucketRegionalDomainName!,
addAdditionalBehaviour: true,
skipDefaultBehavior
})
}

await cloudfrontClient.createInvalidation({
DistributionId: nextCloudfrontStackOutput.CloudfrontDistributionId!,
InvalidationBatch: {
Expand Down
Loading