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

Commit 6f68bf1

Browse files
authored
feat(aws-cloudfront, nextjs-component): support setting certificate input in cloudfront (#727)
1 parent a356443 commit 6f68bf1

File tree

6 files changed

+184
-2
lines changed

6 files changed

+184
-2
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,12 @@ myNextApplication:
217217
geoRestriction:
218218
restrictionType: "blacklist" # valid values are whitelist/blacklist/none. Set to "none" and omit items to disable restrictions
219219
items: ["AA"] # ISO 3166 alpha-2 country codes
220+
certificate:
221+
cloudFrontDefaultCertificate: false # specify false and one of IAM/ACM certificates, or specify true and omit IAM/ACM inputs for default certificate
222+
acmCertificateArn: "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"
223+
iamCertificateId: "iam-certificate-id" # specify either ACM or IAM certificate, not both
224+
sslSupportMethod: "sni-only" # can be omitted, defaults to "sni-only"
225+
minimumProtocolVersion: "TLSv1.2_2019" # can be omitted, defaults to "TLSv1.2_2019"
220226
```
221227

222228
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.

packages/serverless-components/aws-cloudfront/__tests__/general-options.test.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,4 +356,100 @@ describe("General options propagation", () => {
356356
})
357357
});
358358
});
359+
360+
it("create distribution with certificate arn and updates it", async () => {
361+
// Create
362+
await component.default({
363+
certificate: {
364+
cloudFrontDefaultCertificate: false,
365+
acmCertificateArn:
366+
"arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"
367+
},
368+
origins
369+
});
370+
371+
expect(mockCreateDistribution).toBeCalledWith(
372+
expect.objectContaining({
373+
DistributionConfig: expect.objectContaining({
374+
ViewerCertificate: {
375+
CloudFrontDefaultCertificate: false,
376+
ACMCertificateArn:
377+
"arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
378+
SSLSupportMethod: "sni-only",
379+
MinimumProtocolVersion: "TLSv1.2_2019"
380+
}
381+
})
382+
})
383+
);
384+
385+
// Update
386+
await component.default({
387+
certificate: {
388+
cloudFrontDefaultCertificate: false,
389+
acmCertificateArn:
390+
"arn:aws:acm:us-east-1:123456789012:certificate/updated"
391+
},
392+
origins
393+
});
394+
395+
expect(mockUpdateDistribution).toBeCalledWith(
396+
expect.objectContaining({
397+
DistributionConfig: expect.objectContaining({
398+
ViewerCertificate: {
399+
CloudFrontDefaultCertificate: false,
400+
ACMCertificateArn:
401+
"arn:aws:acm:us-east-1:123456789012:certificate/updated",
402+
SSLSupportMethod: "sni-only",
403+
MinimumProtocolVersion: "TLSv1.2_2019"
404+
}
405+
})
406+
})
407+
);
408+
});
409+
410+
it("create distribution with default certificate", async () => {
411+
// Create
412+
await component.default({
413+
certificate: {
414+
cloudFrontDefaultCertificate: true
415+
},
416+
origins
417+
});
418+
419+
expect(mockCreateDistribution).toBeCalledWith(
420+
expect.objectContaining({
421+
DistributionConfig: expect.objectContaining({
422+
ViewerCertificate: {
423+
CloudFrontDefaultCertificate: true,
424+
SSLSupportMethod: "sni-only",
425+
MinimumProtocolVersion: "TLSv1.2_2019"
426+
}
427+
})
428+
})
429+
);
430+
});
431+
432+
it("create distribution with IAM certificate", async () => {
433+
// Create
434+
await component.default({
435+
certificate: {
436+
cloudFrontDefaultCertificate: false,
437+
iamCertificateId: "12345"
438+
},
439+
origins
440+
});
441+
442+
expect(mockCreateDistribution).toBeCalledWith(
443+
expect.objectContaining({
444+
DistributionConfig: expect.objectContaining({
445+
ViewerCertificate: {
446+
CloudFrontDefaultCertificate: false,
447+
IAMCertificateId: "12345",
448+
SSLSupportMethod: "sni-only",
449+
MinimumProtocolVersion: "TLSv1.2_2019"
450+
}
451+
})
452+
})
453+
);
454+
});
359455
});

packages/serverless-components/aws-cloudfront/lib/index.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ const createOriginAccessIdentity = require("./createOriginAccessIdentity");
44
const grantCloudFrontBucketAccess = require("./grantCloudFrontBucketAccess");
55
const getCustomErrorResponses = require("./getCustomErrorResponses");
66

7+
const DEFAULT_MINIMUM_PROTOCOL_VERSION = "TLSv1.2_2019";
8+
const DEFAULT_SSL_SUPPORT_METHOD = "sni-only";
9+
710
const servePrivateContentEnabled = (inputs) =>
811
inputs.origins.some((origin) => {
912
return origin && origin.private === true;
@@ -113,6 +116,27 @@ const createCloudFrontDistribution = async (cf, s3, inputs) => {
113116
}
114117
}
115118

119+
// Note this will override the certificate which is also set by domain input
120+
if (inputs.certificate !== undefined && inputs.certificate !== null) {
121+
if (typeof inputs.certificate !== "object") {
122+
throw new Error(
123+
"Certificate input must be an object with cloudFrontDefaultCertificate, acmCertificateArn, iamCertificateId, sslSupportMethod, minimumProtocolVersion."
124+
);
125+
}
126+
127+
distributionConfig.ViewerCertificate = {
128+
CloudFrontDefaultCertificate:
129+
inputs.certificate.cloudFrontDefaultCertificate,
130+
ACMCertificateArn: inputs.certificate.acmCertificateArn,
131+
IAMCertificateId: inputs.certificate.iamCertificateId,
132+
SSLSupportMethod:
133+
inputs.certificate.sslSupportMethod || DEFAULT_SSL_SUPPORT_METHOD,
134+
MinimumProtocolVersion:
135+
inputs.certificate.minimumProtocolVersion ||
136+
DEFAULT_MINIMUM_PROTOCOL_VERSION
137+
};
138+
}
139+
116140
const res = await cf.createDistribution(params).promise();
117141

118142
return {
@@ -181,6 +205,26 @@ const updateCloudFrontDistribution = async (cf, s3, distributionId, inputs) => {
181205
}
182206
}
183207

208+
// Note this will override the certificate which is also set by domain input
209+
if (inputs.certificate !== undefined && inputs.certificate !== null) {
210+
if (typeof inputs.certificate !== "object") {
211+
throw new Error(
212+
"Certificate input must be an object with cloudFrontDefaultCertificate, acmCertificateArn, iamCertificateId, sslSupportMethod, minimumProtocolVersion."
213+
);
214+
}
215+
params.DistributionConfig.ViewerCertificate = {
216+
CloudFrontDefaultCertificate:
217+
inputs.certificate.cloudFrontDefaultCertificate,
218+
ACMCertificateArn: inputs.certificate.acmCertificateArn,
219+
IAMCertificateId: inputs.certificate.iamCertificateId,
220+
SSLSupportMethod:
221+
inputs.certificate.sslSupportMethod || DEFAULT_SSL_SUPPORT_METHOD,
222+
MinimumProtocolVersion:
223+
inputs.certificate.minimumProtocolVersion ||
224+
DEFAULT_MINIMUM_PROTOCOL_VERSION
225+
};
226+
}
227+
184228
let s3CanonicalUserId;
185229
let originAccessIdentityId;
186230

packages/serverless-components/aws-cloudfront/serverless.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ class CloudFront extends Component {
6161
!equals(this.state.priceClass, inputs.priceClass) ||
6262
!equals(this.state.errorPages, inputs.errorPages) ||
6363
!equals(this.state.webACLId, inputs.webACLId) ||
64-
!equals(this.state.restrictions, inputs.restrictions)
64+
!equals(this.state.restrictions, inputs.restrictions) ||
65+
!equals(this.state.certificate, inputs.certificate)
6566
) {
6667
this.context.debug(
6768
`Updating CloudFront distribution of ID ${this.state.id}.`

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,5 +1097,38 @@ describe("Custom inputs", () => {
10971097
}
10981098
});
10991099
});
1100+
1101+
it("sets certificate with an ACM ARN", async () => {
1102+
await createNextComponent().default({
1103+
cloudfront: {
1104+
certificate: {
1105+
cloudFrontDefaultCertificate: false,
1106+
acmCertificateArn:
1107+
"arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"
1108+
}
1109+
}
1110+
});
1111+
});
1112+
1113+
it("sets certificate with an IAM certificate", async () => {
1114+
await createNextComponent().default({
1115+
cloudfront: {
1116+
certificate: {
1117+
cloudFrontDefaultCertificate: false,
1118+
iamCertificateId: "iam-cert-id"
1119+
}
1120+
}
1121+
});
1122+
});
1123+
1124+
it("sets certificate to default", async () => {
1125+
await createNextComponent().default({
1126+
cloudfront: {
1127+
certificate: {
1128+
cloudFrontDefaultCertificate: true
1129+
}
1130+
}
1131+
});
1132+
});
11001133
});
11011134
});

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ class NextjsComponent extends Component {
241241
comment: cloudFrontComment,
242242
webACLId: cloudFrontWebACLId,
243243
restrictions: cloudFrontRestrictions,
244+
certificate: cloudFrontCertificate,
244245
...cloudFrontOtherInputs
245246
} = inputs.cloudfront || {};
246247

@@ -591,7 +592,8 @@ class NextjsComponent extends Component {
591592
}),
592593
comment: cloudFrontComment,
593594
webACLId: cloudFrontWebACLId,
594-
restrictions: cloudFrontRestrictions
595+
restrictions: cloudFrontRestrictions,
596+
certificate: cloudFrontCertificate
595597
});
596598

597599
let appUrl = cloudFrontOutputs.url;

0 commit comments

Comments
 (0)