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

Commit e74a212

Browse files
committed
✨ feat(domains): rework how domains are passed, allow for many, auto-guess zone (#73, #79)
Allow for passing multiple domains. Get domains from CLI, automatically select the most fitting one. BREAKING CHANGE: Changed CLI interface, different way to pass domains #73, #79
1 parent 137c2a5 commit e74a212

File tree

11 files changed

+182
-116
lines changed

11 files changed

+182
-116
lines changed

lib/cdk/config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,21 @@ const RawEnvConfig = cleanEnv(process.env, {
2222
LAMBDA_RUNTIME: str({ default: RuntimeEnum.NODEJS_16_X, choices: Object.values(RuntimeEnum) }),
2323
IMAGE_LAMBDA_TIMEOUT: num({ default: IMAGE_LAMBDA_DEFAULT_TIMEOUT }),
2424
IMAGE_LAMBDA_MEMORY: num({ default: IMAGE_LAMBDA_DEFAULT_MEMORY }),
25-
HOSTED_ZONE: str({ default: undefined }),
26-
DNS_PREFIX: str({ default: undefined }),
2725
CUSTOM_API_DOMAIN: str({ default: undefined }),
2826
REDIRECT_FROM_APEX: bool({ default: false }),
27+
DOMAIN_NAMES: str({ default: undefined }),
28+
PROFILE: str({ default: undefined }),
2929
})
3030

3131
export const envConfig = {
32+
profile: RawEnvConfig.PROFILE,
3233
stackName: RawEnvConfig.STACK_NAME,
3334
lambdaMemory: RawEnvConfig.LAMBDA_MEMORY,
3435
lambdaTimeout: RawEnvConfig.LAMBDA_TIMEOUT,
3536
lambdaRuntime: runtimeMap[RawEnvConfig.LAMBDA_RUNTIME],
3637
imageLambdaMemory: RawEnvConfig.IMAGE_LAMBDA_MEMORY,
3738
imageLambdaTimeout: RawEnvConfig.IMAGE_LAMBDA_TIMEOUT,
38-
hostedZone: RawEnvConfig.HOSTED_ZONE,
39-
dnsPrefix: RawEnvConfig.DNS_PREFIX,
4039
customApiDomain: RawEnvConfig.CUSTOM_API_DOMAIN,
4140
redirectFromApex: RawEnvConfig.REDIRECT_FROM_APEX,
41+
domainNames: RawEnvConfig.DOMAIN_NAMES ? RawEnvConfig.DOMAIN_NAMES.split(',').map((a) => a.trim()) : [],
4242
}

lib/cdk/stack.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@ import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager'
44
import { IDistribution } from 'aws-cdk-lib/aws-cloudfront'
55
import { HttpOrigin } from 'aws-cdk-lib/aws-cloudfront-origins'
66
import { Function } from 'aws-cdk-lib/aws-lambda'
7-
import { HostedZone, IHostedZone } from 'aws-cdk-lib/aws-route53'
87
import { Bucket } from 'aws-cdk-lib/aws-s3'
9-
import { CustomStackProps } from './types'
8+
import { CustomStackProps, MappedDomain } from './types'
109
import { setupApiGateway, SetupApiGwProps } from './utils/apiGw'
1110
import { setupCfnCertificate, SetupCfnCertificateProps } from './utils/cfnCertificate'
1211
import { setupCfnDistro, SetupCfnDistroProps } from './utils/cfnDistro'
13-
import { setupDnsRecords, SetupDnsRecordsProps } from './utils/dnsRecords'
12+
import { PrepareDomainProps, prepareDomains, setupDnsRecords, SetupDnsRecordsProps } from './utils/dnsRecords'
1413
import { setupImageLambda, SetupImageLambdaProps } from './utils/imageLambda'
1514
import { setupApexRedirect, SetupApexRedirectProps } from './utils/redirect'
1615
import { setupAssetsBucket, UploadAssetsProps, uploadStaticAssets } from './utils/s3'
@@ -23,22 +22,17 @@ export class NextStandaloneStack extends Stack {
2322
assetsBucket?: Bucket
2423
cfnDistro?: IDistribution
2524
cfnCertificate?: ICertificate
26-
hostedZone?: IHostedZone
27-
domainName?: string
25+
domains: MappedDomain[]
2826

2927
constructor(scope: App, id: string, config: CustomStackProps) {
3028
super(scope, id, config)
3129

3230
console.log("CDK's config:", config)
3331

34-
if (config.hostedZone) {
35-
this.hostedZone = HostedZone.fromLookup(this, 'HostedZone_certificate', { domainName: config.hostedZone })
36-
this.domainName = config.dnsPrefix ? `${config.dnsPrefix}.${config.hostedZone}` : config.hostedZone
32+
if (!!config.customApiDomain && config.domainNames.length > 1) {
33+
throw new Error('Cannot use Apex redirect with multiple domains')
3734
}
3835

39-
console.log('Hosted zone:', this.hostedZone?.zoneName)
40-
console.log('Normalized domain name:', this.domainName)
41-
4236
this.assetsBucket = this.setupAssetsBucket()
4337

4438
this.imageLambda = this.setupImageLambda({
@@ -68,10 +62,16 @@ export class NextStandaloneStack extends Stack {
6862
serverBasePath: config.apigwServerPath,
6963
})
7064

71-
if (!!this.hostedZone && !!this.domainName) {
65+
if (config.domainNames.length > 0) {
66+
this.domains = this.prepareDomains({
67+
domains: config.domainNames,
68+
profile: config.awsProfile,
69+
})
70+
}
71+
72+
if (this.domains.length > 0) {
7273
this.cfnCertificate = this.setupCfnCertificate({
73-
hostedZone: this.hostedZone,
74-
domainName: this.domainName,
74+
domains: this.domains,
7575
})
7676
}
7777

@@ -80,7 +80,7 @@ export class NextStandaloneStack extends Stack {
8080
apiGateway: this.apiGateway,
8181
imageBasePath: config.apigwImagePath,
8282
serverBasePath: config.apigwServerPath,
83-
domainName: this.domainName,
83+
domains: this.domains,
8484
certificate: this.cfnCertificate,
8585
customApiOrigin: config.customApiDomain ? new HttpOrigin(config.customApiDomain) : undefined,
8686
})
@@ -91,22 +91,24 @@ export class NextStandaloneStack extends Stack {
9191
cfnDistribution: this.cfnDistro,
9292
})
9393

94-
if (!!this.hostedZone && !!this.domainName) {
94+
if (this.domains.length > 0) {
9595
this.setupDnsRecords({
9696
cfnDistro: this.cfnDistro,
97-
hostedZone: this.hostedZone,
98-
dnsPrefix: config.dnsPrefix,
97+
domains: this.domains,
9998
})
10099

101100
if (!!config.redirectFromApex) {
102101
this.setupApexRedirect({
103-
sourceHostedZone: this.hostedZone,
104-
targetDomain: this.domainName,
102+
domain: this.domains[0],
105103
})
106104
}
107105
}
108106
}
109107

108+
prepareDomains(props: PrepareDomainProps) {
109+
return prepareDomains(this, props)
110+
}
111+
110112
setupAssetsBucket() {
111113
return setupAssetsBucket(this)
112114
}

lib/cdk/types.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { StackProps } from 'aws-cdk-lib'
22
import { Runtime } from 'aws-cdk-lib/aws-lambda'
3+
import { IHostedZone } from 'aws-cdk-lib/aws-route53'
34

45
export interface CustomStackProps extends StackProps {
56
apigwServerPath: string
@@ -17,8 +18,14 @@ export interface CustomStackProps extends StackProps {
1718
lambdaRuntime: Runtime
1819
imageLambdaTimeout?: number
1920
imageLambdaMemory?: number
20-
hostedZone?: string
21-
dnsPrefix?: string
21+
domainNames: string[]
22+
redirectFromApex: boolean
23+
awsProfile?: string
2224
customApiDomain?: string
23-
redirectFromApex?: boolean
25+
}
26+
27+
export interface MappedDomain {
28+
recordName: string
29+
domain: string
30+
zone: IHostedZone
2431
}

lib/cdk/utils/cfnCertificate.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
import { CfnOutput, Stack } from 'aws-cdk-lib'
2-
import { DnsValidatedCertificate } from 'aws-cdk-lib/aws-certificatemanager'
3-
import { IHostedZone } from 'aws-cdk-lib/aws-route53'
2+
import { Certificate, CertificateValidation } from 'aws-cdk-lib/aws-certificatemanager'
3+
import { MappedDomain } from '../types'
44

55
export interface SetupCfnCertificateProps {
6-
hostedZone: IHostedZone
7-
domainName: string
6+
domains: MappedDomain[]
87
}
98

10-
export const setupCfnCertificate = (scope: Stack, { hostedZone, domainName }: SetupCfnCertificateProps) => {
9+
export const setupCfnCertificate = (scope: Stack, { domains }: SetupCfnCertificateProps) => {
10+
const [firstDomain, ...otherDomains] = domains
11+
1112
// us-east-1 is needed for Cloudfront to accept certificate.
12-
const certificate = new DnsValidatedCertificate(scope, 'Certificate', { domainName, hostedZone, region: 'us-east-1' })
13+
// https://github.com/aws/aws-cdk/issues/8934
14+
const multiZoneMap = domains.reduce((acc, curr) => ({ ...acc, [curr.domain]: curr.zone }), {})
15+
16+
const certificate = new Certificate(scope, 'Certificate', {
17+
domainName: firstDomain.domain,
18+
19+
subjectAlternativeNames: otherDomains.map((a) => a.domain),
20+
validation: CertificateValidation.fromDnsMultiZone(multiZoneMap),
21+
})
1322

1423
new CfnOutput(scope, 'certificateArn', { value: certificate.certificateArn })
1524

lib/cdk/utils/cfnDistro.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import {
1414
} from 'aws-cdk-lib/aws-cloudfront'
1515
import { HttpOrigin, S3Origin } from 'aws-cdk-lib/aws-cloudfront-origins'
1616
import { Bucket } from 'aws-cdk-lib/aws-s3'
17+
import { MappedDomain } from '../types'
1718

1819
export interface SetupCfnDistroProps {
19-
domainName?: string
20+
domains: MappedDomain[]
2021
certificate?: ICertificate
2122
apiGateway: HttpApi
2223
imageBasePath: string
@@ -27,7 +28,7 @@ export interface SetupCfnDistroProps {
2728

2829
export const setupCfnDistro = (
2930
scope: Stack,
30-
{ apiGateway, imageBasePath, serverBasePath, assetsBucket, domainName, certificate, customApiOrigin }: SetupCfnDistroProps,
31+
{ apiGateway, imageBasePath, serverBasePath, assetsBucket, domains, certificate, customApiOrigin }: SetupCfnDistroProps,
3132
) => {
3233
const apiGwDomainName = `${apiGateway.apiId}.execute-api.${scope.region}.amazonaws.com`
3334

@@ -75,7 +76,7 @@ export const setupCfnDistro = (
7576
comment: `CloudFront distribution for ${scope.stackName}`,
7677
enableIpv6: true,
7778
priceClass: PriceClass.PRICE_CLASS_100,
78-
domainNames: domainName ? [domainName] : undefined,
79+
domainNames: domains.length > 0 ? domains.map((a) => a.domain) : undefined,
7980
certificate,
8081
defaultBehavior: {
8182
origin: serverOrigin,

lib/cdk/utils/dnsRecords.ts

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,74 @@
11
import { CfnOutput, Stack } from 'aws-cdk-lib'
22
import { IDistribution } from 'aws-cdk-lib/aws-cloudfront'
3-
import { AaaaRecord, ARecord, IHostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53'
3+
import { AaaaRecord, ARecord, HostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53'
44
import { CloudFrontTarget } from 'aws-cdk-lib/aws-route53-targets'
5+
import { execSync } from 'child_process'
6+
import { MappedDomain } from '../types'
7+
import { readFileSync } from 'fs'
8+
import { tmpdir } from 'os'
9+
import path from 'path'
10+
11+
export interface PrepareDomainProps {
12+
domains: string[]
13+
profile?: string
14+
}
515

616
export interface SetupDnsRecordsProps {
7-
dnsPrefix?: string
8-
hostedZone: IHostedZone
17+
domains: MappedDomain[]
918
cfnDistro: IDistribution
1019
}
1120

12-
export const setupDnsRecords = (scope: Stack, { dnsPrefix: recordName, hostedZone: zone, cfnDistro }: SetupDnsRecordsProps) => {
21+
// AWS-CDK does not have a way to retrieve the hosted zones in given account, so we need to go around.
22+
const getAvailableHostedZones = (profile?: string): string[] => {
23+
const tmpDir = path.join(tmpdir(), 'hosted-zones.json')
24+
const profileFlag = profile ? `--profile ${profile}` : ''
25+
execSync(`aws route53 list-hosted-zones --output json ${profileFlag} > ${tmpDir}`)
26+
const output = JSON.parse(readFileSync(tmpDir, 'utf8'))
27+
return output.HostedZones.map((zone: any) => zone.Name)
28+
}
29+
30+
const matchDomainToHostedZone = (domainToMatch: string, zones: string[]) => {
31+
const matchedZone = zones.reduce((acc, curr) => {
32+
const matchRegex = new RegExp(`(.*)${curr}$`)
33+
34+
const isMatching = !!`${domainToMatch}.`.match(matchRegex)
35+
const isMoreSpecific = curr.split('.').length > (acc?.split('.').length ?? 0)
36+
37+
if (isMatching && isMoreSpecific) {
38+
return curr
39+
} else {
40+
return acc
41+
}
42+
}, null as string | null)
43+
44+
if (!matchedZone) {
45+
throw new Error(`No hosted zone found for domain: ${domainToMatch}`)
46+
}
47+
48+
return matchedZone.replace('/.$/', '')
49+
}
50+
51+
export const prepareDomains = (scope: Stack, { domains, profile }: PrepareDomainProps): MappedDomain[] => {
52+
const zones = getAvailableHostedZones(profile)
53+
54+
return domains.map((domain, index) => {
55+
const hostedZone = matchDomainToHostedZone(domain, zones)
56+
const recordName = domain.replace(hostedZone, '')
57+
58+
const zone = HostedZone.fromLookup(scope, `Zone_${index}`, { domainName: hostedZone })
59+
60+
return { zone, recordName, domain }
61+
})
62+
}
63+
64+
export const setupDnsRecords = (scope: Stack, { domains, cfnDistro }: SetupDnsRecordsProps) => {
1365
const target = RecordTarget.fromAlias(new CloudFrontTarget(cfnDistro))
1466

15-
const dnsARecord = new ARecord(scope, 'AAliasRecord', { recordName, target, zone })
16-
const dnsAaaaRecord = new AaaaRecord(scope, 'AaaaAliasRecord', { recordName, target, zone })
67+
domains.forEach(({ recordName, zone }, index) => {
68+
const dnsARecord = new ARecord(scope, `AAliasRecord_${index}`, { recordName, target, zone })
69+
const dnsAaaaRecord = new AaaaRecord(scope, `AaaaAliasRecord_${index}`, { recordName, target, zone })
1770

18-
new CfnOutput(scope, 'dns_A_Record', { value: dnsARecord.domainName })
19-
new CfnOutput(scope, 'dns_AAAA_Record', { value: dnsAaaaRecord.domainName })
71+
new CfnOutput(scope, `dns_A_Record_${index}`, { value: dnsARecord.domainName })
72+
new CfnOutput(scope, `dns_AAAA_Record_${index}`, { value: dnsAaaaRecord.domainName })
73+
})
2074
}

lib/cdk/utils/redirect.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import { CfnOutput, Stack } from 'aws-cdk-lib'
2-
import { IHostedZone } from 'aws-cdk-lib/aws-route53'
32
import { HttpsRedirect } from 'aws-cdk-lib/aws-route53-patterns'
3+
import { MappedDomain } from '../types'
44

55
export interface SetupApexRedirectProps {
6-
sourceHostedZone: IHostedZone
7-
targetDomain: string
6+
domain: MappedDomain
87
}
98

10-
export const setupApexRedirect = (scope: Stack, { sourceHostedZone, targetDomain }: SetupApexRedirectProps) => {
9+
export const setupApexRedirect = (scope: Stack, { domain }: SetupApexRedirectProps) => {
1110
new HttpsRedirect(scope, `ApexRedirect`, {
1211
// Currently supports only apex (root) domain.
13-
zone: sourceHostedZone,
14-
targetDomain,
12+
zone: domain.zone,
13+
targetDomain: domain.recordName,
1514
})
1615

17-
new CfnOutput(scope, 'RedirectFrom', { value: sourceHostedZone.zoneName })
16+
new CfnOutput(scope, 'RedirectFrom', { value: domain.zone.zoneName })
1817
}

lib/cli.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ program
6060
.option('--imageLambdaTimeout <sec>', 'Set timeout for lambda function handling image optimization.', Number, IMAGE_LAMBDA_DEFAULT_TIMEOUT)
6161
.option('--imageLambdaMemory <mb>', 'Set memory for lambda function handling image optimization.', Number, IMAGE_LAMBDA_DEFAULT_MEMORY)
6262
.option('--lambdaRuntime <runtime>', "Specify version of NodeJS to use as Lambda's runtime. Options: node14, node16, node18.", 'node16')
63-
.option('--hostedZone <domainName>', 'Hosted zone domain name to be used for creating DNS records (example: example.com).', undefined)
64-
.option('--domainNamePrefix <prefix>', 'Prefix for creating DNS records, if left undefined, hostedZone will be used (example: app).', undefined)
63+
.option('--domains <domainList>', 'Comma-separated list of domains to use. (example: mydomain.com,mydonain.au,other.domain.com)', undefined)
6564
.option('--customApiDomain <domain>', 'Domain to forward the requests to /api routes, by default API routes will be handled by the server lambda.', undefined)
6665
.option('--redirectFromApex', 'Redirect from apex domain to specified address.', false)
6766
.option('--profile <name>', 'AWS profile to use with CDK.', undefined)
@@ -78,10 +77,9 @@ program
7877
lambdaRuntime,
7978
imageLambdaMemory,
8079
imageLambdaTimeout,
81-
hostedZone,
82-
domainNamePrefix,
8380
customApiDomain,
8481
redirectFromApex,
82+
domains,
8583
hotswap,
8684
profile,
8785
} = options
@@ -97,10 +95,9 @@ program
9795
lambdaRuntime,
9896
imageLambdaMemory,
9997
imageLambdaTimeout,
100-
hostedZone,
101-
domainNamePrefix,
10298
customApiDomain,
10399
redirectFromApex,
100+
domains,
104101
hotswap,
105102
profile,
106103
}),

lib/cli/deploy.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ interface Props {
1111
imageLambdaMemory?: number
1212
imageLambdaTimeout?: number
1313
customApiDomain?: string
14-
hostedZone?: string
15-
domainNamePrefix?: string
14+
domains?: string
1615
redirectFromApex?: boolean
1716
profile?: string
1817
hotswap: boolean
@@ -30,8 +29,7 @@ export const deployHandler = async ({
3029
lambdaRuntime,
3130
imageLambdaMemory,
3231
imageLambdaTimeout,
33-
domainNamePrefix,
34-
hostedZone,
32+
domains,
3533
customApiDomain,
3634
redirectFromApex,
3735
hotswap,
@@ -58,8 +56,7 @@ export const deployHandler = async ({
5856
...(lambdaRuntime && { LAMBDA_RUNTIME: lambdaRuntime.toString() }),
5957
...(imageLambdaMemory && { IMAGE_LAMBDA_MEMORY: imageLambdaMemory.toString() }),
6058
...(imageLambdaTimeout && { IMAGE_LAMBDA_TIMEOUT: imageLambdaTimeout.toString() }),
61-
...(hostedZone && { HOSTED_ZONE: hostedZone }),
62-
...(domainNamePrefix && { DNS_PREFIX: domainNamePrefix }),
59+
...(domains && { DOMAINS: domains }),
6360
...(customApiDomain && { CUSTOM_API_DOMAIN: customApiDomain }),
6461
...(redirectFromApex && { REDIRECT_FROM_APEX: redirectFromApex.toString() }),
6562
}

0 commit comments

Comments
 (0)