diff --git a/src/index.ts b/src/index.ts index b1dd722..231b859 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,18 +17,37 @@ export interface AliasTarget { readonly ttl?: cdk.Duration; } +/** + * @param zoneName The DNS name of the zone to create + * @param existingPublicZone An existing public zone to use instead of creating a new one + * @param existingPrivateZone An existing private zone to use instead of creating a new one + * @param disallowPrivateZone Override the default behavior of creating a private zone. Will also block adding private records. + * @param includeCertificate Whether to create an ACM certificate for the zone + * @param certAlternateNames Alternate names to include in the certificate + * @param privateZoneVpcs VPCs to associate with the private zone + * @param targets Targets to create A records for + */ export interface ISplitHorizonDnsProps { readonly zoneName: string; + readonly existingPublicZone?: route53.HostedZone; + readonly existingPrivateZone?: route53.HostedZone; + readonly disallowPrivateZone?: boolean; readonly certAlternateNames?: Array; - readonly privateZoneVpcs: Array; + readonly privateZoneVpcs?: Array; readonly targets: Array; readonly includeCertificate?: boolean; } +/** + * Creates a public and private zone for a given domain name, and creates A records for the given targets. + * @property publicZone The public zone created + * @property privateZone The private zone created + * @property records The A records created + */ export class SplitHorizonDns extends Construct { public publicZone: route53.HostedZone; - public privateZone: route53.HostedZone; + public privateZone?: route53.HostedZone; public records: Array; @@ -37,15 +56,22 @@ export class SplitHorizonDns extends Construct { const { zoneName, + existingPublicZone, + existingPrivateZone, + disallowPrivateZone, includeCertificate, certAlternateNames, privateZoneVpcs, targets, } = props; - this.publicZone = new route53.HostedZone(this, 'PublicZone', { - zoneName: zoneName, - }); + if (existingPublicZone) { + this.publicZone = existingPublicZone; + } else { + this.publicZone = new route53.HostedZone(this, 'PublicZone', { + zoneName: zoneName, + }); + } if (includeCertificate) { new acm.Certificate(this, 'Certificate', { @@ -54,10 +80,18 @@ export class SplitHorizonDns extends Construct { }); } - this.privateZone = new route53.HostedZone(this, 'PrivateZone', { - zoneName: zoneName, - vpcs: privateZoneVpcs, - }); + if (disallowPrivateZone) { + console.log('Private zone creation is disallowed. Skipping...'); + } else if (disallowPrivateZone && existingPrivateZone) { + console.error('Private zone creation is disallowed, but an existing private zone was provided. Skipping...'); + } else if (existingPrivateZone) { + this.privateZone = existingPrivateZone; + } else { + this.privateZone = new route53.HostedZone(this, 'PrivateZone', { + zoneName: zoneName, + vpcs: privateZoneVpcs, + }); + } this.records = targets.reduce((accu: Array, curr: AliasTarget) => { let target; @@ -83,12 +117,16 @@ export class SplitHorizonDns extends Construct { records.push(publicARecord); } - if (curr.private) { + if (disallowPrivateZone) { + console.log('Private zone creation is disallowed. Skipping...'); + } else if (curr.private && this.privateZone) { const privateARecord = new route53.ARecord(this, `${curr.target.toString()}PrivateARecord`, { zone: this.privateZone, target: target, }); records.push(privateARecord); + } else if (curr.private && !this.privateZone) { + console.error(`Private zone was specified for ${curr}, but private zone was not created. Omitting...`); } accu.push(records); return accu; diff --git a/test/split-horizon-dns.test.ts b/test/split-horizon-dns.test.ts index c449266..8af20d1 100644 --- a/test/split-horizon-dns.test.ts +++ b/test/split-horizon-dns.test.ts @@ -3,6 +3,7 @@ import * as cdk from 'aws-cdk-lib'; import { Duration } from 'aws-cdk-lib'; import { Match, Template } from 'aws-cdk-lib/assertions'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as route53 from 'aws-cdk-lib/aws-route53'; import * as targets from 'aws-cdk-lib/aws-route53-targets'; import * as s3 from 'aws-cdk-lib/aws-s3'; import { SplitHorizonDns, AliasTarget } from '../src/index'; @@ -341,4 +342,111 @@ describe('split horizon', () => { TTL: '3600', }); }); + + it('omits the private zone if disallowed', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'TestStack'); + new SplitHorizonDns(stack, 'MostBasicTestConstruct', { + zoneName: 'example.com', + disallowPrivateZone: true, + targets: [], + }); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Route53::HostedZone', Match.not({ + Name: Match.anyValue(), + VPCs: Match.arrayWith([ + Match.objectLike({ + VPCId: Match.anyValue(), + }), + ]), + })); + + template.hasResourceProperties('AWS::Route53::HostedZone', { + Name: Match.anyValue(), + }); + }); + + // this test is tricky. as there is no observable property of the record + it.skip('doesnt create private records if disallowed', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'TestStack'); + + const firstTarget: AliasTarget = { + target: [googleDns], + private: true, + public: true, + }; + + new SplitHorizonDns(stack, 'MostBasicTestConstruct', { + zoneName: 'example.com', + disallowPrivateZone: true, + targets: [firstTarget], + }); + + // thing.records.forEach((record) => { + // console.log(record); + // }); + }); + + it('can receive an existing public zone', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'TestStack'); + const existingZone = new route53.PublicHostedZone(stack, 'ExistingPublicZone', { + zoneName: 'example.com', + }); + + const firstTarget: AliasTarget = { + target: [googleDns], + public: true, + }; + + new SplitHorizonDns(stack, 'MostBasicTestConstruct', { + zoneName: 'example.com', + existingPublicZone: existingZone, + targets: [firstTarget], + }); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Route53::RecordSet', { + Name: Match.stringLikeRegexp(`${exampleDomain}\.`), + Type: 'A', + HostedZoneId: Match.objectLike({ + Ref: Match.stringLikeRegexp('ExistingPublicZone'), + }), + }); + }); + + it('can receive an existing private zone', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'TestStack'); + const vpc = new ec2.Vpc(stack, 'myvpc'); + const existingZone = new route53.PrivateHostedZone(stack, 'ExistingPrivateZone', { + zoneName: 'example.com', + vpc, + }); + + const firstTarget: AliasTarget = { + target: [googleDns], + private: true, + }; + + new SplitHorizonDns(stack, 'MostBasicTestConstruct', { + zoneName: 'example.com', + existingPrivateZone: existingZone, + targets: [firstTarget], + }); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Route53::RecordSet', { + Name: Match.stringLikeRegexp(`${exampleDomain}\.`), + Type: 'A', + HostedZoneId: Match.objectLike({ + Ref: Match.stringLikeRegexp('ExistingPrivateZone'), + }), + }); + }); });