packages/aws-cdk-lib/aws-route53/lib/record-set.ts (445 lines of code) (raw):
import { Construct } from 'constructs';
import { AliasRecordTargetConfig, IAliasRecordTarget } from './alias-record-target';
import { GeoLocation } from './geo-location';
import { IHealthCheck } from './health-check';
import { IHostedZone } from './hosted-zone-ref';
import { CfnRecordSet } from './route53.generated';
import { determineFullyQualifiedDomainName } from './util';
import * as iam from '../../aws-iam';
import { CustomResource, Duration, IResource, Names, RemovalPolicy, Resource, Token } from '../../core';
import { ValidationError } from '../../core/lib/errors';
import { addConstructMetadata } from '../../core/lib/metadata-resource';
import { CrossAccountZoneDelegationProvider } from '../../custom-resource-handlers/dist/aws-route53/cross-account-zone-delegation-provider.generated';
import { DeleteExistingRecordSetProvider } from '../../custom-resource-handlers/dist/aws-route53/delete-existing-record-set-provider.generated';
const CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE = 'Custom::CrossAccountZoneDelegation';
const DELETE_EXISTING_RECORD_SET_RESOURCE_TYPE = 'Custom::DeleteExistingRecordSet';
/**
* A record set
*/
export interface IRecordSet extends IResource {
/**
* The domain name of the record
*/
readonly domainName: string;
}
/**
* The record type.
*/
export enum RecordType {
/**
* route traffic to a resource, such as a web server, using an IPv4 address in dotted decimal
* notation
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#AFormat
*/
A = 'A',
/**
* route traffic to a resource, such as a web server, using an IPv6 address in colon-separated
* hexadecimal format
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#AAAAFormat
*/
AAAA = 'AAAA',
/**
* A CAA record specifies which certificate authorities (CAs) are allowed to issue certificates
* for a domain or subdomain
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#CAAFormat
*/
CAA = 'CAA',
/**
* A CNAME record maps DNS queries for the name of the current record, such as acme.example.com,
* to another domain (example.com or example.net) or subdomain (acme.example.com or zenith.example.org).
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#CNAMEFormat
*/
CNAME = 'CNAME',
/**
* A delegation signer (DS) record refers a zone key for a delegated subdomain zone.
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#DSFormat
*/
DS = 'DS',
/**
* An HTTPS resource record is a form of the Service Binding (SVCB) DNS record that provides extended configuration information,
* enabling a client to easily and securely connect to a service with an HTTP protocol.
* The configuration information is provided in parameters that allow the connection in one DNS query, rather than necessitating multiple DNS queries.
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#HTTPSFormat
*/
HTTPS = 'HTTPS',
/**
* An MX record specifies the names of your mail servers and, if you have two or more mail servers,
* the priority order.
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#MXFormat
*/
MX = 'MX',
/**
* A Name Authority Pointer (NAPTR) is a type of record that is used by Dynamic Delegation Discovery
* System (DDDS) applications to convert one value to another or to replace one value with another.
* For example, one common use is to convert phone numbers into SIP URIs.
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#NAPTRFormat
*/
NAPTR = 'NAPTR',
/**
* An NS record identifies the name servers for the hosted zone
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#NSFormat
*/
NS = 'NS',
/**
* A PTR record maps an IP address to the corresponding domain name.
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#PTRFormat
*/
PTR = 'PTR',
/**
* A start of authority (SOA) record provides information about a domain and the corresponding Amazon
* Route 53 hosted zone
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#SOAFormat
*/
SOA = 'SOA',
/**
* SPF records were formerly used to verify the identity of the sender of email messages.
* Instead of an SPF record, we recommend that you create a TXT record that contains the applicable value.
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#SPFFormat
*/
SPF = 'SPF',
/**
* An SRV record Value element consists of four space-separated values. The first three values are
* decimal numbers representing priority, weight, and port. The fourth value is a domain name.
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#SRVFormat
*/
SRV = 'SRV',
/**
* A Secure Shell fingerprint record (SSHFP) identifies SSH keys associated with the domain name.
* SSHFP records must be secured with DNSSEC for a chain of trust to be established.
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#SSHFPFormat
*/
SSHFP = 'SSHFP',
/**
* You use an SVCB record to deliver configuration information for accessing service endpoints.
* The SVCB is a generic DNS record and can be used to negotiate parameters for a variety of application protocols.
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#SVCBFormat
*/
SVCB = 'SVCB',
/**
* You use a TLSA record to use DNS-Based Authentication of Named Entities (DANE).
* A TLSA record associates a certificate/public key with a Transport Layer Security (TLS) endpoint, and clients can validate the certificate/public key using a TLSA record signed with DNSSEC.
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#TLSAFormat
*/
TLSA = 'TLSA',
/**
* A TXT record contains one or more strings that are enclosed in double quotation marks (").
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#TXTFormat
*/
TXT = 'TXT',
}
/**
* Options for a RecordSet.
*/
export interface RecordSetOptions {
/**
* The hosted zone in which to define the new record.
*/
readonly zone: IHostedZone;
/**
* The geographical origin for this record to return DNS records based on the user's location.
*/
readonly geoLocation?: GeoLocation;
/**
* The subdomain name for this record. This should be relative to the zone root name.
*
* For example, if you want to create a record for acme.example.com, specify
* "acme".
*
* You can also specify the fully qualified domain name which terminates with a
* ".". For example, "acme.example.com.".
*
* @default zone root
*/
readonly recordName?: string;
/**
* The resource record cache time to live (TTL).
*
* @default Duration.minutes(30)
*/
readonly ttl?: Duration;
/**
* A comment to add on the record.
*
* @default no comment
*/
readonly comment?: string;
/**
* Whether to delete the same record set in the hosted zone if it already exists (dangerous!)
*
* This allows to deploy a new record set while minimizing the downtime because the
* new record set will be created immediately after the existing one is deleted. It
* also avoids "manual" actions to delete existing record sets.
*
* > **N.B.:** this feature is dangerous, use with caution! It can only be used safely when
* > `deleteExisting` is set to `true` as soon as the resource is added to the stack. Changing
* > an existing Record Set's `deleteExisting` property from `false -> true` after deployment
* > will delete the record!
*
* @default false
*/
readonly deleteExisting?: boolean;
/**
* Among resource record sets that have the same combination of DNS name and type,
* a value that determines the proportion of DNS queries that Amazon Route 53 responds to using the current resource record set.
*
* Route 53 calculates the sum of the weights for the resource record sets that have the same combination of DNS name and type.
* Route 53 then responds to queries based on the ratio of a resource's weight to the total.
*
* This value can be a number between 0 and 255.
*
* @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-weighted.html
*
* @default - Do not set weighted routing
*/
readonly weight?: number;
/**
* The Amazon EC2 Region where you created the resource that this resource record set refers to.
* The resource typically is an AWS resource, such as an EC2 instance or an ELB load balancer,
* and is referred to by an IP address or a DNS domain name, depending on the record type.
*
* When Amazon Route 53 receives a DNS query for a domain name and type for which you have created latency resource record sets,
* Route 53 selects the latency resource record set that has the lowest latency between the end user and the associated Amazon EC2 Region.
* Route 53 then returns the value that is associated with the selected resource record set.
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53-recordset.html#cfn-route53-recordset-region
*
* @default - Do not set latency based routing
*/
readonly region?: string;
/**
* Whether to return multiple values, such as IP addresses for your web servers, in response to DNS queries.
*
* @default false
*/
readonly multiValueAnswer?: boolean;
/**
* A string used to distinguish between different records with the same combination of DNS name and type.
* It can only be set when either weight or geoLocation is defined.
*
* This parameter must be between 1 and 128 characters in length.
*
* @default - Auto generated string
*/
readonly setIdentifier?: string;
/**
* The health check to associate with the record set.
*
* Route53 will return this record set in response to DNS queries only if the health check is passing.
*
* @default - No health check configured
*/
readonly healthCheck?: IHealthCheck;
}
/**
* Type union for a record that accepts multiple types of target.
*/
export class RecordTarget {
/**
* Use string values as target.
*/
public static fromValues(...values: string[]) {
return new RecordTarget(values);
}
/**
* Use an alias as target.
*/
public static fromAlias(aliasTarget: IAliasRecordTarget) {
return new RecordTarget(undefined, aliasTarget);
}
/**
* Use ip addresses as target.
*/
public static fromIpAddresses(...ipAddresses: string[]) {
return RecordTarget.fromValues(...ipAddresses);
}
/**
*
* @param values correspond with the chosen record type (e.g. for 'A' Type, specify one or more IP addresses)
* @param aliasTarget alias for targets such as CloudFront distribution to route traffic to
*/
protected constructor(public readonly values?: string[], public readonly aliasTarget?: IAliasRecordTarget) {
}
}
/**
* Construction properties for a RecordSet.
*/
export interface RecordSetProps extends RecordSetOptions {
/**
* The record type.
*/
readonly recordType: RecordType;
/**
* The target for this record, either `RecordTarget.fromValues()` or
* `RecordTarget.fromAlias()`.
*/
readonly target: RecordTarget;
}
/**
* A record set.
*/
export class RecordSet extends Resource implements IRecordSet {
public readonly domainName: string;
private readonly geoLocation?: GeoLocation;
private readonly weight?: number;
private readonly region?: string;
private readonly multiValueAnswer?: boolean;
constructor(scope: Construct, id: string, props: RecordSetProps) {
super(scope, id);
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);
if (props.weight && !Token.isUnresolved(props.weight) && (props.weight < 0 || props.weight > 255)) {
throw new ValidationError(`weight must be between 0 and 255 inclusive, got: ${props.weight}`, this);
}
if (props.setIdentifier && (props.setIdentifier.length < 1 || props.setIdentifier.length > 128)) {
throw new ValidationError(`setIdentifier must be between 1 and 128 characters long, got: ${props.setIdentifier.length}`, this);
}
if (props.setIdentifier && props.weight === undefined && !props.geoLocation && !props.region && !props.multiValueAnswer) {
throw new ValidationError('setIdentifier can only be specified for non-simple routing policies', this);
}
if (props.multiValueAnswer && props.target.aliasTarget) {
throw new ValidationError('multiValueAnswer cannot be specified for alias record', this);
}
const nonSimpleRoutingPolicies = [
props.geoLocation,
props.region,
props.weight,
props.multiValueAnswer,
].filter((variable) => variable !== undefined).length;
if (nonSimpleRoutingPolicies > 1) {
throw new ValidationError('Only one of region, weight, multiValueAnswer or geoLocation can be defined', this);
}
this.geoLocation = props.geoLocation;
this.weight = props.weight;
this.region = props.region;
this.multiValueAnswer = props.multiValueAnswer;
const ttl = props.target.aliasTarget ? undefined : ((props.ttl && props.ttl.toSeconds()) ?? 1800).toString();
const recordName = determineFullyQualifiedDomainName(props.recordName || props.zone.zoneName, props.zone);
const recordSet = new CfnRecordSet(this, 'Resource', {
hostedZoneId: props.zone.hostedZoneId,
name: recordName,
type: props.recordType,
resourceRecords: props.target.values,
aliasTarget: props.target.aliasTarget && props.target.aliasTarget.bind(this, props.zone),
ttl,
comment: props.comment,
geoLocation: props.geoLocation ? {
continentCode: props.geoLocation.continentCode,
countryCode: props.geoLocation.countryCode,
subdivisionCode: props.geoLocation.subdivisionCode,
} : undefined,
multiValueAnswer: props.multiValueAnswer,
setIdentifier: props.setIdentifier ?? this.configureSetIdentifier(),
weight: props.weight,
region: props.region,
healthCheckId: props.healthCheck?.healthCheckId,
});
this.domainName = recordSet.ref;
if (props.deleteExisting) {
// Delete existing record before creating the new one
const provider = DeleteExistingRecordSetProvider.getOrCreateProvider(this, DELETE_EXISTING_RECORD_SET_RESOURCE_TYPE, {
policyStatements: [{ // IAM permissions for all providers
Effect: 'Allow',
Action: 'route53:GetChange',
Resource: '*',
}],
});
// Add to the singleton policy for this specific provider
provider.addToRolePolicy({
Effect: 'Allow',
Action: 'route53:ListResourceRecordSets',
Resource: props.zone.hostedZoneArn,
});
provider.addToRolePolicy({
Effect: 'Allow',
Action: 'route53:ChangeResourceRecordSets',
Resource: props.zone.hostedZoneArn,
Condition: {
'ForAllValues:StringEquals': {
'route53:ChangeResourceRecordSetsRecordTypes': [props.recordType],
'route53:ChangeResourceRecordSetsActions': ['DELETE'],
},
},
});
const customResource = new CustomResource(this, 'DeleteExistingRecordSetCustomResource', {
resourceType: DELETE_EXISTING_RECORD_SET_RESOURCE_TYPE,
serviceToken: provider.serviceToken,
properties: {
HostedZoneId: props.zone.hostedZoneId,
RecordName: recordName,
RecordType: props.recordType,
},
});
recordSet.node.addDependency(customResource);
}
}
private configureSetIdentifier(): string | undefined {
if (this.geoLocation) {
let identifier = 'GEO';
if (this.geoLocation.continentCode) {
identifier = identifier.concat('_CONTINENT_', this.geoLocation.continentCode);
}
if (this.geoLocation.countryCode) {
identifier = identifier.concat('_COUNTRY_', this.geoLocation.countryCode);
}
if (this.geoLocation.subdivisionCode) {
identifier = identifier.concat('_SUBDIVISION_', this.geoLocation.subdivisionCode);
}
return identifier;
}
if (this.weight !== undefined) {
if (Token.isUnresolved(this.weight)) {
const replacement = 'XXX'; // XXX simply because 255 is the highest value for a record weight
const idPrefix = `WEIGHT_${replacement}_ID_`;
const idTemplate = this.createIdentifier(idPrefix);
return idTemplate.replace(replacement, Token.asString(this.weight));
} else {
const idPrefix = `WEIGHT_${this.weight}_ID_`;
return this.createIdentifier(idPrefix);
}
}
if (this.region) {
const idPrefix= `REGION_${this.region}_ID_`;
return this.createIdentifier(idPrefix);
}
if (this.multiValueAnswer) {
const idPrefix = 'MVA_ID_';
return this.createIdentifier(idPrefix);
}
return undefined;
}
private createIdentifier(prefix: string): string {
return `${prefix}${Names.uniqueResourceName(this, { maxLength: 64 - prefix.length })}`;
}
}
/**
* Target for a DNS A Record
*
* @deprecated Use RecordTarget
*/
export class AddressRecordTarget extends RecordTarget {
}
/**
* Construction properties for a ARecord.
*/
export interface ARecordProps extends RecordSetOptions {
/**
* The target.
*/
readonly target: RecordTarget;
}
/**
* Construction properties to import existing ARecord as target.
*/
export interface ARecordAttrs extends RecordSetOptions{
/**
* Existing A record DNS name to set RecordTarget
*/
readonly targetDNS: string;
}
/**
* A DNS A record
*
* @resource AWS::Route53::RecordSet
*/
export class ARecord extends RecordSet {
/**
* Creates new A record of type alias with target set to an existing A Record DNS.
* Use when the target A record is created outside of CDK
* For records created as part of CDK use @aws-cdk-lib/aws-route53-targets/route53-record.ts
* @param scope the parent Construct for this Construct
* @param id Logical Id of the resource
* @param attrs the ARecordAttributes (Target Arecord DNS name and HostedZone)
* @returns AWS::Route53::RecordSet of type A with target alias set to existing A record
*/
public static fromARecordAttributes(scope: Construct, id: string, attrs: ARecordAttrs): ARecord {
const aliasTarget = RecordTarget.fromAlias(new ARecordAsAliasTarget(attrs));
return new ARecord(scope, id, {
...attrs,
target: aliasTarget,
});
}
constructor(scope: Construct, id: string, props: ARecordProps) {
super(scope, id, {
...props,
recordType: RecordType.A,
target: props.target,
});
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);
}
}
/**
* Converts the type of a given ARecord DNS name, created outside CDK, to an AliasRecordTarget
*/
class ARecordAsAliasTarget implements IAliasRecordTarget {
constructor(private readonly aRrecordAttrs: ARecordAttrs) {
}
public bind(record: IRecordSet, zone?: IHostedZone | undefined): AliasRecordTargetConfig {
if (!zone) {
throw new ValidationError('Cannot bind to record without a zone', record);
}
return {
dnsName: this.aRrecordAttrs.targetDNS,
hostedZoneId: this.aRrecordAttrs.zone.hostedZoneId,
};
}
}
/**
* Construction properties for a AaaaRecord.
*/
export interface AaaaRecordProps extends RecordSetOptions {
/**
* The target.
*/
readonly target: RecordTarget;
}
/**
* A DNS AAAA record
*
* @resource AWS::Route53::RecordSet
*/
export class AaaaRecord extends RecordSet {
constructor(scope: Construct, id: string, props: AaaaRecordProps) {
super(scope, id, {
...props,
recordType: RecordType.AAAA,
target: props.target,
});
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);
}
}
/**
* Construction properties for a CnameRecord.
*/
export interface CnameRecordProps extends RecordSetOptions {
/**
* The domain name of the target that this record should point to.
*/
readonly domainName: string;
}
/**
* A DNS CNAME record
*
* @resource AWS::Route53::RecordSet
*/
export class CnameRecord extends RecordSet {
constructor(scope: Construct, id: string, props: CnameRecordProps) {
super(scope, id, {
...props,
recordType: RecordType.CNAME,
target: RecordTarget.fromValues(props.domainName),
});
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);
}
}
/**
* Construction properties for a TxtRecord.
*/
export interface TxtRecordProps extends RecordSetOptions {
/**
* The text values.
*/
readonly values: string[];
}
/**
* A DNS TXT record
*
* @resource AWS::Route53::RecordSet
*/
export class TxtRecord extends RecordSet {
constructor(scope: Construct, id: string, props: TxtRecordProps) {
super(scope, id, {
...props,
recordType: RecordType.TXT,
target: RecordTarget.fromValues(...props.values.map(v => formatTxt(v))),
});
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);
}
}
/**
* Formats a text value for use in a TXT record
*
* Use `JSON.stringify` to correctly escape and enclose in double quotes ("").
*
* DNS TXT records can contain up to 255 characters in a single string. TXT
* record strings over 255 characters must be split into multiple text strings
* within the same record.
*
* @see https://aws.amazon.com/premiumsupport/knowledge-center/route53-resolve-dkim-text-record-error/
*/
function formatTxt(string: string): string {
const result = [];
let idx = 0;
while (idx < string.length) {
result.push(string.slice(idx, idx += 255)); // chunks of 255 characters long
}
return result.map(r => JSON.stringify(r)).join('');
}
/**
* Properties for a SRV record value.
*/
export interface SrvRecordValue {
/**
* The priority.
*/
readonly priority: number;
/**
* The weight.
*/
readonly weight: number;
/**
* The port.
*/
readonly port: number;
/**
* The server host name.
*/
readonly hostName: string;
}
/**
* Construction properties for a SrvRecord.
*/
export interface SrvRecordProps extends RecordSetOptions {
/**
* The values.
*/
readonly values: SrvRecordValue[];
}
/**
* A DNS SRV record
*
* @resource AWS::Route53::RecordSet
*/
export class SrvRecord extends RecordSet {
constructor(scope: Construct, id: string, props: SrvRecordProps) {
super(scope, id, {
...props,
recordType: RecordType.SRV,
target: RecordTarget.fromValues(...props.values.map(v => `${v.priority} ${v.weight} ${v.port} ${v.hostName}`)),
});
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);
}
}
/**
* The CAA tag.
*/
export enum CaaTag {
/**
* Explicity authorizes a single certificate authority to issue a
* certificate (any type) for the hostname.
*/
ISSUE = 'issue',
/**
* Explicity authorizes a single certificate authority to issue a
* wildcard certificate (and only wildcard) for the hostname.
*/
ISSUEWILD = 'issuewild',
/**
* Specifies a URL to which a certificate authority may report policy
* violations.
*/
IODEF = 'iodef',
}
/**
* Properties for a CAA record value.
*/
export interface CaaRecordValue {
/**
* The flag.
*/
readonly flag: number;
/**
* The tag.
*/
readonly tag: CaaTag;
/**
* The value associated with the tag.
*/
readonly value: string;
}
/**
* Construction properties for a CaaRecord.
*/
export interface CaaRecordProps extends RecordSetOptions {
/**
* The values.
*/
readonly values: CaaRecordValue[];
}
/**
* A DNS CAA record
*
* @resource AWS::Route53::RecordSet
*/
export class CaaRecord extends RecordSet {
constructor(scope: Construct, id: string, props: CaaRecordProps) {
super(scope, id, {
...props,
recordType: RecordType.CAA,
target: RecordTarget.fromValues(...props.values.map(v => `${v.flag} ${v.tag} "${v.value}"`)),
});
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);
}
}
/**
* Construction properties for a CaaAmazonRecord.
*/
export interface CaaAmazonRecordProps extends RecordSetOptions {}
/**
* A DNS Amazon CAA record.
*
* A CAA record to restrict certificate authorities allowed
* to issue certificates for a domain to Amazon only.
*
* @resource AWS::Route53::RecordSet
*/
export class CaaAmazonRecord extends CaaRecord {
constructor(scope: Construct, id: string, props: CaaAmazonRecordProps) {
super(scope, id, {
...props,
values: [
{
flag: 0,
tag: CaaTag.ISSUE,
value: 'amazon.com',
},
],
});
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);
}
}
/**
* Properties for a MX record value.
*/
export interface MxRecordValue {
/**
* The priority.
*/
readonly priority: number;
/**
* The mail server host name.
*/
readonly hostName: string;
}
/**
* Construction properties for a MxRecord.
*/
export interface MxRecordProps extends RecordSetOptions {
/**
* The values.
*/
readonly values: MxRecordValue[];
}
/**
* A DNS MX record
*
* @resource AWS::Route53::RecordSet
*/
export class MxRecord extends RecordSet {
constructor(scope: Construct, id: string, props: MxRecordProps) {
super(scope, id, {
...props,
recordType: RecordType.MX,
target: RecordTarget.fromValues(...props.values.map(v => `${v.priority} ${v.hostName}`)),
});
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);
}
}
/**
* Construction properties for a NSRecord.
*/
export interface NsRecordProps extends RecordSetOptions {
/**
* The NS values.
*/
readonly values: string[];
}
/**
* A DNS NS record
*
* @resource AWS::Route53::RecordSet
*/
export class NsRecord extends RecordSet {
constructor(scope: Construct, id: string, props: NsRecordProps) {
super(scope, id, {
...props,
recordType: RecordType.NS,
target: RecordTarget.fromValues(...props.values),
});
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);
}
}
/**
* Construction properties for a DSRecord.
*/
export interface DsRecordProps extends RecordSetOptions {
/**
* The DS values.
*/
readonly values: string[];
}
/**
* A DNS DS record
*
* @resource AWS::Route53::RecordSet
*/
export class DsRecord extends RecordSet {
constructor(scope: Construct, id: string, props: DsRecordProps) {
super(scope, id, {
...props,
recordType: RecordType.DS,
target: RecordTarget.fromValues(...props.values),
});
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);
}
}
/**
* Construction properties for a ZoneDelegationRecord
*/
export interface ZoneDelegationRecordProps extends RecordSetOptions {
/**
* The name servers to report in the delegation records.
*/
readonly nameServers: string[];
}
/**
* A record to delegate further lookups to a different set of name servers.
*/
export class ZoneDelegationRecord extends RecordSet {
constructor(scope: Construct, id: string, props: ZoneDelegationRecordProps) {
super(scope, id, {
...props,
recordType: RecordType.NS,
target: RecordTarget.fromValues(...Token.isUnresolved(props.nameServers)
? props.nameServers // Can't map a string-array token!
: props.nameServers.map(ns => (Token.isUnresolved(ns) || ns.endsWith('.')) ? ns : `${ns}.`),
),
ttl: props.ttl || Duration.days(2),
});
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);
}
}
/**
* Construction properties for a CrossAccountZoneDelegationRecord
*/
export interface CrossAccountZoneDelegationRecordProps {
/**
* The zone to be delegated
*/
readonly delegatedZone: IHostedZone;
/**
* The hosted zone name in the parent account
*
* @default - no zone name
*/
readonly parentHostedZoneName?: string;
/**
* The hosted zone id in the parent account
*
* @default - no zone id
*/
readonly parentHostedZoneId?: string;
/**
* The delegation role in the parent account
*/
readonly delegationRole: iam.IRole;
/**
* The resource record cache time to live (TTL).
*
* @default Duration.days(2)
*/
readonly ttl?: Duration;
/**
* The removal policy to apply to the record set.
*
* @default RemovalPolicy.DESTROY
*/
readonly removalPolicy?: RemovalPolicy;
/**
* Region from which to obtain temporary credentials.
*
* @default - the Route53 signing region in the current partition
*/
readonly assumeRoleRegion?: string;
}
/**
* A Cross Account Zone Delegation record. This construct uses custom resource lambda that calls Route53
* ChangeResourceRecordSets API to upsert a NS record into the `parentHostedZone`.
*
* WARNING: The default removal policy of this resource is DESTROY, therefore, if this resource's logical ID changes or
* if this resource is removed from the stack, the existing NS record will be removed.
*/
export class CrossAccountZoneDelegationRecord extends Construct {
constructor(scope: Construct, id: string, props: CrossAccountZoneDelegationRecordProps) {
super(scope, id);
if (!props.parentHostedZoneName && !props.parentHostedZoneId) {
throw Error('At least one of parentHostedZoneName or parentHostedZoneId is required');
}
if (props.parentHostedZoneName && props.parentHostedZoneId) {
throw Error('Only one of parentHostedZoneName and parentHostedZoneId is supported');
}
const provider = CrossAccountZoneDelegationProvider.getOrCreateProvider(this, CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE);
const role = iam.Role.fromRoleArn(this, 'cross-account-zone-delegation-handler-role', provider.roleArn);
const addToPrinciplePolicyResult = role.addToPrincipalPolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['sts:AssumeRole'],
resources: [props.delegationRole.roleArn],
}));
const customResource = new CustomResource(this, 'CrossAccountZoneDelegationCustomResource', {
resourceType: CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE,
serviceToken: provider.serviceToken,
removalPolicy: props.removalPolicy,
properties: {
AssumeRoleArn: props.delegationRole.roleArn,
ParentZoneName: props.parentHostedZoneName,
ParentZoneId: props.parentHostedZoneId,
DelegatedZoneName: props.delegatedZone.zoneName,
DelegatedZoneNameServers: props.delegatedZone.hostedZoneNameServers!,
TTL: (props.ttl || Duration.days(2)).toSeconds(),
AssumeRoleRegion: props.assumeRoleRegion,
},
});
if (addToPrinciplePolicyResult.policyDependable) {
customResource.node.addDependency(addToPrinciplePolicyResult.policyDependable);
}
}
}