Skip to content

Commit

Permalink
fixing origin allowed headers, behaviors, rewrite retries
Browse files Browse the repository at this point in the history
  • Loading branch information
revmischa committed Oct 11, 2022
1 parent 25c118a commit f83ef45
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 95 deletions.
36 changes: 18 additions & 18 deletions API.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class NextjsSst extends Nextjs {
const app = this.node.root as App;
app.registerSiteEnvironment({
id: this.node.id,
path: this.props.path,
path: this.props.nextjsPath,
stack: Stack.of(this).node.id,
environmentOutputs,
} as BaseSiteEnvironmentOutputsInfo);
Expand Down
53 changes: 23 additions & 30 deletions src/Nextjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ export interface NextjsCdkProps {
readonly cachePolicies?: NextjsCachePolicyProps;

/**
* Override the default CloudFront image origin request policy created internally
* Override the default CloudFront lambda origin request policy created internally
*/
readonly imageOriginRequestPolicy?: cloudfront.IOriginRequestPolicy;
readonly lambdaOriginRequestPolicy?: cloudfront.IOriginRequestPolicy;

/**
* Override static file deployment settings.
Expand Down Expand Up @@ -161,11 +161,13 @@ export class Nextjs extends Construct {
};

/**
* The default CloudFront image origin request policy properties for Next images.
* The default CloudFront lambda origin request policy.
*/
public static imageOriginRequestPolicyProps: cloudfront.OriginRequestPolicyProps = {
public static lambdaOriginRequestPolicyProps: cloudfront.OriginRequestPolicyProps = {
cookieBehavior: cloudfront.OriginRequestCookieBehavior.all(),
queryStringBehavior: cloudfront.OriginRequestQueryStringBehavior.all(),
comment: 'Nextjs Lambda Default Origin Request Policy',
headerBehavior: cloudfront.OriginRequestHeaderBehavior.all(),
comment: 'Nextjs Lambda Origin Request Policy',
};

/**
Expand Down Expand Up @@ -317,21 +319,6 @@ export class Nextjs extends Construct {
return this.distribution.distributionDomainName;
}

/////////////////////
// Public Methods
/////////////////////

// FIXME: SST
// public getConstructMetadata() {
// return {
// type: 'Nextjs' as const,
// data: {
// distributionId: this.distribution.distributionId,
// customDomainUrl: this.customDomainUrl,
// },
// };
// }

/////////////////////
// CloudFront Distribution
/////////////////////
Expand Down Expand Up @@ -372,9 +359,9 @@ export class Nextjs extends Construct {
}

const staticCachePolicy = cdk?.cachePolicies?.staticCachePolicy ?? this.createCloudFrontStaticCachePolicy();

const imageCachePolicy = cdk?.cachePolicies?.imageCachePolicy ?? this.createCloudFrontImageCachePolicy();
const imageOriginRequestPolicy = cdk?.imageOriginRequestPolicy ?? this.createCloudFrontImageOriginRequestPolicy();

const lambdaOriginRequestPolicy = cdk?.lambdaOriginRequestPolicy ?? this.createLambdaOriginRequestPolicy();

// main server function origin (lambda URL HTTP origin)
const fnUrl = this.serverFunction.addFunctionUrl({
Expand Down Expand Up @@ -410,9 +397,12 @@ export class Nextjs extends Construct {
viewerProtocolPolicy,
origin: serverFunctionOrigin,
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
// allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
// cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
originRequestPolicy: lambdaOriginRequestPolicy,
compress: true,
cachePolicy: lambdaCachePolicy,
// cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
};

// if we don't have a static file called index.html then we should
Expand All @@ -430,12 +420,15 @@ export class Nextjs extends Construct {
domainNames,
certificate: this.certificate,
defaultBehavior: {
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
origin: fallbackOriginGroup, // try S3 first, then lambda
// allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
// allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, // doesn't work with an OriginGroup
// cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
compress: true,
cachePolicy: lambdaCachePolicy, // what goes here? static or lambda/

// what goes here? static or lambda?
cachePolicy: lambdaCachePolicy,
originRequestPolicy: lambdaOriginRequestPolicy,
},

additionalBehaviors: {
Expand All @@ -455,11 +448,11 @@ export class Nextjs extends Construct {
'_next/image*': {
viewerProtocolPolicy,
origin: serverFunctionOrigin,
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
compress: true,
cachePolicy: imageCachePolicy,
originRequestPolicy: imageOriginRequestPolicy,
originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER, // not sure what goes here
},

...(cfDistributionProps?.additionalBehaviors || {}),
Expand All @@ -475,8 +468,8 @@ export class Nextjs extends Construct {
return new cloudfront.CachePolicy(this, 'ImageCache', Nextjs.imageCachePolicyProps);
}

private createCloudFrontImageOriginRequestPolicy(): cloudfront.OriginRequestPolicy {
return new cloudfront.OriginRequestPolicy(this, 'ImageOriginRequest', Nextjs.imageOriginRequestPolicyProps);
private createLambdaOriginRequestPolicy(): cloudfront.OriginRequestPolicy {
return new cloudfront.OriginRequestPolicy(this, 'LambdaOriginPolicy', Nextjs.lambdaOriginRequestPolicyProps);
}

private createCloudFrontLambdaCachePolicy(): cloudfront.CachePolicy {
Expand Down
114 changes: 68 additions & 46 deletions src/NextjsAssetsDeployment.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as path from 'path';
import { CustomResource, RemovalPolicy, Token } from 'aws-cdk-lib';
import { CustomResource, Duration, RemovalPolicy, Token } from 'aws-cdk-lib';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
Expand Down Expand Up @@ -115,7 +115,7 @@ export class NextJsAssetsDeployment extends Construct {

// do rewrites of unresolved CDK tokens in static files
const rewriter = this.createRewriteResource();
rewriter?.node.addDependency(...deployments);
rewriter?.node.addDependency(...deployments.map((d) => d.deployedBucket));

return deployments;
}
Expand All @@ -129,53 +129,74 @@ export class NextJsAssetsDeployment extends Construct {
const rewriteFn = new lambda.Function(this, 'RewriteOnEventHandler', {
runtime: lambda.Runtime.NODEJS_16_X,
memorySize: 1024,
timeout: Duration.minutes(5),
handler: 'index.handler',
code: lambda.Code.fromInline(`
const AWS = require('aws-sdk');
// search and replace tokenized values of designated objects in s3
exports.handler = async (event) => {
const requestType = event.RequestType;
if (requestType === 'Create' || requestType === 'Update') {
// rewrite static files
const s3 = new AWS.S3();
const { s3keys, bucket, replacements } = event.ResourceProperties;
if (!s3keys || !bucket || !replacements) {
console.error("Missing required properties")
return
}
const promises = s3keys.map(async (key) => {
const params = { Bucket: bucket, Key: key };
// console.info('Rewriting', key, 'in bucket', bucket);
const data = await s3.getObject(params).promise();
const bodyPre = data.Body.toString('utf-8');
let bodyPost = bodyPre;
// do replacements of tokens
Object.entries(replacements).forEach(([key, value]) => {
bodyPost = bodyPost.replace(key, value);
});
// didn't change?
if (bodyPost === bodyPre)
return;
// upload
console.info('Rewrote', key, 'in bucket', bucket);
const putParams = {
...params,
Body: bodyPost,
ContentType: data.ContentType,
ContentEncoding: data.ContentEncoding,
CacheControl: data.CacheControl,
}
await s3.putObject(putParams).promise();
});
await Promise.all(promises);
}
const AWS = require("aws-sdk");
async function tryGetObject(bucket, key, tries) {
const s3 = new AWS.S3();
try {
return await s3.getObject({ Bucket: bucket, Key: key }).promise();
} catch (err) {
console.error("Failed to retrieve object", key, "\\nCode:", err.code, err);
// if access denied - wait a few seconds and try again
if (err.code === "AccessDenied" && tries < 3) {
console.info("Retrying for object", key);
await new Promise((res) => setTimeout(res, 5000));
return tryGetObject(bucket, key, ++tries);
} else {
throw err;
}
}
}
return event;
};
async function doRewrites(event) {
// rewrite static files
const s3 = new AWS.S3();
const { s3keys, bucket, replacements } = event.ResourceProperties;
if (!s3keys || !bucket || !replacements) {
console.error("Missing required properties");
return;
}
const promises = s3keys.map(async (key) => {
const params = { Bucket: bucket, Key: key };
console.info("Rewriting", key, "in bucket", bucket);
const res = await tryGetObject(bucket, key, 0);
const bodyPre = res.Body.toString("utf-8");
let bodyPost = bodyPre;
// do replacements of tokens
Object.entries(replacements).forEach(([key, value]) => {
bodyPost = bodyPost.replace(key, value);
});
// didn't change?
if (bodyPost === bodyPre) return;
// upload
console.info("Rewrote", key, "in bucket", bucket);
const putParams = {
...params,
Body: bodyPost,
ContentType: res.ContentType,
ContentEncoding: res.ContentEncoding,
CacheControl: res.CacheControl,
};
await s3.putObject(putParams).promise();
});
await Promise.all(promises);
}
// search and replace tokenized values of designated objects in s3
exports.handler = async (event) => {
const requestType = event.RequestType;
if (requestType === "Create" || requestType === "Update") {
await doRewrites(event);
}
return event;
}
`),
initialPolicy: [
new iam.PolicyStatement({
Expand Down Expand Up @@ -303,6 +324,7 @@ export class NextJsAssetsDeployment extends Construct {

Object.entries(this.props.environment || {})
.filter(([, value]) => Token.isUnresolved(value))
.filter(([key]) => key.startsWith('NEXT_PUBLIC_')) // don't replace server-only env vars
.forEach(([key, value]) => {
const token = `{{ ${key} }}`;
replacements[token] = value.toString();
Expand Down

0 comments on commit f83ef45

Please sign in to comment.