Skip to content
This repository was archived by the owner on Jan 28, 2025. It is now read-only.

Commit 3091c7c

Browse files
authored
fix(nextjs-component, cloudfront): wait for distribution to be ready before creating invalidations (#1281)
1 parent 2e8df93 commit 3091c7c

File tree

11 files changed

+189
-11
lines changed

11 files changed

+189
-11
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ myNextApplication:
244244
minimumProtocolVersion: "TLSv1.2_2019" # can be omitted, defaults to "TLSv1.2_2019"
245245
originAccessIdentityId: XYZEXAMPLE #optional
246246
paths: ["/*"] # which paths should be invalidated on deploy, default matches everything, empty array skips invalidation completely
247+
waitBeforeInvalidate: true # by default true, it waits for the CloudFront distribution to have completed before invalidating, to avoid possibly caching old page
247248
```
248249

249250
This is particularly useful for caching any of your Next.js pages at CloudFront's edge locations. See [this](https://github.com/serverless-nextjs/serverless-next.js/tree/master/packages/serverless-components/nextjs-component/examples/app-with-custom-caching-config) for an example application with custom cache configuration.
@@ -527,8 +528,6 @@ The fourth cache behaviour handles next API requests `api/*`.
527528
| authentication.password | `string` | `undefined` | Password for basic authentication. **Note: it is highly recommended not to reuse a password here as it gets inlined in plaintext in the Lambda handler.** |
528529
| enableS3Acceleration | `boolean` | `true` | Whether to enable S3 transfer acceleration. This may be useful to disable as some AWS regions do not support it. See [reference](https://docs.amazonaws.cn/en_us/aws/latest/userguide/s3.html). |
529530

530-
|
531-
532531
Custom inputs can be configured like this:
533532

534533
```yaml

packages/libs/cloudfront/src/index.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export type CreateInvalidationOptions = {
77
paths?: string[];
88
};
99

10-
const createInvalidation = async (
10+
const createInvalidation = (
1111
options: CreateInvalidationOptions
1212
): Promise<AWS.CloudFront.CreateInvalidationResult> => {
1313
const { credentials, distributionId, paths } = options;
@@ -18,4 +18,36 @@ const createInvalidation = async (
1818
return cf.createInvalidation({ distributionId, paths });
1919
};
2020

21-
export default createInvalidation;
21+
export type CheckCloudFrontDistributionReadyOptions = {
22+
credentials: Credentials;
23+
distributionId: string;
24+
waitDuration: number;
25+
pollInterval: number;
26+
};
27+
28+
const checkCloudFrontDistributionReady = async (
29+
options: CheckCloudFrontDistributionReadyOptions
30+
): Promise<boolean> => {
31+
const { credentials, distributionId, waitDuration, pollInterval } = options;
32+
const startDate = new Date();
33+
const startTime = startDate.getTime();
34+
const waitDurationMillis = waitDuration * 1000;
35+
36+
const cf = CloudFrontClientFactory({
37+
credentials
38+
});
39+
40+
while (new Date().getTime() - startTime < waitDurationMillis) {
41+
const result = await cf.getDistribution(distributionId);
42+
43+
if (result.Distribution?.Status === "Deployed") {
44+
return true;
45+
}
46+
47+
await new Promise((r) => setTimeout(r, pollInterval * 1000));
48+
}
49+
50+
return false;
51+
};
52+
53+
export { createInvalidation, checkCloudFrontDistributionReady };

packages/libs/cloudfront/src/lib/cloudfront.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ export type CloudFrontClient = {
1515
createInvalidation: (
1616
options: CreateInvalidationOptions
1717
) => Promise<AWS.CloudFront.CreateInvalidationResult>;
18+
getDistribution: (
19+
distributionId: string
20+
) => Promise<AWS.CloudFront.GetDistributionResult>;
1821
};
1922

2023
export type Credentials = {
@@ -51,6 +54,15 @@ export default ({
5154
}
5255
})
5356
.promise();
57+
},
58+
getDistribution: async (
59+
distributionId: string
60+
): Promise<AWS.CloudFront.GetDistributionResult> => {
61+
return await cloudFront
62+
.getDistribution({
63+
Id: distributionId
64+
})
65+
.promise();
5466
}
5567
};
5668
};

packages/libs/cloudfront/tests/aws-sdk.mock.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
declare module "aws-sdk" {
22
const mockCreateInvalidation: jest.Mock;
33
const mockCreateInvalidationPromise: jest.Mock;
4+
const mockGetDistribution: jest.Mock;
5+
const mockGetDistributionPromise: jest.Mock;
46
}
57

68
const promisifyMock = (mockFn: jest.Mock): jest.Mock => {
@@ -14,8 +16,12 @@ export const mockCreateInvalidationPromise = promisifyMock(
1416
mockCreateInvalidation
1517
);
1618

19+
export const mockGetDistribution = jest.fn();
20+
export const mockGetDistributionPromise = promisifyMock(mockGetDistribution);
21+
1722
const MockCloudFront = jest.fn(() => ({
18-
createInvalidation: mockCreateInvalidation
23+
createInvalidation: mockCreateInvalidation,
24+
getDistribution: mockGetDistribution
1925
}));
2026

2127
export default {

packages/libs/cloudfront/tests/cache-invalidation.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import AWS, { mockCreateInvalidation } from "aws-sdk";
2-
import createInvalidation, { CreateInvalidationOptions } from "../src/index";
2+
import { createInvalidation, CreateInvalidationOptions } from "../src/index";
33
import { ALL_FILES_PATH } from "../src/lib/constants";
44

55
jest.mock("aws-sdk", () => require("./aws-sdk.mock"));
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import AWS, { mockGetDistribution } from "aws-sdk";
2+
import {
3+
checkCloudFrontDistributionReady,
4+
CheckCloudFrontDistributionReadyOptions
5+
} from "../src/index";
6+
7+
jest.mock("aws-sdk", () => require("./aws-sdk.mock"));
8+
9+
const checkReady = (
10+
options: Partial<CheckCloudFrontDistributionReadyOptions> = {}
11+
): Promise<boolean> => {
12+
return checkCloudFrontDistributionReady({
13+
...options,
14+
distributionId: "fake-distribution-id",
15+
credentials: {
16+
accessKeyId: "fake-access-key",
17+
secretAccessKey: "fake-secret-key",
18+
sessionToken: "fake-session-token"
19+
},
20+
waitDuration: 1,
21+
pollInterval: 1
22+
});
23+
};
24+
25+
describe("Check CloudFront distribution ready tests", () => {
26+
it("passes credentials to CloudFront client", async () => {
27+
mockGetDistribution.mockReturnValue({
28+
promise: () => {
29+
return {
30+
Distribution: {
31+
Status: "Deployed"
32+
}
33+
};
34+
}
35+
});
36+
37+
await checkReady();
38+
39+
expect(AWS.CloudFront).toBeCalledWith({
40+
credentials: {
41+
accessKeyId: "fake-access-key",
42+
secretAccessKey: "fake-secret-key",
43+
sessionToken: "fake-session-token"
44+
}
45+
});
46+
});
47+
48+
it("successfully waits for CloudFront distribution", async () => {
49+
const isReady = await checkReady();
50+
51+
expect(isReady).toBe(true);
52+
expect(mockGetDistribution).toBeCalledWith({ Id: "fake-distribution-id" });
53+
});
54+
55+
it("times out waiting for CloudFront distribution", async () => {
56+
mockGetDistribution.mockReturnValue({
57+
promise: () => {
58+
return {
59+
Distribution: {
60+
Status: "InProgress"
61+
}
62+
};
63+
}
64+
});
65+
66+
const isReady = await checkReady();
67+
68+
expect(isReady).toBe(false);
69+
expect(mockGetDistribution).toBeCalledWith({ Id: "fake-distribution-id" });
70+
});
71+
});
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
1-
module.exports = jest.fn();
1+
const mockCreateInvalidation = jest.fn();
2+
const mockCheckCloudFrontDistributionReady = jest.fn();
3+
4+
module.exports = {
5+
mockCreateInvalidation,
6+
mockCheckCloudFrontDistributionReady,
7+
checkCloudFrontDistributionReady: mockCheckCloudFrontDistributionReady,
8+
createInvalidation: mockCreateInvalidation
9+
};

packages/serverless-components/nextjs-component/__tests__/basepath.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ describe("basepath tests", () => {
6060
secretAccessKey: "456"
6161
}
6262
};
63+
component.context.debug = () => {
64+
// intentionally empty
65+
};
6366

6467
await component.build();
6568

packages/serverless-components/nextjs-component/__tests__/custom-inputs.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { mockDomain } from "@sls-next/domain";
44
import { mockS3 } from "@serverless/aws-s3";
55
import { mockUpload } from "aws-sdk";
66
import { mockLambda, mockLambdaPublish } from "@sls-next/aws-lambda";
7-
import mockCreateInvalidation from "@sls-next/cloudfront";
7+
import { mockCreateInvalidation } from "@sls-next/cloudfront";
88
import { mockCloudFront } from "@sls-next/aws-cloudfront";
99
import { mockSQS } from "@sls-next/aws-sqs";
1010

@@ -26,6 +26,9 @@ const createNextComponent = () => {
2626
secretAccessKey: "456"
2727
}
2828
};
29+
component.context.debug = () => {
30+
// intentionally empty
31+
};
2932
return component;
3033
};
3134

packages/serverless-components/nextjs-component/__tests__/deploy.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import fse from "fs-extra";
33
import { mockS3 } from "@serverless/aws-s3";
44
import { mockCloudFront } from "@sls-next/aws-cloudfront";
55
import { mockLambda, mockLambdaPublish } from "@sls-next/aws-lambda";
6-
import mockCreateInvalidation from "@sls-next/cloudfront";
6+
import {
7+
mockCreateInvalidation,
8+
mockCheckCloudFrontDistributionReady
9+
} from "@sls-next/cloudfront";
710
import NextjsComponent from "../src/component";
811
import { mockSQS } from "@sls-next/aws-sqs";
912
import {
@@ -82,6 +85,9 @@ describe.each`
8285
secretAccessKey: "456"
8386
}
8487
};
88+
component.context.debug = () => {
89+
// intentionally empty
90+
};
8591

8692
await component.build();
8793

@@ -384,6 +390,16 @@ describe.each`
384390
});
385391

386392
it("invalidates distribution cache", () => {
393+
expect(mockCheckCloudFrontDistributionReady).toBeCalledWith({
394+
credentials: {
395+
accessKeyId: "123",
396+
secretAccessKey: "456"
397+
},
398+
distributionId: "cloudfrontdistrib",
399+
pollInterval: 10,
400+
waitDuration: 600
401+
});
402+
387403
expect(mockCreateInvalidation).toBeCalledWith({
388404
credentials: {
389405
accessKeyId: "123",

packages/serverless-components/nextjs-component/src/component.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import {
1313
deleteOldStaticAssets,
1414
uploadStaticAssetsFromBuild
1515
} from "@sls-next/s3-static-assets";
16-
import createInvalidation from "@sls-next/cloudfront";
16+
import {
17+
createInvalidation,
18+
checkCloudFrontDistributionReady
19+
} from "@sls-next/cloudfront";
1720
import obtainDomains from "./lib/obtainDomains";
1821
import {
1922
DEFAULT_LAMBDA_CODE_DIR,
@@ -310,6 +313,7 @@ class NextjsComponent extends Component {
310313
certificate: cloudFrontCertificate,
311314
originAccessIdentityId: cloudFrontOriginAccessIdentityId,
312315
paths: cloudFrontPaths,
316+
waitBeforeInvalidate: cloudFrontWaitBeforeInvalidate = true,
313317
...cloudFrontOtherInputs
314318
} = inputs.cloudfront || {};
315319

@@ -850,12 +854,36 @@ class NextjsComponent extends Component {
850854

851855
let appUrl = cloudFrontOutputs.url;
852856

857+
const distributionId = cloudFrontOutputs.id;
853858
if (!cloudFrontPaths || cloudFrontPaths.length) {
859+
// We need to wait for distribution to be fully propagated before trying to invalidate paths, otherwise we may cache old page
860+
// This could add ~1-2 minute to deploy time but it is safer
861+
const waitDuration = 600;
862+
const pollInterval = 10;
863+
if (cloudFrontWaitBeforeInvalidate) {
864+
this.context.debug(
865+
`Waiting for CloudFront distribution ${distributionId} to be ready before invalidations, for up to ${waitDuration} seconds, checking every ${pollInterval} seconds.`
866+
);
867+
await checkCloudFrontDistributionReady({
868+
distributionId: distributionId,
869+
credentials: this.context.credentials.aws,
870+
waitDuration: waitDuration,
871+
pollInterval: pollInterval
872+
});
873+
} else {
874+
this.context.debug(
875+
`Skipped waiting for CloudFront distribution ${distributionId} to be ready.`
876+
);
877+
}
878+
879+
this.context.debug(`Creating invalidations on ${distributionId}.`);
854880
await createInvalidation({
855-
distributionId: cloudFrontOutputs.id,
881+
distributionId: distributionId,
856882
credentials: this.context.credentials.aws,
857883
paths: cloudFrontPaths
858884
});
885+
} else {
886+
this.context.debug(`No invalidations needed for ${distributionId}.`);
859887
}
860888

861889
const { domain, subdomain } = obtainDomains(inputs.domain);

0 commit comments

Comments
 (0)