From 99842fe2726cefd02d64d4739abddb5298de4f93 Mon Sep 17 00:00:00 2001 From: Ben Stickley <35735118+bestickley@users.noreply.github.com> Date: Tue, 28 Nov 2023 09:07:22 -0500 Subject: [PATCH] feat: extract NextjsDomain from NextjsDistribution (#174) * feat: NextjsDomain * docs: note changes * fix: make getHostedZone private * chore: self mutation Signed-off-by: github-actions * fix: certificate and nextjsPath on NextDistribution * chore: self mutation Signed-off-by: github-actions * fix: make NextjsDomain optional for NextjsDistribution * chore: self mutation Signed-off-by: github-actions * fix: allow overriding Distribution domainNames and certificate * chore: self mutation Signed-off-by: github-actions * feat: remove domainAliases, create dns records for alternateNames, add isWildcardCertificate prop to create wildcard certificate by default * chore: self mutation Signed-off-by: github-actions * docs: improve NextjsDomain TSDoc * chore: self mutation Signed-off-by: github-actions * docs: add Cloudflare example doc * chore: self mutation Signed-off-by: github-actions * fix: remove isWildcardCertificate * chore: self mutation Signed-off-by: github-actions * docs: improve docs * chore: self mutation Signed-off-by: github-actions --------- Signed-off-by: github-actions Co-authored-by: github-actions --- API.md | 418 +++++++++++++++++++++----------------- docs/major-changes.md | 1 + open-next | 2 +- src/Nextjs.ts | 39 ++-- src/NextjsBase.ts | 38 ---- src/NextjsDistribution.ts | 231 +-------------------- src/NextjsDomain.ts | 166 +++++++++++++++ src/index.ts | 7 +- 8 files changed, 432 insertions(+), 470 deletions(-) delete mode 100644 src/NextjsBase.ts create mode 100644 src/NextjsDomain.ts diff --git a/API.md b/API.md index 7bc95afd..aa6b5e87 100644 --- a/API.md +++ b/API.md @@ -215,6 +215,7 @@ Any object. | revalidation | NextjsRevalidation | Revalidation handler and queue. | | serverFunction | NextjsServer | The main NextJS server handler lambda function. | | staticAssets | NextjsStaticAssets | Asset deployment to S3. | +| domain | NextjsDomain | Optional Route53 Hosted Zone, ACM Certificate, and Route53 DNS Records. | --- @@ -346,6 +347,18 @@ Asset deployment to S3. --- +##### `domain`Optional + +```typescript +public readonly domain: NextjsDomain; +``` + +- *Type:* NextjsDomain + +Optional Route53 Hosted Zone, ACM Certificate, and Route53 DNS Records. + +--- + ### NextjsBucketDeployment @@ -784,11 +797,7 @@ Any object. | distributionDomain | string | The domain name of the internally created CloudFront Distribution. | | distributionId | string | The ID of the internally created CloudFront Distribution. | | url | string | The CloudFront URL of the website. | -| customDomainName | string | *No description.* | -| customDomainUrl | string | If the custom domain is enabled, this is the URL of the website with the custom domain. | | distribution | aws-cdk-lib.aws_cloudfront.Distribution | The internally created CloudFront `Distribution` instance. | -| certificate | aws-cdk-lib.aws_certificatemanager.ICertificate | The AWS Certificate Manager certificate for the custom domain. | -| hostedZone | aws-cdk-lib.aws_route53.IHostedZone | The Route 53 hosted zone for the custom domain. | --- @@ -864,41 +873,164 @@ The CloudFront URL of the website. --- -##### `customDomainName`Optional +##### `distribution`Required ```typescript -public readonly customDomainName: string; +public readonly distribution: Distribution; ``` -- *Type:* string +- *Type:* aws-cdk-lib.aws_cloudfront.Distribution + +The internally created CloudFront `Distribution` instance. --- -##### `customDomainUrl`Optional + +### NextjsDomain + +Use a custom domain with `Nextjs`. + +Requires a Route53 hosted zone to have been +created within the same AWS account. For DNS setups where you cannot use a +Route53 hosted zone in the same AWS account, use the `defaults.distribution.cdk.distribution` +prop of {@link NextjsProps}. + +See {@link NextjsDomainProps} TS Doc comments for detailed docs on how to customize. +This construct is helpful to user to not have to worry about interdependencies +between Route53 Hosted Zone, CloudFront Distribution, and Route53 Hosted Zone Records. + +Note, if you're using another service for domain name registration, you can +still create a Route53 hosted zone. Please see [Configuring DNS Delegation from +CloudFlare to AWS Route53](https://veducate.co.uk/dns-delegation-route53/) +as an example. + +#### Initializers ```typescript -public readonly customDomainUrl: string; +import { NextjsDomain } from 'cdk-nextjs-standalone' + +new NextjsDomain(scope: Construct, id: string, props: NextjsDomainProps) ``` +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| scope | constructs.Construct | *No description.* | +| id | string | *No description.* | +| props | NextjsDomainProps | *No description.* | + +--- + +##### `scope`Required + +- *Type:* constructs.Construct + +--- + +##### `id`Required + - *Type:* string -If the custom domain is enabled, this is the URL of the website with the custom domain. +--- + +##### `props`Required + +- *Type:* NextjsDomainProps --- -##### `distribution`Required +#### Methods + +| **Name** | **Description** | +| --- | --- | +| toString | Returns a string representation of this construct. | +| createDnsRecords | Creates DNS records (A and AAAA) records for {@link NextjsDomainProps.domainName} and {@link NextjsDomainProps.alternateNames} if defined. | + +--- + +##### `toString` ```typescript -public readonly distribution: Distribution; +public toString(): string ``` +Returns a string representation of this construct. + +##### `createDnsRecords` + +```typescript +public createDnsRecords(distribution: Distribution): void +``` + +Creates DNS records (A and AAAA) records for {@link NextjsDomainProps.domainName} and {@link NextjsDomainProps.alternateNames} if defined. + +###### `distribution`Required + - *Type:* aws-cdk-lib.aws_cloudfront.Distribution -The internally created CloudFront `Distribution` instance. +--- + +#### Static Functions + +| **Name** | **Description** | +| --- | --- | +| isConstruct | Checks if `x` is a construct. | + +--- + +##### ~~`isConstruct`~~ + +```typescript +import { NextjsDomain } from 'cdk-nextjs-standalone' + +NextjsDomain.isConstruct(x: any) +``` + +Checks if `x` is a construct. + +###### `x`Required + +- *Type:* any + +Any object. --- -##### `certificate`Optional +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| node | constructs.Node | The tree node. | +| domainNames | string[] | Concatentation of {@link NextjsDomainProps.domainName} and {@link NextjsDomainProps.alternateNames}. Used in instantiation of CloudFront Distribution in NextjsDistribution. | +| certificate | aws-cdk-lib.aws_certificatemanager.ICertificate | ACM Certificate. | +| hostedZone | aws-cdk-lib.aws_route53.IHostedZone | Route53 Hosted Zone. | + +--- + +##### `node`Required + +```typescript +public readonly node: Node; +``` + +- *Type:* constructs.Node + +The tree node. + +--- + +##### `domainNames`Required + +```typescript +public readonly domainNames: string[]; +``` + +- *Type:* string[] + +Concatentation of {@link NextjsDomainProps.domainName} and {@link NextjsDomainProps.alternateNames}. Used in instantiation of CloudFront Distribution in NextjsDistribution. + +--- + +##### `certificate`Required ```typescript public readonly certificate: ICertificate; @@ -906,11 +1038,11 @@ public readonly certificate: ICertificate; - *Type:* aws-cdk-lib.aws_certificatemanager.ICertificate -The AWS Certificate Manager certificate for the custom domain. +ACM Certificate. --- -##### `hostedZone`Optional +##### `hostedZone`Required ```typescript public readonly hostedZone: IHostedZone; @@ -918,7 +1050,7 @@ public readonly hostedZone: IHostedZone; - *Type:* aws-cdk-lib.aws_route53.IHostedZone -The Route 53 hosted zone for the custom domain. +Route53 Hosted Zone. --- @@ -2477,113 +2609,6 @@ Bucket containing assets. ## Structs -### BaseSiteDomainProps - -#### Initializer - -```typescript -import { BaseSiteDomainProps } from 'cdk-nextjs-standalone' - -const baseSiteDomainProps: BaseSiteDomainProps = { ... } -``` - -#### Properties - -| **Name** | **Type** | **Description** | -| --- | --- | --- | -| domainName | string | The domain to be assigned to the website URL (ie. domain.com). | -| alternateNames | string[] | Specify additional names that should route to the Cloudfront Distribution. | -| certificate | aws-cdk-lib.aws_certificatemanager.ICertificate | Import the certificate for the domain. | -| domainAlias | string | An alternative domain to be assigned to the website URL. | -| hostedZone | aws-cdk-lib.aws_route53.IHostedZone | Import the underlying Route 53 hosted zone. | -| isExternalDomain | boolean | Set this option if the domain is not hosted on Amazon Route 53. | - ---- - -##### `domainName`Required - -```typescript -public readonly domainName: string; -``` - -- *Type:* string - -The domain to be assigned to the website URL (ie. domain.com). - -Supports domains that are hosted either on [Route 53](https://aws.amazon.com/route53/) or externally. - ---- - -##### `alternateNames`Optional - -```typescript -public readonly alternateNames: string[]; -``` - -- *Type:* string[] - -Specify additional names that should route to the Cloudfront Distribution. - -Note, certificates for these names will not be automatically generated so the `certificate` option must be specified. - ---- - -##### `certificate`Optional - -```typescript -public readonly certificate: ICertificate; -``` - -- *Type:* aws-cdk-lib.aws_certificatemanager.ICertificate - -Import the certificate for the domain. - -By default, SST will create a certificate with the domain name. The certificate will be created in the `us-east-1`(N. Virginia) region as required by AWS CloudFront. - -Set this option if you have an existing certificate in the `us-east-1` region in AWS Certificate Manager you want to use. - ---- - -##### `domainAlias`Optional - -```typescript -public readonly domainAlias: string; -``` - -- *Type:* string - -An alternative domain to be assigned to the website URL. - -Visitors to the alias will be redirected to the main domain. (ie. `www.domain.com`). - -Use this to create a `www.` version of your domain and redirect visitors to the root domain. - ---- - -##### `hostedZone`Optional - -```typescript -public readonly hostedZone: IHostedZone; -``` - -- *Type:* aws-cdk-lib.aws_route53.IHostedZone - -Import the underlying Route 53 hosted zone. - ---- - -##### `isExternalDomain`Optional - -```typescript -public readonly isExternalDomain: boolean; -``` - -- *Type:* boolean - -Set this option if the domain is not hosted on Amazon Route 53. - ---- - ### NextjsBucketDeploymentProps #### Initializer @@ -3007,9 +3032,9 @@ const nextjsDistributionProps: NextjsDistributionProps = { ... } | basePath | string | *No description.* | | cachePolicies | NextjsCachePolicyProps | Override the default CloudFront cache policies created internally. | | cdk | NextjsDistributionCdkProps | Overrides for created CDK resources. | -| customDomain | string \| NextjsDomainProps | The customDomain for this website. Supports domains that are hosted either on [Route 53](https://aws.amazon.com/route53/) or externally. | | distribution | aws-cdk-lib.aws_cloudfront.Distribution | *No description.* | | functionUrlAuthType | aws-cdk-lib.aws_lambda.FunctionUrlAuthType | Override lambda function url auth type. | +| nextDomain | NextjsDomain | *No description.* | | originRequestPolicies | NextjsOriginRequestPolicyProps | Override the default CloudFront origin request policies created internally. | --- @@ -3116,38 +3141,6 @@ Overrides for created CDK resources. --- -##### `customDomain`Optional - -```typescript -public readonly customDomain: string | NextjsDomainProps; -``` - -- *Type:* string | NextjsDomainProps - -The customDomain for this website. Supports domains that are hosted either on [Route 53](https://aws.amazon.com/route53/) or externally. - -Note that you can also migrate externally hosted domains to Route 53 by -[following this guide](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/MigratingDNS.html). - ---- - -*Example* - -```typescript -new NextjsDistribution(this, "Dist", { - customDomain: "domain.com", -}); - -new NextjsDistribution(this, "Dist", { - customDomain: { - domainName: "domain.com", - domainAlias: "www.domain.com", - hostedZone: "domain.com" - }, -}); -``` - - ##### `distribution`Optional ```typescript @@ -3173,6 +3166,18 @@ Override lambda function url auth type. --- +##### `nextDomain`Optional + +```typescript +public readonly nextDomain: NextjsDomain; +``` + +- *Type:* NextjsDomain + +> [{@link NextjsDomain }]({@link NextjsDomain }) + +--- + ##### `originRequestPolicies`Optional ```typescript @@ -3199,12 +3204,11 @@ const nextjsDomainProps: NextjsDomainProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | -| domainName | string | The domain to be assigned to the website URL (ie. domain.com). | -| alternateNames | string[] | Specify additional names that should route to the Cloudfront Distribution. | -| certificate | aws-cdk-lib.aws_certificatemanager.ICertificate | Import the certificate for the domain. | -| domainAlias | string | An alternative domain to be assigned to the website URL. | -| hostedZone | aws-cdk-lib.aws_route53.IHostedZone | Import the underlying Route 53 hosted zone. | -| isExternalDomain | boolean | Set this option if the domain is not hosted on Amazon Route 53. | +| domainName | string | An easy to remember address of your website. | +| alternateNames | string[] | Alternate domain names that should route to the Cloudfront Distribution. | +| certificate | aws-cdk-lib.aws_certificatemanager.ICertificate | If this prop is `undefined` then an ACM `Certificate` will be created based on {@link NextjsDomainProps.domainName} with DNS Validation. This prop allows you to control the TLS/SSL certificate created. The certificate you create must be in the `us-east-1` (N. Virginia) region as required by AWS CloudFront. | +| certificateDomainName | string | The domain name used in this construct when creating an ACM `Certificate`. | +| hostedZone | aws-cdk-lib.aws_route53.IHostedZone | You must create the hosted zone out-of-band. | --- @@ -3216,12 +3220,22 @@ public readonly domainName: string; - *Type:* string -The domain to be assigned to the website URL (ie. domain.com). +An easy to remember address of your website. -Supports domains that are hosted either on [Route 53](https://aws.amazon.com/route53/) or externally. +Only supports domains hosted +on [Route 53](https://aws.amazon.com/route53/). Used as `domainName` for +ACM `Certificate` if {@link NextjsDomainProps.certificate} and +{@link NextjsDomainProps.certificateDomainName} are `undefined`. --- +*Example* + +```typescript +"example.com" +``` + + ##### `alternateNames`Optional ```typescript @@ -3230,12 +3244,33 @@ public readonly alternateNames: string[]; - *Type:* string[] -Specify additional names that should route to the Cloudfront Distribution. +Alternate domain names that should route to the Cloudfront Distribution. -Note, certificates for these names will not be automatically generated so the `certificate` option must be specified. +For example, if you specificied `"example.com"` as your {@link NextjsDomainProps.domainName}, +you could specify `["www.example.com", "api.example.com"]`. +Learn more about the [requirements](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/CNAMEs.html#alternate-domain-names-requirements) +and [restrictions](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/CNAMEs.html#alternate-domain-names-restrictions) +for using alternate domain names with CloudFront. + +Note, in order to use alternate domain names, they must be covered by your +certificate. By default, the certificate created in this construct only covers +the {@link NextjsDomainProps.domainName}. Therefore, you'll need to specify +a wildcard domain name like `"*.example.com"` with {@link NextjsDomainProps.certificateDomainName} +so that this construct will create the certificate the covers the alternate +domain names. Otherwise, you can use {@link NextjsDomainProps.certificate} +to create the certificate yourself where you'll need to ensure it has a +wildcard or uses subject alternative names including the +alternative names specified here. --- +*Example* + +```typescript +["www.example.com", "api.example.com"] +``` + + ##### `certificate`Optional ```typescript @@ -3244,27 +3279,28 @@ public readonly certificate: ICertificate; - *Type:* aws-cdk-lib.aws_certificatemanager.ICertificate -Import the certificate for the domain. - -By default, SST will create a certificate with the domain name. The certificate will be created in the `us-east-1`(N. Virginia) region as required by AWS CloudFront. +If this prop is `undefined` then an ACM `Certificate` will be created based on {@link NextjsDomainProps.domainName} with DNS Validation. This prop allows you to control the TLS/SSL certificate created. The certificate you create must be in the `us-east-1` (N. Virginia) region as required by AWS CloudFront. Set this option if you have an existing certificate in the `us-east-1` region in AWS Certificate Manager you want to use. --- -##### `domainAlias`Optional +##### `certificateDomainName`Optional ```typescript -public readonly domainAlias: string; +public readonly certificateDomainName: string; ``` - *Type:* string -An alternative domain to be assigned to the website URL. +The domain name used in this construct when creating an ACM `Certificate`. -Visitors to the alias will be redirected to the main domain. (ie. `www.domain.com`). +Useful +when passing {@link NextjsDomainProps.alternateNames} and you need to specify +a wildcard domain like "*.example.com". If `undefined`, then {@link NextjsDomainProps.domainName} +will be used. -Use this to create a `www.` version of your domain and redirect visitors to the root domain. +If {@link NextjsDomainProps.certificate} is passed, then this prop is ignored. --- @@ -3276,19 +3312,11 @@ public readonly hostedZone: IHostedZone; - *Type:* aws-cdk-lib.aws_route53.IHostedZone -Import the underlying Route 53 hosted zone. +You must create the hosted zone out-of-band. ---- - -##### `isExternalDomain`Optional - -```typescript -public readonly isExternalDomain: boolean; -``` - -- *Type:* boolean - -Set this option if the domain is not hosted on Amazon Route 53. +You can lookup the hosted zone outside this construct and pass it in via this prop. +Alternatively if this prop is `undefined`, then the hosted zone will be +**looked up** (not created) via `HostedZone.fromLookup` with {@link NextjsDomainProps.domainName}. --- @@ -3452,6 +3480,7 @@ const nextjsProps: NextjsProps = { ... } | buildPath | string | The directory to execute `npm run build` from. | | defaults | NextjsDefaultsProps | Allows you to override defaults for the resources created by this construct. | | distribution | aws-cdk-lib.aws_cloudfront.Distribution | Optional CloudFront Distribution created outside of this construct that will be used to add Next.js behaviors and origins onto. Useful with `basePath`. | +| domainProps | NextjsDomainProps | Props to configure {@link NextjsDomain}. | | environment | {[ key: string ]: string} | Custom environment variables to pass to the NextJS build **and** runtime. | | imageOptimizationBucket | aws-cdk-lib.aws_s3.IBucket | Optional S3 Bucket to use, defaults to assets bucket. | | quiet | boolean | Less build output. | @@ -3550,6 +3579,21 @@ Optional CloudFront Distribution created outside of this construct that will be --- +##### `domainProps`Optional + +```typescript +public readonly domainProps: NextjsDomainProps; +``` + +- *Type:* NextjsDomainProps + +Props to configure {@link NextjsDomain}. + +See details on how to customize at +{@link NextjsDomainProps} + +--- + ##### `environment`Optional ```typescript diff --git a/docs/major-changes.md b/docs/major-changes.md index c7336db8..c436e9ea 100644 --- a/docs/major-changes.md +++ b/docs/major-changes.md @@ -21,6 +21,7 @@ - Remove `NextjsBaseProps` to simplify props - Remove `projectRoot` as it's not being used - Remove `tempBuildDir` as it's not being used +- Create `NextjsDomain`. Remove custom domain related props from `NextjsDistribution`. ## v3 diff --git a/open-next b/open-next index 34c78cbc..8bc075b9 160000 --- a/open-next +++ b/open-next @@ -1 +1 @@ -Subproject commit 34c78cbc7466e8f74ba06b41066fcc0969045b96 +Subproject commit 8bc075b9696857d60ffe520154b8395c3a730b00 diff --git a/src/Nextjs.ts b/src/Nextjs.ts index 4711503e..3432fcb7 100644 --- a/src/Nextjs.ts +++ b/src/Nextjs.ts @@ -3,17 +3,15 @@ import * as lambda from 'aws-cdk-lib/aws-lambda'; import { FunctionOptions } from 'aws-cdk-lib/aws-lambda'; import * as s3 from 'aws-cdk-lib/aws-s3'; import { Construct } from 'constructs'; -import { BaseSiteDomainProps } from './NextjsBase'; import { NextjsBuild } from './NextjsBuild'; import { NextjsDistribution, NextjsDistributionProps } from './NextjsDistribution'; +import { NextjsDomain, NextjsDomainProps } from './NextjsDomain'; import { NextjsImage } from './NextjsImage'; import { NextjsInvalidation } from './NextjsInvalidation'; import { NextjsRevalidation } from './NextjsRevalidation'; import { NextjsServer } from './NextjsServer'; import { NextjsStaticAssets, NextjsStaticAssetsProps } from './NextjsStaticAssets'; -export interface NextjsDomainProps extends BaseSiteDomainProps {} - /** * Defaults for created resources. * Why `any`? see https://github.com/aws/jsii/issues/2901 @@ -69,6 +67,11 @@ export interface NextjsProps { * be used to add Next.js behaviors and origins onto. Useful with `basePath`. */ readonly distribution?: Distribution; + /** + * Props to configure {@link NextjsDomain}. See details on how to customize at + * {@link NextjsDomainProps} + */ + readonly domainProps?: NextjsDomainProps; /** * Custom environment variables to pass to the NextJS build **and** runtime. */ @@ -119,27 +122,26 @@ export class Nextjs extends Construct { * The main NextJS server handler lambda function. */ public serverFunction: NextjsServer; - /** * The image optimization handler lambda function. */ public imageOptimizationFunction: NextjsImage; - /** * Built NextJS project output. */ public nextBuild: NextjsBuild; - /** * Asset deployment to S3. */ public staticAssets: NextjsStaticAssets; - + /** + * Optional Route53 Hosted Zone, ACM Certificate, and Route53 DNS Records + */ + public domain?: NextjsDomain; /** * CloudFront distribution. */ public distribution: NextjsDistribution; - /** * Revalidation handler and queue. */ @@ -156,41 +158,48 @@ export class Nextjs extends Construct { // deploy nextjs static assets to s3 this.staticAssets = new NextjsStaticAssets(this, 'StaticAssets', { + basePath: props.basePath, bucket: props.defaults?.assetDeployment?.bucket, environment: props.environment, nextBuild: this.nextBuild, - basePath: props.basePath, }); this.serverFunction = new NextjsServer(this, 'Server', { - ...props, nextBuild: this.nextBuild, lambda: props.defaults?.lambda, staticAssetBucket: this.staticAssets.bucket, }); // build image optimization this.imageOptimizationFunction = new NextjsImage(this, 'ImgOptFn', { - ...props, - nextBuild: this.nextBuild, bucket: props.imageOptimizationBucket || this.bucket, lambdaOptions: props.defaults?.lambda, + nextBuild: this.nextBuild, }); // build revalidation queue and handler function this.revalidation = new NextjsRevalidation(this, 'Revalidation', { - ...props, + lambdaOptions: props.defaults?.lambda, nextBuild: this.nextBuild, serverFunction: this.serverFunction, }); + if (this.props.domainProps) { + this.domain = new NextjsDomain(this, 'Domain', this.props.domainProps); + } this.distribution = new NextjsDistribution(this, 'Distribution', { - ...props, + nextjsPath: props.nextjsPath, + basePath: props.basePath, + distribution: props.distribution, ...props.defaults?.distribution, staticAssetsBucket: this.staticAssets.bucket, nextBuild: this.nextBuild, + nextDomain: this.domain, serverFunction: this.serverFunction.lambdaFunction, imageOptFunction: this.imageOptimizationFunction, }); + if (this.domain) { + this.domain.createDnsRecords(this.distribution.distribution); + } if (!this.props.skipFullInvalidation) { new NextjsInvalidation(this, 'Invalidation', { @@ -204,7 +213,7 @@ export class Nextjs extends Construct { * URL of Next.js App. */ public get url(): string { - const customDomain = this.distribution.customDomainName; + const customDomain = this.props.domainProps?.domainName; return customDomain ? `https://${customDomain}` : this.distribution.url; } diff --git a/src/NextjsBase.ts b/src/NextjsBase.ts deleted file mode 100644 index 4661c8d0..00000000 --- a/src/NextjsBase.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager'; -import { IHostedZone } from 'aws-cdk-lib/aws-route53'; - -///// stuff below taken from https://github.com/serverless-stack/sst/blob/8d377e941467ced81d8cc31ee67d5a06550f04d4/packages/resources/src/BaseSite.ts - -export interface BaseSiteDomainProps { - /** - * The domain to be assigned to the website URL (ie. domain.com). - * - * Supports domains that are hosted either on [Route 53](https://aws.amazon.com/route53/) or externally. - */ - readonly domainName: string; - /** - * An alternative domain to be assigned to the website URL. Visitors to the alias will be redirected to the main domain. (ie. `www.domain.com`). - * - * Use this to create a `www.` version of your domain and redirect visitors to the root domain. - */ - readonly domainAlias?: string; - /** - * Specify additional names that should route to the Cloudfront Distribution. Note, certificates for these names will not be automatically generated so the `certificate` option must be specified. - */ - readonly alternateNames?: string[]; - /** - * Set this option if the domain is not hosted on Amazon Route 53. - */ - readonly isExternalDomain?: boolean; - - /** - * Import the underlying Route 53 hosted zone. - */ - readonly hostedZone?: IHostedZone; - /** - * Import the certificate for the domain. By default, SST will create a certificate with the domain name. The certificate will be created in the `us-east-1`(N. Virginia) region as required by AWS CloudFront. - * - * Set this option if you have an existing certificate in the `us-east-1` region in AWS Certificate Manager you want to use. - */ - readonly certificate?: ICertificate; -} diff --git a/src/NextjsDistribution.ts b/src/NextjsDistribution.ts index d0418f0f..28dfbfb7 100644 --- a/src/NextjsDistribution.ts +++ b/src/NextjsDistribution.ts @@ -1,24 +1,18 @@ import * as fs from 'node:fs'; import * as path from 'path'; import { Duration, Fn, RemovalPolicy } from 'aws-cdk-lib'; -import * as acm from 'aws-cdk-lib/aws-certificatemanager'; import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; import { Distribution, ResponseHeadersPolicy } from 'aws-cdk-lib/aws-cloudfront'; import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'; import { PolicyStatement, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import { Runtime } from 'aws-cdk-lib/aws-lambda'; -import * as route53 from 'aws-cdk-lib/aws-route53'; -import * as route53Patterns from 'aws-cdk-lib/aws-route53-patterns'; -import * as route53Targets from 'aws-cdk-lib/aws-route53-targets'; import * as s3 from 'aws-cdk-lib/aws-s3'; import { Construct } from 'constructs'; import { DEFAULT_STATIC_MAX_AGE, NEXTJS_BUILD_DIR, NEXTJS_STATIC_DIR } from './constants'; import { NextjsProps } from './Nextjs'; -import { BaseSiteDomainProps } from './NextjsBase'; import { NextjsBuild } from './NextjsBuild'; - -export interface NextjsDomainProps extends BaseSiteDomainProps {} +import { NextjsDomain } from './NextjsDomain'; export type NextjsDistributionCdkOverrideProps = cloudfront.DistributionProps; @@ -61,27 +55,6 @@ export interface NextjsDistributionProps { * Overrides for created CDK resources. */ readonly cdk?: NextjsDistributionCdkProps; - /** - * The customDomain for this website. Supports domains that are hosted - * either on [Route 53](https://aws.amazon.com/route53/) or externally. - * - * Note that you can also migrate externally hosted domains to Route 53 by - * [following this guide](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/MigratingDNS.html). - * - * @example - * new NextjsDistribution(this, "Dist", { - * customDomain: "domain.com", - * }); - * - * new NextjsDistribution(this, "Dist", { - * customDomain: { - * domainName: "domain.com", - * domainAlias: "www.domain.com", - * hostedZone: "domain.com" - * }, - * }); - */ - readonly customDomain?: string | NextjsDomainProps; /** * @see {@link NextjsProps.distribution} */ @@ -100,6 +73,10 @@ export interface NextjsDistributionProps { * @see {@link NextjsBuild} */ readonly nextBuild: NextjsBuild; + /** + * @see {@link NextjsDomain} + */ + readonly nextDomain?: NextjsDomain; /** * @see {@link NextjsProps.nextjsPath} */ @@ -160,7 +137,7 @@ export class NextjsDistribution extends Construct { comment: 'Nextjs Image Default Cache Policy', }; - protected props: NextjsDistributionProps; + private props: NextjsDistributionProps; ///////////////////// // Public Properties @@ -169,14 +146,6 @@ export class NextjsDistribution extends Construct { * The internally created CloudFront `Distribution` instance. */ public distribution: Distribution; - /** - * The Route 53 hosted zone for the custom domain. - */ - hostedZone?: route53.IHostedZone; - /** - * The AWS Certificate Manager certificate for the custom domain. - */ - certificate?: acm.ICertificate; private commonBehaviorOptions: Pick = { viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, @@ -198,11 +167,6 @@ export class NextjsDistribution extends Construct { this.props = props; - // Create Custom Domain - this.validateCustomDomainSettings(); - this.hostedZone = this.lookupHostedZone(); - this.certificate = this.createCertificate(); - // Create Behaviors this.s3Origin = new origins.S3Origin(this.props.staticAssetsBucket); this.staticBehaviorOptions = this.createStaticBehaviorOptions(); @@ -216,9 +180,6 @@ export class NextjsDistribution extends Construct { this.distribution = this.getCloudFrontDistribution(); this.addStaticBehaviorsToDistribution(); this.addRootPathBehavior(); - - // Connect Custom Domain to CloudFront Distribution - this.createRoute53Records(); } /** @@ -228,29 +189,6 @@ export class NextjsDistribution extends Construct { return `https://${this.distribution.distributionDomainName}`; } - get customDomainName(): string | undefined { - const { customDomain } = this.props; - - if (!customDomain) { - return; - } - - if (typeof customDomain === 'string') { - return customDomain; - } - - return customDomain.domainName; - } - - /** - * If the custom domain is enabled, this is the URL of the website with the - * custom domain. - */ - public get customDomainUrl(): string | undefined { - const customDomainName = this.customDomainName; - return customDomainName ? `https://${customDomainName}` : undefined; - } - /** * The ID of the internally created CloudFront Distribution. */ @@ -436,20 +374,17 @@ export class NextjsDistribution extends Construct { * create a CloudFront Distribution if one is passed in by user. */ private createCloudFrontDistribution(cfDistributionProps?: NextjsDistributionCdkOverrideProps) { - // build domainNames - const domainNames = this.buildDistributionDomainNames(); - return new cloudfront.Distribution(this, 'Distribution', { // defaultRootObject: "index.html", defaultRootObject: '', minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021, + domainNames: this.props.nextDomain?.domainNames, + certificate: this.props.nextDomain?.certificate, // Override props. ...cfDistributionProps, // these values can NOT be overwritten by cfDistributionProps - domainNames, - certificate: this.certificate, defaultBehavior: this.serverBehaviorOptions, }); } @@ -509,154 +444,4 @@ export class NextjsDistribution extends Construct { return pathPattern; } - - private buildDistributionDomainNames(): string[] { - const customDomain = - typeof this.props.customDomain === 'string' ? this.props.customDomain : this.props.customDomain?.domainName; - - const alternateNames = - typeof this.props.customDomain === 'string' ? [] : this.props.customDomain?.alternateNames || []; - - return customDomain ? [customDomain, ...alternateNames] : []; - } - - ///////////////////// - // Custom Domain - ///////////////////// - - protected validateCustomDomainSettings() { - const { customDomain } = this.props; - - if (!customDomain) { - return; - } - - if (typeof customDomain === 'string') { - return; - } - - if (customDomain.isExternalDomain === true) { - if (!customDomain.certificate) { - throw new Error('A valid certificate is required when "isExternalDomain" is set to "true".'); - } - if (customDomain.domainAlias) { - throw new Error( - 'Domain alias is only supported for domains hosted on Amazon Route 53. Do not set the "customDomain.domainAlias" when "isExternalDomain" is enabled.' - ); - } - if (customDomain.hostedZone) { - throw new Error( - 'Hosted zones can only be configured for domains hosted on Amazon Route 53. Do not set the "customDomain.hostedZone" when "isExternalDomain" is enabled.' - ); - } - } - } - - protected lookupHostedZone(): route53.IHostedZone | undefined { - const { customDomain } = this.props; - - // Skip if customDomain is not configured - if (!customDomain) { - return; - } - - let hostedZone; - - if (typeof customDomain === 'string') { - hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', { - domainName: customDomain, - }); - } else if (typeof customDomain.hostedZone === 'string') { - hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', { - domainName: customDomain.hostedZone, - }); - } else if (customDomain.hostedZone) { - hostedZone = customDomain.hostedZone; - } else if (typeof customDomain.domainName === 'string') { - // Skip if domain is not a Route53 domain - if (customDomain.isExternalDomain === true) { - return; - } - - hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', { - domainName: customDomain.domainName, - }); - } else { - hostedZone = customDomain.hostedZone; - } - - return hostedZone; - } - - private createCertificate(): acm.ICertificate | undefined { - const { customDomain } = this.props; - - if (!customDomain) { - return; - } - - let acmCertificate; - - // HostedZone is set for Route 53 domains - if (this.hostedZone) { - if (typeof customDomain === 'string') { - acmCertificate = new acm.DnsValidatedCertificate(this, 'Certificate', { - domainName: customDomain, - hostedZone: this.hostedZone, - region: 'us-east-1', - }); - } else if (customDomain.certificate) { - acmCertificate = customDomain.certificate; - } else { - acmCertificate = new acm.DnsValidatedCertificate(this, 'Certificate', { - domainName: customDomain.domainName, - hostedZone: this.hostedZone, - region: 'us-east-1', - }); - } - } - // HostedZone is NOT set for non-Route 53 domains - else { - if (typeof customDomain !== 'string') { - acmCertificate = customDomain.certificate; - } - } - - return acmCertificate; - } - - private createRoute53Records(): void { - const { customDomain } = this.props; - - if (!customDomain || !this.hostedZone) { - return; - } - - let recordName; - let domainAlias; - if (typeof customDomain === 'string') { - recordName = customDomain; - } else { - recordName = customDomain.domainName; - domainAlias = customDomain.domainAlias; - } - - // Create DNS record - const recordProps = { - recordName, - zone: this.hostedZone, - target: route53.RecordTarget.fromAlias(new route53Targets.CloudFrontTarget(this.distribution)), - }; - new route53.ARecord(this, 'AliasRecord', recordProps); - new route53.AaaaRecord(this, 'AliasRecordAAAA', recordProps); - - // Create Alias redirect record - if (domainAlias) { - new route53Patterns.HttpsRedirect(this, 'Redirect', { - zone: this.hostedZone, - recordNames: [domainAlias], - targetDomain: recordName, - }); - } - } } diff --git a/src/NextjsDomain.ts b/src/NextjsDomain.ts new file mode 100644 index 00000000..5bafc9b9 --- /dev/null +++ b/src/NextjsDomain.ts @@ -0,0 +1,166 @@ +import { ICertificate, Certificate, CertificateValidation } from 'aws-cdk-lib/aws-certificatemanager'; +import { Distribution } from 'aws-cdk-lib/aws-cloudfront'; +import { + ARecord, + ARecordProps, + AaaaRecord, + AaaaRecordProps, + HostedZone, + IHostedZone, + RecordTarget, +} from 'aws-cdk-lib/aws-route53'; +import { CloudFrontTarget } from 'aws-cdk-lib/aws-route53-targets'; +import { Construct } from 'constructs'; +import { NextjsProps } from '.'; + +export interface NextjsDomainProps { + /** + * An easy to remember address of your website. Only supports domains hosted + * on [Route 53](https://aws.amazon.com/route53/). Used as `domainName` for + * ACM `Certificate` if {@link NextjsDomainProps.certificate} and + * {@link NextjsDomainProps.certificateDomainName} are `undefined`. + * @example "example.com" + */ + readonly domainName: string; + /** + * Alternate domain names that should route to the Cloudfront Distribution. + * For example, if you specificied `"example.com"` as your {@link NextjsDomainProps.domainName}, + * you could specify `["www.example.com", "api.example.com"]`. + * Learn more about the [requirements](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/CNAMEs.html#alternate-domain-names-requirements) + * and [restrictions](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/CNAMEs.html#alternate-domain-names-restrictions) + * for using alternate domain names with CloudFront. + * + * Note, in order to use alternate domain names, they must be covered by your + * certificate. By default, the certificate created in this construct only covers + * the {@link NextjsDomainProps.domainName}. Therefore, you'll need to specify + * a wildcard domain name like `"*.example.com"` with {@link NextjsDomainProps.certificateDomainName} + * so that this construct will create the certificate the covers the alternate + * domain names. Otherwise, you can use {@link NextjsDomainProps.certificate} + * to create the certificate yourself where you'll need to ensure it has a + * wildcard or uses subject alternative names including the + * alternative names specified here. + * @example ["www.example.com", "api.example.com"] + */ + readonly alternateNames?: string[]; + /** + * You must create the hosted zone out-of-band. + * You can lookup the hosted zone outside this construct and pass it in via this prop. + * Alternatively if this prop is `undefined`, then the hosted zone will be + * **looked up** (not created) via `HostedZone.fromLookup` with {@link NextjsDomainProps.domainName}. + */ + readonly hostedZone?: IHostedZone; + /** + * If this prop is `undefined` then an ACM `Certificate` will be created based on {@link NextjsDomainProps.domainName} + * with DNS Validation. This prop allows you to control the TLS/SSL + * certificate created. The certificate you create must be in the `us-east-1` + * (N. Virginia) region as required by AWS CloudFront. + * + * Set this option if you have an existing certificate in the `us-east-1` region in AWS Certificate Manager you want to use. + */ + readonly certificate?: ICertificate; + /** + * The domain name used in this construct when creating an ACM `Certificate`. Useful + * when passing {@link NextjsDomainProps.alternateNames} and you need to specify + * a wildcard domain like "*.example.com". If `undefined`, then {@link NextjsDomainProps.domainName} + * will be used. + * + * If {@link NextjsDomainProps.certificate} is passed, then this prop is ignored. + */ + readonly certificateDomainName?: string; +} + +/** + * Use a custom domain with `Nextjs`. Requires a Route53 hosted zone to have been + * created within the same AWS account. For DNS setups where you cannot use a + * Route53 hosted zone in the same AWS account, use the `defaults.distribution.cdk.distribution` + * prop of {@link NextjsProps}. + * + * See {@link NextjsDomainProps} TS Doc comments for detailed docs on how to customize. + * This construct is helpful to user to not have to worry about interdependencies + * between Route53 Hosted Zone, CloudFront Distribution, and Route53 Hosted Zone Records. + * + * Note, if you're using another service for domain name registration, you can + * still create a Route53 hosted zone. Please see [Configuring DNS Delegation from + * CloudFlare to AWS Route53](https://veducate.co.uk/dns-delegation-route53/) + * as an example. + */ +export class NextjsDomain extends Construct { + /** + * Concatentation of {@link NextjsDomainProps.domainName} and {@link NextjsDomainProps.alternateNames}. + * Used in instantiation of CloudFront Distribution in NextjsDistribution + */ + get domainNames(): string[] { + const names = [this.props.domainName]; + if (this.props.alternateNames?.length) { + names.push(...this.props.alternateNames); + } + return names; + } + /** + * Route53 Hosted Zone. + */ + hostedZone: IHostedZone; + /** + * ACM Certificate. + */ + certificate: ICertificate; + + private props: NextjsDomainProps; + + constructor(scope: Construct, id: string, props: NextjsDomainProps) { + super(scope, id); + this.props = props; + this.hostedZone = this.getHostedZone(); + this.certificate = this.getCertificate(); + } + + private getHostedZone(): IHostedZone { + if (!this.props.hostedZone) { + return HostedZone.fromLookup(this, 'HostedZone', { + domainName: this.props.domainName, + }); + } else { + return this.props.hostedZone; + } + } + + private getCertificate(): ICertificate { + if (!this.props.certificate) { + return new Certificate(this, 'Certificate', { + domainName: this.props.certificateDomainName ?? this.props.domainName, + validation: CertificateValidation.fromDns(this.hostedZone), + }); + } else { + return this.props.certificate; + } + } + + /** + * Creates DNS records (A and AAAA) records for {@link NextjsDomainProps.domainName} + * and {@link NextjsDomainProps.alternateNames} if defined. + */ + createDnsRecords(distribution: Distribution): void { + // Create DNS record + const recordProps: ARecordProps & AaaaRecordProps = { + recordName: this.props.domainName, + zone: this.hostedZone, + target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)), + }; + new ARecord(this, 'ARecordMain', recordProps); // IPv4 + new AaaaRecord(this, 'AaaaRecordMain', recordProps); // IPv6 + if (this.props.alternateNames?.length) { + let i = 1; + for (const alternateName of this.props.alternateNames) { + new ARecord(this, 'ARecordAlt' + i, { + ...recordProps, + recordName: `${alternateName}.`, + }); + new AaaaRecord(this, 'AaaaRecordAlt' + i, { + ...recordProps, + recordName: `${alternateName}.`, + }); + i++; + } + } + } +} diff --git a/src/index.ts b/src/index.ts index b5914291..074b065a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,3 @@ -export { BaseSiteDomainProps } from './NextjsBase'; - -// L2 constructs export { NextjsStaticAssets, NextjsStaticAssetsProps } from './NextjsStaticAssets'; export { NextjsRevalidation, NextjsRevalidationProps } from './NextjsRevalidation'; export { NextjsBuild, NextjsBuildProps } from './NextjsBuild'; @@ -12,11 +9,9 @@ export { NextjsDistributionCdkProps, NextjsDistributionCdkOverrideProps, NextjsDistributionProps, - NextjsDomainProps, NextjsCachePolicyProps, NextjsOriginRequestPolicyProps, } from './NextjsDistribution'; export { NextjsInvalidation, NextjsInvalidationProps } from './NextjsInvalidation'; - -// L3 constructs +export { NextjsDomain, NextjsDomainProps } from './NextjsDomain'; export { Nextjs, NextjsProps, NextjsDefaultsProps } from './Nextjs';