From fb8b46e5e73c4c31dab22ceff4729a0947423137 Mon Sep 17 00:00:00 2001 From: oscar Date: Wed, 14 Aug 2024 10:57:28 +0100 Subject: [PATCH 1/8] started working on updating existing cloudfront distribution --- .../constructs/CheckExpirationLambdaEdge.ts | 2 + src/cdk/constructs/CloudFrontDistribution.ts | 2 + src/cdk/constructs/RoutingLambdaEdge.ts | 2 + src/common/aws.ts | 139 ++++++++++++++++++ 4 files changed, 145 insertions(+) diff --git a/src/cdk/constructs/CheckExpirationLambdaEdge.ts b/src/cdk/constructs/CheckExpirationLambdaEdge.ts index 1197598..62f810d 100644 --- a/src/cdk/constructs/CheckExpirationLambdaEdge.ts +++ b/src/cdk/constructs/CheckExpirationLambdaEdge.ts @@ -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 @@ -62,5 +63,6 @@ export class CheckExpirationLambdaEdge extends Construct { }) this.lambdaEdge.addToRolePolicy(policyStatement) + addOutput(this, `${id}-CheckExpirationFunctionArn`, this.lambdaEdge.functionArn) } } diff --git a/src/cdk/constructs/CloudFrontDistribution.ts b/src/cdk/constructs/CloudFrontDistribution.ts index af46b40..3471125 100644 --- a/src/cdk/constructs/CloudFrontDistribution.ts +++ b/src/cdk/constructs/CloudFrontDistribution.ts @@ -90,5 +90,7 @@ export class CloudFrontDistribution extends Construct { }) addOutput(this, `${id}-CloudfrontDistributionId`, this.cf.distributionId) + addOutput(this, `${id}-SplitCachePolicyId`, splitCachePolicy.cachePolicyId) + addOutput(this, `${id}-LongCachePolicyId`, splitCachePolicy.cachePolicyId) } } diff --git a/src/cdk/constructs/RoutingLambdaEdge.ts b/src/cdk/constructs/RoutingLambdaEdge.ts index e3a0015..46d91fb 100644 --- a/src/cdk/constructs/RoutingLambdaEdge.ts +++ b/src/cdk/constructs/RoutingLambdaEdge.ts @@ -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 @@ -62,5 +63,6 @@ export class RoutingLambdaEdge extends Construct { }) this.lambdaEdge.addToRolePolicy(policyStatement) + addOutput(this, `${id}-RoutingFunctionArn`, this.lambdaEdge.functionArn) } } diff --git a/src/common/aws.ts b/src/common/aws.ts index 2ff2aee..dcca375 100644 --- a/src/common/aws.ts +++ b/src/common/aws.ts @@ -6,6 +6,13 @@ import * as AWS from 'aws-sdk' import { partition } from '@aws-sdk/util-endpoints' import fs from 'node:fs' import path from 'node:path' +import { + CloudFront, + UpdateDistributionCommand, + GetDistributionCommand, + CacheBehavior +} from '@aws-sdk/client-cloudfront' +import { LambdaEdgeEventType } from 'aws-cdk-lib/aws-cloudfront' type GetAWSBasicProps = | { @@ -153,3 +160,135 @@ export const getCDKAssetsPublisher = ( ) => { return new AssetPublishing(AssetManifest.fromFile(manifestPath), { aws: new AWSClient(region, profile) }) } + +export const getCloudFrontDistribution = async (cfClient: CloudFront, distributionId: string) => { + const command = new GetDistributionCommand({ Id: distributionId }) + const response = await cfClient.send(command) + return response +} + +export const updateDistribution = async ( + cfClient: CloudFront, + distributionId: string, + config: { + staticBucketName?: string + longCachePolicyId?: string + splitCachePolicyId?: string + routingFunctionArn?: string + checkExpirationFunctionArn?: string + addAdditionalBehaviour?: boolean + } +) => { + const { + staticBucketName, + routingFunctionArn, + splitCachePolicyId, + addAdditionalBehaviour, + longCachePolicyId, + checkExpirationFunctionArn + } = config + const { Distribution, ETag } = await getCloudFrontDistribution(cfClient, distributionId) + if (Distribution && Distribution.DistributionConfig) { + if (staticBucketName && Distribution.DistributionConfig.Origins && Distribution.DistributionConfig.Origins.Items) { + const updatedOrigins = Distribution.DistributionConfig.Origins.Items?.map((origin) => { + if (origin.Id === Distribution.DistributionConfig?.DefaultCacheBehavior?.TargetOriginId) { + return { + ...origin, + DomainName: staticBucketName, + CustomOriginConfig: undefined // Remove any custom origin settings + } + } + return origin + }) + + // Update the Origins with the modified origin + Distribution.DistributionConfig.Origins.Items = updatedOrigins + } + + if (addAdditionalBehaviour) { + const behaviours: CacheBehavior[] = [ + { + PathPattern: '/_next/data/*', + TargetOriginId: undefined, + ViewerProtocolPolicy: 'allow-all', + LambdaFunctionAssociations: { + Quantity: 1, + Items: [ + { + EventType: LambdaEdgeEventType.ORIGIN_REQUEST, + LambdaFunctionARN: routingFunctionArn + } + ] + }, + CachePolicyId: splitCachePolicyId + }, + { + PathPattern: '/_next/*', + TargetOriginId: undefined, + ViewerProtocolPolicy: 'allow-all', + LambdaFunctionAssociations: { + Quantity: 1, + Items: [ + { + EventType: LambdaEdgeEventType.ORIGIN_REQUEST, + LambdaFunctionARN: routingFunctionArn + } + ] + }, + CachePolicyId: longCachePolicyId + } + ] + const updatedBehaviors = behaviours.map((behaviour) => { + const oldBehavior = (Distribution.DistributionConfig?.CacheBehaviors?.Items || []).find( + (b) => b.PathPattern === behaviour.PathPattern + ) + if (oldBehavior) { + return { + ...oldBehavior, + ...behaviour + } + } + return behaviour + }) + + const mergedBehaviours = (Distribution.DistributionConfig?.CacheBehaviors?.Items || []) + .filter((a) => !updatedBehaviors.find((b) => a.PathPattern === b.PathPattern)) + .concat(updatedBehaviors) + + Distribution.DistributionConfig.CacheBehaviors = { + Items: mergedBehaviours, + Quantity: mergedBehaviours.length + } + } + + Distribution.DistributionConfig.DefaultCacheBehavior = { + LambdaFunctionAssociations: { + Quantity: 2, + Items: [ + { + EventType: LambdaEdgeEventType.ORIGIN_REQUEST, + LambdaFunctionARN: routingFunctionArn + }, + { + EventType: LambdaEdgeEventType.ORIGIN_RESPONSE, + LambdaFunctionARN: checkExpirationFunctionArn + } + ] + }, + TargetOriginId: undefined, + ViewerProtocolPolicy: 'allow-all' + } + } + + // Update the distribution with the modified config + const updateParams = { + Id: distributionId, + IfMatch: ETag, // Required for updating the distribution + DistributionConfig: Distribution?.DistributionConfig + } + const command = new UpdateDistributionCommand(updateParams) + const updateResponse = await cfClient.send(command) + + console.log('Distribution updated successfully:', updateResponse) + return updateResponse +} From c8971dae9e2e37a155b6f94be7183e64ca864b5f Mon Sep 17 00:00:00 2001 From: oscar Date: Fri, 16 Aug 2024 00:51:15 +0100 Subject: [PATCH 2/8] refactored update distribution function --- src/common/aws.ts | 105 +++++++++++++++++++++++++++++----------------- 1 file changed, 67 insertions(+), 38 deletions(-) diff --git a/src/common/aws.ts b/src/common/aws.ts index dcca375..2e20e84 100644 --- a/src/common/aws.ts +++ b/src/common/aws.ts @@ -10,7 +10,8 @@ import { CloudFront, UpdateDistributionCommand, GetDistributionCommand, - CacheBehavior + CacheBehavior, + GetDistributionCommandOutput } from '@aws-sdk/client-cloudfront' import { LambdaEdgeEventType } from 'aws-cdk-lib/aws-cloudfront' @@ -161,6 +162,47 @@ export const getCDKAssetsPublisher = ( return new AssetPublishing(AssetManifest.fromFile(manifestPath), { aws: new AWSClient(region, profile) }) } +const behaviorMapper = (config: { + targetOriginId?: string + functionArn?: string + cachePolicyId: string + pathPattern?: string +}): CacheBehavior => { + const { pathPattern, targetOriginId, functionArn, cachePolicyId } = config + + return { + PathPattern: pathPattern, + TargetOriginId: targetOriginId, + ViewerProtocolPolicy: 'allow-all', + LambdaFunctionAssociations: functionArn + ? { + Quantity: 1, + Items: [ + { + EventType: LambdaEdgeEventType.ORIGIN_REQUEST, + LambdaFunctionARN: functionArn + } + ] + } + : { + Quantity: 0, + Items: [] + }, + CachePolicyId: cachePolicyId, + SmoothStreaming: false, + Compress: true, + FieldLevelEncryptionId: '', + AllowedMethods: { + Quantity: 2, + Items: ['GET', 'HEAD'], + CachedMethods: { + Quantity: 2, + Items: ['GET', 'HEAD'] + } + } + } +} + export const getCloudFrontDistribution = async (cfClient: CloudFront, distributionId: string) => { const command = new GetDistributionCommand({ Id: distributionId }) const response = await cfClient.send(command) @@ -169,7 +211,7 @@ export const getCloudFrontDistribution = async (cfClient: CloudFront, distributi export const updateDistribution = async ( cfClient: CloudFront, - distributionId: string, + distribution: GetDistributionCommandOutput, config: { staticBucketName?: string longCachePolicyId?: string @@ -187,11 +229,12 @@ export const updateDistribution = async ( longCachePolicyId, checkExpirationFunctionArn } = config - const { Distribution, ETag } = await getCloudFrontDistribution(cfClient, distributionId) + const { Distribution, ETag } = distribution if (Distribution && Distribution.DistributionConfig) { + const targetOriginId = Distribution.DistributionConfig?.DefaultCacheBehavior?.TargetOriginId if (staticBucketName && Distribution.DistributionConfig.Origins && Distribution.DistributionConfig.Origins.Items) { const updatedOrigins = Distribution.DistributionConfig.Origins.Items?.map((origin) => { - if (origin.Id === Distribution.DistributionConfig?.DefaultCacheBehavior?.TargetOriginId) { + if (origin.Id === targetOriginId) { return { ...origin, DomainName: staticBucketName, @@ -207,36 +250,17 @@ export const updateDistribution = async ( if (addAdditionalBehaviour) { const behaviours: CacheBehavior[] = [ - { - PathPattern: '/_next/data/*', - TargetOriginId: undefined, - ViewerProtocolPolicy: 'allow-all', - LambdaFunctionAssociations: { - Quantity: 1, - Items: [ - { - EventType: LambdaEdgeEventType.ORIGIN_REQUEST, - LambdaFunctionARN: routingFunctionArn - } - ] - }, - CachePolicyId: splitCachePolicyId - }, - { - PathPattern: '/_next/*', - TargetOriginId: undefined, - ViewerProtocolPolicy: 'allow-all', - LambdaFunctionAssociations: { - Quantity: 1, - Items: [ - { - EventType: LambdaEdgeEventType.ORIGIN_REQUEST, - LambdaFunctionARN: routingFunctionArn - } - ] - }, - CachePolicyId: longCachePolicyId - } + behaviorMapper({ + pathPattern: '/_next/data/*', + targetOriginId, + cachePolicyId: splitCachePolicyId!, + functionArn: routingFunctionArn + }), + behaviorMapper({ + pathPattern: '/_next/*', + targetOriginId, + cachePolicyId: longCachePolicyId! + }) ] const updatedBehaviors = behaviours.map((behaviour) => { const oldBehavior = (Distribution.DistributionConfig?.CacheBehaviors?.Items || []).find( @@ -261,7 +285,10 @@ export const updateDistribution = async ( } } + const defBeh = Distribution.DistributionConfig.DefaultCacheBehavior + Distribution.DistributionConfig.DefaultCacheBehavior = { + ...defBeh, LambdaFunctionAssociations: { Quantity: 2, Items: [ @@ -275,20 +302,22 @@ export const updateDistribution = async ( } ] }, - TargetOriginId: undefined, - ViewerProtocolPolicy: 'allow-all' + TargetOriginId: targetOriginId, + ViewerProtocolPolicy: 'allow-all', + SmoothStreaming: false, + Compress: true, + CachePolicyId: splitCachePolicyId } } // Update the distribution with the modified config const updateParams = { - Id: distributionId, + Id: Distribution?.Id, IfMatch: ETag, // Required for updating the distribution DistributionConfig: Distribution?.DistributionConfig } const command = new UpdateDistributionCommand(updateParams) const updateResponse = await cfClient.send(command) - console.log('Distribution updated successfully:', updateResponse) return updateResponse } From 8202721d09411e958a274c3d5cecb1ff0a976d7b Mon Sep 17 00:00:00 2001 From: oscar Date: Fri, 16 Aug 2024 00:52:32 +0100 Subject: [PATCH 3/8] pass custom cf distribution to cloudfront stack to avoid new cloudfront creation --- src/cdk/constructs/CloudFrontDistribution.ts | 69 ++++++++++++-------- src/cdk/stacks/NextCloudfrontStack.ts | 16 ++++- 2 files changed, 57 insertions(+), 28 deletions(-) diff --git a/src/cdk/constructs/CloudFrontDistribution.ts b/src/cdk/constructs/CloudFrontDistribution.ts index 3471125..bd8bc40 100644 --- a/src/cdk/constructs/CloudFrontDistribution.ts +++ b/src/cdk/constructs/CloudFrontDistribution.ts @@ -13,6 +13,8 @@ interface CloudFrontPropsDistribution { requestEdgeFunction: cloudfront.experimental.EdgeFunction responseEdgeFunction: cloudfront.experimental.EdgeFunction cacheConfig: CacheConfig + customCloudFrontId?: string + customCloudFrontDomainName?: string } const OneMonthCache = Duration.days(30) @@ -20,12 +22,19 @@ 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`, @@ -55,42 +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, { + 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`, splitCachePolicy.cachePolicyId) + addOutput(this, `${id}-LongCachePolicyId`, longCachePolicy.cachePolicyId) + addOutput(this, `${id}-StaticBucketRegionalDomainName`, staticBucket.bucketRegionalDomainName) } } diff --git a/src/cdk/stacks/NextCloudfrontStack.ts b/src/cdk/stacks/NextCloudfrontStack.ts index 444173d..44546b9 100644 --- a/src/cdk/stacks/NextCloudfrontStack.ts +++ b/src/cdk/stacks/NextCloudfrontStack.ts @@ -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' @@ -13,6 +14,7 @@ export interface NextCloudfrontStackProps extends StackProps { ebAppDomain: string buildOutputPath: string cacheConfig: CacheConfig + customCloudFrontDistribution?: cloudfront.Distribution } export class NextCloudfrontStack extends Stack { @@ -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, @@ -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) From ceb644b2919e0d349def172f86b837f430e91c71 Mon Sep 17 00:00:00 2001 From: oscar Date: Fri, 16 Aug 2024 00:53:32 +0100 Subject: [PATCH 4/8] update custom distribution if exists --- src/commands/deploy.ts | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 6296f31..08d2144 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -1,13 +1,20 @@ 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 } from '../common/aws' +import { + getAWSCredentials, + uploadFolderToS3, + uploadFileToS3, + AWS_EDGE_REGION, + updateDistribution, + getCloudFrontDistribution +} from '../common/aws' import { AppStack } from '../common/cdk' import { getProjectSettings } from '../common/project' import loadConfig from './helpers/loadConfig' @@ -22,6 +29,7 @@ export interface DeployConfig { region?: string profile?: string } + cloudFrontId?: string } export interface DeployStackProps { @@ -55,9 +63,10 @@ const createOutputFolder = () => { export const deploy = async (config: DeployConfig) => { let cleanNextApp try { - const { pruneBeforeDeploy = false, siteName, stage = 'development', aws } = config + const { pruneBeforeDeploy = false, siteName, stage = 'development', aws, cloudFrontId } = 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.') @@ -99,6 +108,10 @@ export const deploy = async (config: DeployConfig) => { } const siteNameLowerCased = siteName.toLowerCase() + if (cloudFrontId) { + customCFDistribution = await getCloudFrontDistribution(cloudfrontClient, cloudFrontId) + } + const nextRenderServerStack = new AppStack( `${siteNameLowerCased}-server`, NextRenderServerStack, @@ -136,7 +149,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() @@ -190,6 +204,18 @@ export const deploy = async (config: DeployConfig) => { VersionLabel: versionLabel }) + // if custom cf distribution, update it + if (customCFDistribution) { + await updateDistribution(cloudfrontClient, customCFDistribution, { + longCachePolicyId: nextCloudfrontStackOutput.LongCachePolicyId!, + splitCachePolicyId: nextCloudfrontStackOutput.SplitCachePolicyId!, + routingFunctionArn: nextCloudfrontStackOutput.RoutingFunctionArn!, + checkExpirationFunctionArn: nextCloudfrontStackOutput.CheckExpirationFunctionArn!, + staticBucketName: nextCloudfrontStackOutput.StaticBucketRegionalDomainName!, + addAdditionalBehaviour: true + }) + } + await cloudfrontClient.createInvalidation({ DistributionId: nextCloudfrontStackOutput.CloudfrontDistributionId!, InvalidationBatch: { From 22c1b21d18c8d8d388018d59955658ad031e1b6c Mon Sep 17 00:00:00 2001 From: oscar Date: Fri, 16 Aug 2024 00:56:12 +0100 Subject: [PATCH 5/8] pass cloudfront id as arg during deploymet --- src/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 5377144..f869530 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ interface CLIOptions { profile?: string nodejs?: string production?: boolean + cloudFrontId?: string } const cli = yargs(hideBin(process.argv)) @@ -46,6 +47,10 @@ const cli = yargs(hideBin(process.argv)) description: 'Creates production stack.', default: false }) + .option('cloudFrontId', { + type: 'string', + describe: 'Existing cloudfront Id.' + }) cli.command( 'bootstrap', @@ -62,7 +67,7 @@ cli.command( 'app deployment', () => {}, async (argv) => { - const { siteName, pruneBeforeDeploy, stage, region, profile, nodejs, production } = argv + const { siteName, pruneBeforeDeploy, stage, region, profile, nodejs, production, cloudFrontId } = argv await deploy({ siteName, @@ -70,6 +75,7 @@ cli.command( pruneBeforeDeploy, nodejs, isProduction: production, + cloudFrontId, aws: { region, profile From 13d8dad8fb4b975c2c4139c5a40067da122f251a Mon Sep 17 00:00:00 2001 From: oscar Date: Fri, 16 Aug 2024 00:56:35 +0100 Subject: [PATCH 6/8] updated Readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bf40ef8..a3c2cd8 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,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:
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 From d17b73cb3fabe6be93090b4fe6b59b67ce52b0fb Mon Sep 17 00:00:00 2001 From: oscar Date: Wed, 21 Aug 2024 00:40:09 +0100 Subject: [PATCH 7/8] add ability to prevent updating cloudfront if there were no changes to it. --- src/common/aws.ts | 79 ++++++++++++++++++++++++++++++++++++++++------ src/types/index.ts | 9 ++++++ 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/src/common/aws.ts b/src/common/aws.ts index 2e20e84..2206af3 100644 --- a/src/common/aws.ts +++ b/src/common/aws.ts @@ -11,9 +11,11 @@ import { UpdateDistributionCommand, GetDistributionCommand, CacheBehavior, - GetDistributionCommandOutput + GetDistributionCommandOutput, + Distribution } from '@aws-sdk/client-cloudfront' import { LambdaEdgeEventType } from 'aws-cdk-lib/aws-cloudfront' +import { UpdateCloudFrontDistribution } from '../types' type GetAWSBasicProps = | { @@ -209,17 +211,70 @@ export const getCloudFrontDistribution = async (cfClient: CloudFront, distributi return response } +export const shouldUpdateDistro = (config: UpdateCloudFrontDistribution, distribution?: Distribution) => { + const { + staticBucketName, + routingFunctionArn, + splitCachePolicyId, + addAdditionalBehaviour, + longCachePolicyId, + checkExpirationFunctionArn + } = config + + const targetOriginId = distribution?.DistributionConfig?.DefaultCacheBehavior?.TargetOriginId + + if (staticBucketName && distribution?.DistributionConfig?.Origins && distribution.DistributionConfig.Origins.Items) { + const mainOrigin = distribution.DistributionConfig.Origins.Items?.find((origin) => origin.Id === targetOriginId) + + if (mainOrigin && mainOrigin.DomainName !== staticBucketName) { + return true + } + } + + if (addAdditionalBehaviour) { + const _nextDataBehaviour = (distribution?.DistributionConfig?.CacheBehaviors?.Items || []).find( + (b) => b.PathPattern === '/_next/data/*' + ) + + if ( + !_nextDataBehaviour || + (_nextDataBehaviour && + (_nextDataBehaviour.CachePolicyId !== splitCachePolicyId || + !_nextDataBehaviour.LambdaFunctionAssociations?.Items?.find( + (item) => item.LambdaFunctionARN === routingFunctionArn + ))) + ) { + return true + } + + const _nextBehaviour = (distribution?.DistributionConfig?.CacheBehaviors?.Items || []).find( + (b) => b.PathPattern === '/_next/*' + ) + + if (!_nextBehaviour || _nextBehaviour.CachePolicyId !== longCachePolicyId) { + return true + } + } + + const defBehavior = distribution?.DistributionConfig?.DefaultCacheBehavior + const originReqLambdaFunc = defBehavior?.LambdaFunctionAssociations?.Items?.find( + (item) => item.LambdaFunctionARN === routingFunctionArn + ) + const originResLambdaFunc = defBehavior?.LambdaFunctionAssociations?.Items?.find( + (item) => item.LambdaFunctionARN === checkExpirationFunctionArn + ) + + if (defBehavior?.CachePolicyId !== splitCachePolicyId || !originResLambdaFunc || !originReqLambdaFunc) { + return true + } + + return false +} + export const updateDistribution = async ( cfClient: CloudFront, distribution: GetDistributionCommandOutput, - config: { - staticBucketName?: string - longCachePolicyId?: string - splitCachePolicyId?: string - routingFunctionArn?: string - checkExpirationFunctionArn?: string - addAdditionalBehaviour?: boolean - } + config: UpdateCloudFrontDistribution ) => { const { staticBucketName, @@ -230,6 +285,12 @@ export const updateDistribution = async ( checkExpirationFunctionArn } = config const { Distribution, ETag } = distribution + + //shouldn't update distribution if nothing changed + if (!shouldUpdateDistro(config, Distribution)) { + return + } + if (Distribution && Distribution.DistributionConfig) { const targetOriginId = Distribution.DistributionConfig?.DefaultCacheBehavior?.TargetOriginId if (staticBucketName && Distribution.DistributionConfig.Origins && Distribution.DistributionConfig.Origins.Items) { diff --git a/src/types/index.ts b/src/types/index.ts index 921261f..0cbe46c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,3 +4,12 @@ export interface CacheConfig { cacheQueries?: string[] enableDeviceSplit?: boolean } + +export interface UpdateCloudFrontDistribution { + staticBucketName?: string + longCachePolicyId?: string + splitCachePolicyId?: string + routingFunctionArn?: string + checkExpirationFunctionArn?: string + addAdditionalBehaviour?: boolean +} From f47986b91dee258d46a821a1c723623d5132d5b6 Mon Sep 17 00:00:00 2001 From: oscar Date: Wed, 21 Aug 2024 01:34:52 +0100 Subject: [PATCH 8/8] add cli option that skipes setting the default behavior of custom cloudfront distribution --- src/commands/deploy.ts | 6 ++++-- src/common/aws.ts | 47 ++++++++++++++++++++++-------------------- src/index.ts | 9 +++++++- src/types/index.ts | 1 + 4 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 07185a1..908d129 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -30,6 +30,7 @@ export interface DeployConfig { profile?: string } cloudFrontId?: string + skipDefaultBehavior?: boolean } export interface DeployStackProps { @@ -62,7 +63,7 @@ const createOutputFolder = () => { export const deploy = async (config: DeployConfig) => { let cleanNextApp try { - const { siteName, stage = 'development', aws, cloudFrontId } = 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 @@ -212,7 +213,8 @@ export const deploy = async (config: DeployConfig) => { routingFunctionArn: nextCloudfrontStackOutput.RoutingFunctionArn!, checkExpirationFunctionArn: nextCloudfrontStackOutput.CheckExpirationFunctionArn!, staticBucketName: nextCloudfrontStackOutput.StaticBucketRegionalDomainName!, - addAdditionalBehaviour: true + addAdditionalBehaviour: true, + skipDefaultBehavior }) } diff --git a/src/common/aws.ts b/src/common/aws.ts index 7f51a4c..cd05ee3 100644 --- a/src/common/aws.ts +++ b/src/common/aws.ts @@ -326,7 +326,8 @@ export const updateDistribution = async ( splitCachePolicyId, addAdditionalBehaviour, longCachePolicyId, - checkExpirationFunctionArn + checkExpirationFunctionArn, + skipDefaultBehavior } = config const { Distribution, ETag } = distribution @@ -390,28 +391,30 @@ export const updateDistribution = async ( } } - const defBeh = Distribution.DistributionConfig.DefaultCacheBehavior + if (!skipDefaultBehavior) { + const defBeh = Distribution.DistributionConfig.DefaultCacheBehavior - Distribution.DistributionConfig.DefaultCacheBehavior = { - ...defBeh, - LambdaFunctionAssociations: { - Quantity: 2, - Items: [ - { - EventType: LambdaEdgeEventType.ORIGIN_REQUEST, - LambdaFunctionARN: routingFunctionArn - }, - { - EventType: LambdaEdgeEventType.ORIGIN_RESPONSE, - LambdaFunctionARN: checkExpirationFunctionArn - } - ] - }, - TargetOriginId: targetOriginId, - ViewerProtocolPolicy: 'allow-all', - SmoothStreaming: false, - Compress: true, - CachePolicyId: splitCachePolicyId + Distribution.DistributionConfig.DefaultCacheBehavior = { + ...defBeh, + LambdaFunctionAssociations: { + Quantity: 2, + Items: [ + { + EventType: LambdaEdgeEventType.ORIGIN_REQUEST, + LambdaFunctionARN: routingFunctionArn + }, + { + EventType: LambdaEdgeEventType.ORIGIN_RESPONSE, + LambdaFunctionARN: checkExpirationFunctionArn + } + ] + }, + TargetOriginId: targetOriginId, + ViewerProtocolPolicy: 'allow-all', + SmoothStreaming: false, + Compress: true, + CachePolicyId: splitCachePolicyId + } } } diff --git a/src/index.ts b/src/index.ts index ef31a13..07443aa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ interface CLIOptions { nodejs?: string production?: boolean cloudFrontId?: string + skipDefaultBehavior?: boolean } const cli = yargs(hideBin(process.argv)) @@ -45,6 +46,11 @@ const cli = yargs(hideBin(process.argv)) type: 'string', describe: 'Existing cloudfront Id.' }) + .option('skipDefaultBehavior', { + type: 'boolean', + description: 'Skip updating default cloudfront default behavior.', + default: false + }) cli.command( 'bootstrap', @@ -61,7 +67,7 @@ cli.command( 'app deployment', () => {}, async (argv) => { - const { siteName, stage, region, profile, nodejs, production, cloudFrontId } = argv + const { siteName, stage, region, profile, nodejs, production, cloudFrontId, skipDefaultBehavior } = argv await deploy({ siteName, @@ -69,6 +75,7 @@ cli.command( nodejs, isProduction: production, cloudFrontId, + skipDefaultBehavior, aws: { region, profile diff --git a/src/types/index.ts b/src/types/index.ts index 0cbe46c..cd94ba9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -12,4 +12,5 @@ export interface UpdateCloudFrontDistribution { routingFunctionArn?: string checkExpirationFunctionArn?: string addAdditionalBehaviour?: boolean + skipDefaultBehavior?: boolean }