diff --git a/packages/@aws-cdk/aws-ec2/lib/peer.ts b/packages/@aws-cdk/aws-ec2/lib/peer.ts index 11f10df226765..023a56aaccd6e 100644 --- a/packages/@aws-cdk/aws-ec2/lib/peer.ts +++ b/packages/@aws-cdk/aws-ec2/lib/peer.ts @@ -79,7 +79,7 @@ class CidrIPv4 implements IPeer { constructor(private readonly cidrIp: string) { if (!Token.isUnresolved(cidrIp)) { - const cidrMatch = cidrIp.match(/^(\d{1,3}\.){3}\d{1,3}(\/\d+)?$/); + const cidrMatch = cidrIp.match(CIDR_VALIDATION_REGEXES.ipv4); if (!cidrMatch) { throw new Error(`Invalid IPv4 CIDR: "${cidrIp}"`); @@ -126,7 +126,7 @@ class CidrIPv6 implements IPeer { constructor(private readonly cidrIpv6: string) { if (!Token.isUnresolved(cidrIpv6)) { - const cidrMatch = cidrIpv6.match(/^([\da-f]{0,4}:){2,7}([\da-f]{0,4})?(\/\d+)?$/); + const cidrMatch = cidrIpv6.match(CIDR_VALIDATION_REGEXES.ipv6); if (!cidrMatch) { throw new Error(`Invalid IPv6 CIDR: "${cidrIpv6}"`); @@ -188,4 +188,9 @@ class PrefixList implements IPeer { public toEgressRuleConfig(): any { return { destinationPrefixListId: this.prefixListId }; } -} \ No newline at end of file +} + +export const CIDR_VALIDATION_REGEXES = { + ipv4: /^(\d{1,3}\.){3}\d{1,3}(\/\d+)?$/, + ipv6: /^([\da-f]{0,4}:){2,7}([\da-f]{0,4})?(\/\d+)?$/, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/lib/heath-check/alarm-health-check.ts b/packages/@aws-cdk/aws-route53/lib/heath-check/alarm-health-check.ts new file mode 100644 index 0000000000000..5a1af81fcbda4 --- /dev/null +++ b/packages/@aws-cdk/aws-route53/lib/heath-check/alarm-health-check.ts @@ -0,0 +1,80 @@ +import cloudwatch = require('@aws-cdk/aws-cloudwatch'); +import {Aws, Construct} from '@aws-cdk/core'; +import {AdvancedHealthCheckOptions, HealthCheck} from "./health-check"; + +/** + * Alarm health check properties + * @experimental + */ +export interface AlarmHealthCheckProps extends AdvancedHealthCheckOptions { + /** + * The CloudWatch alarm to be monitored + * + * Supported alarms: + * * Standard-resolution metrics (High-resolution metrics aren't supported) + * * Statistics: Average, Minimum, Maximum, Sum, and SampleCount + * + * Route 53 does not support alarms that use metric math to query multiple CloudWatch metrics. + */ + readonly alarm: cloudwatch.Alarm; + /** + * Status of the health check when CloudWatch has insufficient data to determine + * the state of the alarm that you chose for CloudWatch alarm + * + * @default InsufficientDataHealthStatus.LAST_KNOWN_STATUS + */ + readonly insufficientDataHealthStatus?: InsufficientDataHealthStatusType; +} + +/** + * Alarm health check construct + * + * @resource AWS::Route53::HealthCheck + * @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/health-checks-creating-values.html#health-checks-creating-values-cloudwatch + * @experimental + */ +export class AlarmHealthCheck extends HealthCheck { + public constructor(scope: Construct, id: string, props: AlarmHealthCheckProps) { + const {alarm, ...baseProps} = props; + super(scope, id, { + type: AlarmHealthCheckType.CALCULATED, + ...baseProps, + alarmIdentifier: { + name: props.alarm.alarmName, + // FIXME not always true? + region: Aws.REGION, + }, + }); + } +} + +/** + * The type of Route 53 health check + */ +enum AlarmHealthCheckType { + /** + * For health checks that monitor the status of other health checks, + * Route 53 adds up the number of health checks that Route 53 health checkers consider to be healthy and + * compares that number with the value of HealthThreshold. + */ + CALCULATED = 'CALCULATED', +} + +/** + * Type of InsufficientDataHealthStatus + */ +export enum InsufficientDataHealthStatusType { + /** + * Route 53 considers the health check to be healthy. + */ + HEALTHY = 'Healthy', + /** + * Route 53 considers the health check to be unhealthy. + */ + UNHEALTHY = 'Unhealthy', + /** + * Route 53 uses the status of the health check from the last time that CloudWatch had sufficient data to determine the alarm state. + * For new health checks that have no last known status, the default status for the health check is healthy. + */ + LAST_KNOWN_STATUS = 'LastKnownStatus', +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/lib/heath-check/calculated-health-check.ts b/packages/@aws-cdk/aws-route53/lib/heath-check/calculated-health-check.ts new file mode 100644 index 0000000000000..6fc79423e1d6b --- /dev/null +++ b/packages/@aws-cdk/aws-route53/lib/heath-check/calculated-health-check.ts @@ -0,0 +1,58 @@ +import {Construct} from '@aws-cdk/core'; +import {AdvancedHealthCheckOptions, HealthCheck, IHealthCheck} from "./health-check"; + +/** + * Calculated health check properties + * @experimental + */ +export interface CalculatedHealthCheckProps extends AdvancedHealthCheckOptions { + /** + * List of health checks to be monitored + */ + readonly childHealthChecks: IHealthCheck[]; + /** + * Minimum count of healthy {@link CalculatedHealthCheckProps.childHealthChecks} + * required for the parent health check to be considered healthy + * + * * If you specify a number greater than the number of child health checks, + * Route 53 always considers this health check to be unhealthy. + * * If you specify 0, Route 53 always considers this health check to be healthy. + */ + readonly healthThreshold: number; +} + +/** + * Calculated health check construct + * + * @resource AWS::Route53::HealthCheck + * @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/health-checks-creating-values.html#health-checks-creating-values-calculated + * @experimental + */ +export class CalculatedHealthCheck extends HealthCheck { + public constructor( + scope: Construct, + id: string, + props: CalculatedHealthCheckProps, + ) { + const {childHealthChecks, healthThreshold, inverted} = props; + + super(scope, id, { + type: CalculatedHealthCheckType.CALCULATED, + childHealthChecks: childHealthChecks.map(({healthCheckId}) => healthCheckId), + healthThreshold, + inverted, + }); + } +} + +/** + * The type of Route 53 health check + */ +enum CalculatedHealthCheckType { + /** + * For health checks that monitor the status of other health checks, + * Route 53 adds up the number of health checks that Route 53 health checkers consider to be healthy and + * compares that number with the value of HealthThreshold. + */ + CALCULATED = 'CALCULATED', +} diff --git a/packages/@aws-cdk/aws-route53/lib/heath-check/endpoint-health-check.ts b/packages/@aws-cdk/aws-route53/lib/heath-check/endpoint-health-check.ts new file mode 100644 index 0000000000000..783cd01ed8ca5 --- /dev/null +++ b/packages/@aws-cdk/aws-route53/lib/heath-check/endpoint-health-check.ts @@ -0,0 +1,284 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import {Construct} from '@aws-cdk/core'; +import {CfnHealthCheck} from "../route53.generated"; +import {AdvancedHealthCheckOptions, HealthCheck} from "./health-check"; + +interface BaseHttpEndpointHealthCheckProtocolOptions { + /** + * The path you want to request when performing health checks + * + * @default / + */ + readonly resourcePath?: string; + /** + * The string that you want Route 53 to search for in the body of the response from your endpoint. + * Route 53 considers case when searching for SearchString in the response body. + * + * @default - no string matching + */ + readonly searchString?: string; +} + +export interface HttpEndpointHealthCheckProtocolOptions extends BaseHttpEndpointHealthCheckProtocolOptions { + /** + * The port on the endpoint that you want Amazon Route 53 to perform health checks on + * + * @default 80 + */ + readonly port?: number; +} + +export interface HttpsEndpointHealthCheckProtocolOptions extends BaseHttpEndpointHealthCheckProtocolOptions { + /** + * The port on the endpoint that you want Amazon Route 53 to perform health checks on + * + * @default 443 + */ + readonly port?: number; + + /** + * If true, Route 53 will send the host name to the endpoint in the "client_hello" message during TLS negotiation. + * This allows the endpoint to respond to the HTTPS request with the applicable SSL/TLS certificate. + * + * @default true + */ + readonly enableSni?: boolean; +} + +/** + * Endpoint health check protocol construct + * @experimental + */ +export class EndpointHealthCheckProtocol { + /** + * Generate an HTTP monitoring protocol + * + * @param options protocol options + */ + public static http(options: HttpEndpointHealthCheckProtocolOptions = {}): EndpointHealthCheckProtocol { + return new EndpointHealthCheckProtocol({ + type: options.searchString ? EndpointHealthCheckType.HTTP_STR_MATCH : EndpointHealthCheckType.HTTP, + resourcePath: options.resourcePath || '/', + searchString: options.searchString, + port: options.port || 80, + }); + } + + /** + * Generate an HTTPS monitoring protocol + * + * @param options protocol options + */ + public static https(options: HttpsEndpointHealthCheckProtocolOptions = {}): EndpointHealthCheckProtocol { + return new EndpointHealthCheckProtocol({ + type: options.searchString ? EndpointHealthCheckType.HTTPS_STR_MATCH : EndpointHealthCheckType.HTTPS, + resourcePath: options.resourcePath || '/', + searchString: options.searchString, + port: options.port || 443, + enableSni: options.enableSni != null ? options.enableSni : true, + }); + } + + /** + * Generates TCP monitoring protocol + * + * @param port The port on the endpoint that you want Amazon Route 53 to perform health checks on + */ + public static tcp(port: number): EndpointHealthCheckProtocol { + return new EndpointHealthCheckProtocol({type: EndpointHealthCheckType.TCP, port}); + } + + private constructor(public readonly options: CfnHealthCheck.HealthCheckConfigProperty) { + if (options.searchString && options.searchString.length > 255) { + throw new Error(`searchString cannot be over 255 characters long, got ${options.searchString.length}`); + } + } +} + +/** + * Advanced endpoint health check options + * @experimental + */ +export interface AdvancedEndpointHealthCheckOptions extends AdvancedHealthCheckOptions { + /** + * The number of seconds between the time that each Route 53 health checker gets a response from your endpoint + * and the time that it sends the next health check request. + * + * {@link EndpointHealthCheckRequestInterval.FAST} incurs an additional charge + * + * @default EndpointHealthCheckRequestInterval.STANDARD + */ + readonly requestInterval?: EndpointHealthCheckRequestInterval; + /** + * The number of consecutive health checks that an endpoint must pass or fail for Route 53 + * to change the current status of the endpoint from unhealthy to healthy or vice versa + * + * @default 3 + */ + readonly failureThreshold?: number; + /** + * If true, Route 53 will measure the latency between health checkers in multiple AWS Regions and your endpoint. + * CloudWatch latency graphs will appear on the Latency tab on the Health checks page in the Route 53 console. + * + * @default false + */ + readonly measureLatency?: boolean; + /** + * List of regions from which you want Amazon Route 53 health checkers to check the specified endpoint + * You must specify at least three regions. + * + * @default - all valid regions + */ + readonly regions?: string[]; +} + +/** + * Base endpoint health check properties + * @experimental + */ +export interface BaseEndpointHealthCheckProps extends AdvancedEndpointHealthCheckOptions { + /** + * Protocol to be used to check the health of the endpoint + */ + readonly protocol: EndpointHealthCheckProtocol; +} + +/** + * IP address health check properties + * @experimental + */ +export interface IpAddressHealthCheckProps extends BaseEndpointHealthCheckProps { + /** + * The IPv4 or IPv6 address of the endpoint that you want Amazon Route 53 to perform health checks on + */ + readonly ipAddress: string; + /** + * The domain used to construct the HTTP "Host" header + * for all {@link EndpointHealthCheckProtocol.http} and {@link EndpointHealthCheckProtocol.https} health checks + * + * * For the default port values (80 for HTTP, 443 for HTTPS), the "fullyQualifiedDomainName" will be passed + * * For any other port value, "fullyQualifiedDomainName:port" will be passed + */ + readonly fullyQualifiedDomainName?: string; +} + +/** + * Domain name health check properties + * @experimental + */ +export interface DomainNameHealthCheckProps extends BaseEndpointHealthCheckProps { + /** + * The domain name that Route 53 will send a DNS request to. + * Route 53 will retrieve the IPv4 address, and check the health of that endpoint. + * + * Used with {@link EndpointHealthCheckProtocol.http} and {@link EndpointHealthCheckProtocol.https}, + * the domain name will also be passed in the HTTP "Host" header + */ + readonly fullyQualifiedDomainName: string; +} + +/** + * Endpoint health check construct + * + * @resource AWS::Route53::HealthCheck + * @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/health-checks-creating-values.html#health-checks-creating-values-endpoint + * @experimental + */ +export class EndpointHealthCheck extends HealthCheck { + /** + * Generate an IP address health check + * + * @param scope the parent Construct for this Construct + * @param id the logical name of this Construct + * @param props IP Address health check properties + */ + public static ipAddress( + scope: Construct, + id: string, + props: IpAddressHealthCheckProps + ) { + const {protocol, ...basicProps} = props; + + if (!ec2.CIDR_VALIDATION_REGEXES.ipv4.test(basicProps.ipAddress) && + !ec2.CIDR_VALIDATION_REGEXES.ipv6.test(basicProps.ipAddress)) { + throw new Error(`Invalid ipAddress: expected valid IPv4 or IPv6 address, got ${basicProps.ipAddress}`) + } + + if (protocol.options.type === EndpointHealthCheckType.TCP && props.fullyQualifiedDomainName) { + throw new Error('fullyQualifiedDomainName will be ignored with a TCP protocol'); + } + + return new EndpointHealthCheck(scope, id, {...basicProps, ...protocol.options}); + } + + /** + * Generate an domain name health check + * + * @param scope the parent Construct for this Construct + * @param id the logical name of this Construct + * @param props Domain name health check properties + */ + public static domainName( + scope: Construct, + id: string, + props: DomainNameHealthCheckProps + ) { + const {protocol, ...basicProps} = props; + return new EndpointHealthCheck(scope, id, {...basicProps, ...protocol.options}); + } + + protected constructor(scope: Construct, id: string, props: CfnHealthCheck.HealthCheckConfigProperty) { + if (props.regions && props.regions.length < 3) { + throw new Error(`If set, regions must contain at least 3, got ${props.regions.length} ([${props.regions.join(', ')}])`); + } + + super(scope, id, props); + } +} + +/** + * The type of Route 53 health check + */ +enum EndpointHealthCheckType { + /** + * Route 53 tries to establish a TCP connection. + * If successful, Route 53 submits an HTTP request and waits for an HTTP status code of 200 or greater and less than 400. + */ + HTTP = 'HTTP', + /** + * Route 53 tries to establish a TCP connection. + * If successful, Route 53 submits an HTTP request and searches the first 5,120 bytes of the response body + * for the string that you specify in SearchString. + */ + HTTP_STR_MATCH = 'HTTP_STR_MATCH', + /** + * Route 53 tries to establish a TCP connection. + * If successful, Route 53 submits an HTTPS request and waits for an HTTP status code of 200 or greater and less than 400. + * **Important**: If you specify HTTPS for the value of Type, the endpoint must support TLS v1.0 or later. + */ + HTTPS = 'HTTPS', + /** + * Route 53 tries to establish a TCP connection. + * If successful, Route 53 submits an HTTPS request and searches the first 5,120 bytes of the response body + * for the string that you specify in SearchString. + */ + HTTPS_STR_MATCH = 'HTTPS_STR_MATCH', + /** + * Route 53 tries to establish a TCP connection. + */ + TCP = 'TCP', +} + +/** + * Request interval frequency + */ +export enum EndpointHealthCheckRequestInterval { + /** + * Standard (30 seconds) + */ + STANDARD = 30, + /** + * Fast (10 seconds) + */ + FAST = 10, +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/lib/heath-check/health-check.ts b/packages/@aws-cdk/aws-route53/lib/heath-check/health-check.ts new file mode 100644 index 0000000000000..b12b764ad8109 --- /dev/null +++ b/packages/@aws-cdk/aws-route53/lib/heath-check/health-check.ts @@ -0,0 +1,47 @@ +import {Construct, IResource, Resource} from '@aws-cdk/core'; +import {CfnHealthCheck} from '../route53.generated'; + +/** + * A health check + */ +export interface IHealthCheck extends IResource { + /** + * The ID of the health check + */ + readonly healthCheckId: string; +} + +export interface HealthCheckProps extends CfnHealthCheck.HealthCheckConfigProperty { +} + +/** + * A health check + * + * @resource AWS::Route53::HealthCheck + * @experimental + */ +export class HealthCheck extends Resource implements IHealthCheck { + public readonly healthCheckId: string; + + protected constructor(scope: Construct, id: string, props: HealthCheckProps) { + super(scope, id); + + // TODO tags? + const healthCheck = new CfnHealthCheck(this, 'Resource', {healthCheckConfig: props}); + this.healthCheckId = healthCheck.ref; + } +} + +/** + * Advanced health check options + * @experimental + */ +export interface AdvancedHealthCheckOptions { + /** + * @default false + */ + readonly inverted?: boolean; + + // TODO https://github.com/aws-cloudformation/aws-cloudformation-coverage-roadmap/issues/209 + // readonly disabled?: boolean; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts index c6edbf56a6533..372fdb2656435 100644 --- a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts +++ b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts @@ -138,6 +138,13 @@ export class HostedZone extends Resource implements IHostedZone { public addVpc(vpc: ec2.IVpc) { this.vpcs.push({ vpcId: vpc.vpcId, vpcRegion: Stack.of(vpc).region }); } + + /** + * Whether the current zone is a {@link PrivateHostedZone} or not + */ + public isPrivateHostedZone(): boolean { + return this.vpcs.length > 0; + } } /** diff --git a/packages/@aws-cdk/aws-route53/lib/index.ts b/packages/@aws-cdk/aws-route53/lib/index.ts index 9a6ed0a853679..76f8006328611 100644 --- a/packages/@aws-cdk/aws-route53/lib/index.ts +++ b/packages/@aws-cdk/aws-route53/lib/index.ts @@ -3,6 +3,10 @@ export * from './hosted-zone-provider'; export * from './hosted-zone-ref'; export * from './record-set'; export * from './alias-record-target'; +export * from './heath-check/health-check'; +export * from './heath-check/alarm-health-check'; +export * from './heath-check/calculated-health-check'; +export * from './heath-check/endpoint-health-check'; // AWS::Route53 CloudFormation Resources: export * from './route53.generated'; diff --git a/packages/@aws-cdk/aws-route53/lib/record-set.ts b/packages/@aws-cdk/aws-route53/lib/record-set.ts index 3137825e85bf0..d6e831a3a84dc 100644 --- a/packages/@aws-cdk/aws-route53/lib/record-set.ts +++ b/packages/@aws-cdk/aws-route53/lib/record-set.ts @@ -1,5 +1,7 @@ import { Construct, Duration, IResource, Resource, Token } from '@aws-cdk/core'; import { IAliasRecordTarget } from './alias-record-target'; +import { IHealthCheck } from './heath-check/health-check'; +import { HostedZone } from './hosted-zone'; import { IHostedZone } from './hosted-zone-ref'; import { CfnRecordSet } from './route53.generated'; import { determineFullyQualifiedDomainName } from './util'; @@ -32,6 +34,57 @@ export enum RecordType { TXT = 'TXT' } +/** + * Geographic location + */ +export class GeoLocation { + /** + * Matches a continent geographic location + * + * @param continentCode The targeted continent + */ + public static continent(continentCode: ContinentCode): GeoLocation { + return new GeoLocation({continentCode}); + } + + /** + * Matches a country geographic location + * + * Route 53 doesn't support creating geolocation records for the following countries: + * * Bouvet Island (BV) + * * Christmas Island (CX) + * * Western Sahara (EH) + * * Heard Island and McDonald Islands (HM) + * + * @param countryCode Two-letter ISO 3166-1 alpha 2 country code + * @see https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements + */ + public static country(countryCode: string): GeoLocation { + return new GeoLocation({countryCode}); + } + + /** + * Matches a United States state geographic location + * + * @param subdivisionCode Two-letter code for a state of the United States. + * @see https://pe.usps.com/text/pub28/28apb.htm + */ + public static unitedStatesSubidivision(subdivisionCode: string): GeoLocation { + return new GeoLocation({subdivisionCode, countryCode: 'US'}); + } + + /** + * Matches all geographic locations that aren't specified in other + * geolocation resource record sets that have the same recordName and type. + */ + public static wildcard(): GeoLocation { + return new GeoLocation({countryCode: '*'}); + } + + private constructor(public readonly options: CfnRecordSet.GeoLocationProperty) { + } +} + /** * Options for a RecordSet. */ @@ -61,8 +114,71 @@ export interface RecordSetOptions { * @default no comment */ readonly comment?: string; + + /** + * Specify the EC2 Region where you created the resource that this resource record set refers to + * + * @default - no specific latency-based routing + * @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy.html#routing-policy-latency + */ + readonly region?: string; + + /** + * Control how Amazon Route 53 responds to DNS queries based on the geographic origin of the query + * + * @default - no specific geographic routing + * @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy.html#routing-policy-geo + */ + readonly geoLocation?: GeoLocation; + + /** + * Controls the proportion of DNS queries that Route 53 responds to using the current record + * + * Route 53 calculates the sum of the weights for the records that have the same combination of DNS name and type. + * + * To disable routing to a resource, set weight to 0 + * If you set weight to 0 for all of the records in the group, traffic is routed to all resources with equal probability + * + * @default - no specific weighted routing + * @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy.html#routing-policy-weighted + */ + readonly weight?: number; + + /** + * If true, will route traffic approximately randomly to multiple resources + * + * @default false - no multi value answer routing + * @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy.html#routing-policy-multivalue + */ + readonly multiValueAnswer?: boolean; + + /** + * Failover routing lets you route traffic to a resource when the resource is healthy, + * or to a different resource when the first resource is unhealthy + * + * @default - no failover routing + * @see https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy.html#routing-policy-failover + */ + readonly failover?: FailoverType; + + /** + * An identifier that differentiates among multiple resource record sets that have the same combination of name and type + * + * @default - no set identifier + */ + readonly setIdentifier?: string; + + /** + * If set, this record will only be sent in response when the status of a health check is healthy + * + * @default - no health check + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-recordset.html#cfn-route53-recordset-healthcheckid + */ + readonly healthCheck?: IHealthCheck; } +const routingPolicyKeys: Array = ['geoLocation', 'region', 'weight', 'multiValueAnswer', 'failover']; + /** * Type union for a record that accepts multiple types of target. */ @@ -119,6 +235,26 @@ export class RecordSet extends Resource implements IRecordSet { const ttl = props.target.aliasTarget ? undefined : ((props.ttl && props.ttl.toSeconds()) || 1800).toString(); + const routingPolicyProps = routingPolicyKeys.filter((key) => !!props[key]); + + if (routingPolicyProps.length > 1) { + throw new Error(`Cannot set more than 1 routing policy property (got ${routingPolicyProps.join(', ')})`); + } + + if (routingPolicyProps.length) { + if (!props.setIdentifier) { + throw new Error(`Cannot create routing record sets without setIdentifier property (got ${routingPolicyProps[0]})`); + } + + if (isHostedZoneConstruct(props.zone) && props.zone.isPrivateHostedZone()) { + throw new Error(`Cannot create routing record sets in private hosted zones (got ${routingPolicyProps[0]})`); + } + } + + if (props.weight && (props.weight > 255 || props.weight < 0)) { + throw new Error(`weight property cannot negative or over 255 (got ${props.weight})`); + } + const recordSet = new CfnRecordSet(this, 'Resource', { hostedZoneId: props.zone.hostedZoneId, name: determineFullyQualifiedDomainName(props.recordName || props.zone.zoneName, props.zone), @@ -126,7 +262,14 @@ export class RecordSet extends Resource implements IRecordSet { resourceRecords: props.target.values, aliasTarget: props.target.aliasTarget && props.target.aliasTarget.bind(this), ttl, - comment: props.comment + comment: props.comment, + setIdentifier: props.setIdentifier, + region: props.region, + geoLocation: props.geoLocation && props.geoLocation.options, + weight: props.weight != null ? props.weight : undefined, + multiValueAnswer: props.multiValueAnswer || undefined, + failover: props.failover, + healthCheckId: props.healthCheck && props.healthCheck.healthCheckId, }); this.domainName = recordSet.ref; @@ -293,7 +436,7 @@ export class SrvRecord extends RecordSet { */ export enum CaaTag { /** - * Explicity authorizes a single certificate authority to issue a + * Explicitly authorizes a single certificate authority to issue a * certificate (any type) for the hostname. */ ISSUE = 'issue', @@ -451,3 +594,54 @@ export class ZoneDelegationRecord extends RecordSet { }); } } + +/** + * Contient code + */ +export enum ContinentCode { + /** + * Africa + */ + AFRICA = 'AF', + /** + * Antarctica + */ + ANTARCTICA = 'AN', + /** + * Asia + */ + ASIA = 'AS', + /** + * Europe + */ + EUROPE = 'EU', + /** + * Oceania + */ + OCEANIA = 'OC', + /** + * North America + */ + NORTH_AMERICA = 'NA', + /** + * South America + */ + SOUTH_AMERICA = 'SA', +} + +/** + * Failover routing type + */ +export enum FailoverType { + /** + * Primary record + */ + PRIMARY = 'PRIMARY', + + /** + * Secondary record + */ + SECONDARY = 'SECONDARY', +} + +const isHostedZoneConstruct = (zone: IHostedZone): zone is HostedZone => !!(zone as HostedZone).isPrivateHostedZone; diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index 323d1e95b545d..5a65c1f890f1b 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -71,6 +71,7 @@ "pkglint": "file:../../../tools/pkglint" }, "dependencies": { + "@aws-cdk/aws-cloudwatch": "^1.12.0", "@aws-cdk/aws-ec2": "^1.12.0", "@aws-cdk/aws-logs": "^1.12.0", "@aws-cdk/core": "^1.12.0", @@ -78,6 +79,7 @@ }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-cloudwatch": "^1.12.0", "@aws-cdk/aws-ec2": "^1.12.0", "@aws-cdk/aws-logs": "^1.12.0", "@aws-cdk/core": "^1.12.0", diff --git a/packages/@aws-cdk/aws-route53/test/test.health-check.ts b/packages/@aws-cdk/aws-route53/test/test.health-check.ts new file mode 100644 index 0000000000000..7a79a52a659f6 --- /dev/null +++ b/packages/@aws-cdk/aws-route53/test/test.health-check.ts @@ -0,0 +1,481 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import cloudwatch = require('@aws-cdk/aws-cloudwatch'); +import { App, Stack, Tag } from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import { EndpointHealthCheckRequestInterval } from "../lib"; +import route53 = require('../lib'); + +export = { + Endpoint: { + 'IP Address': { + basic(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + route53.EndpointHealthCheck.ipAddress(stack, 'HealthCheck', { + ipAddress: '1.1.1.1', + protocol: route53.EndpointHealthCheckProtocol.http(), + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::HealthCheck', { + HealthCheckConfig: { + IPAddress: "1.1.1.1", + Port: 80, + ResourcePath: "/", + Type: "HTTP" + } + })); + test.done(); + }, + IPv6(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + route53.EndpointHealthCheck.ipAddress(stack, 'HealthCheck', { + ipAddress: '::', + protocol: route53.EndpointHealthCheckProtocol.http(), + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::HealthCheck', { + HealthCheckConfig: { + IPAddress: "::", + Port: 80, + ResourcePath: "/", + Type: "HTTP" + } + })); + test.done(); + }, + advanced(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + route53.EndpointHealthCheck.ipAddress(stack, 'HealthCheck', { + ipAddress: '1.1.1.1', + fullyQualifiedDomainName: 'example.com', + protocol: route53.EndpointHealthCheckProtocol.http(), + inverted: true, + failureThreshold: 9, + measureLatency: true, + regions: ['us-west-1', 'us-east-1', 'eu-west-3'], + requestInterval: EndpointHealthCheckRequestInterval.FAST, + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::HealthCheck', { + HealthCheckConfig: { + FailureThreshold: 9, + FullyQualifiedDomainName: "example.com", + IPAddress: "1.1.1.1", + Inverted: true, + MeasureLatency: true, + Port: 80, + Regions: [ + "us-west-1", + "us-east-1", + "eu-west-3" + ], + RequestInterval: 10, + ResourcePath: "/", + Type: "HTTP" + } + })); + test.done(); + }, + }, + 'Domain name': { + basic(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + route53.EndpointHealthCheck.domainName(stack, 'HealthCheck', { + fullyQualifiedDomainName: 'example.com', + protocol: route53.EndpointHealthCheckProtocol.http(), + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::HealthCheck', { + HealthCheckConfig: { + FullyQualifiedDomainName: "example.com", + Port: 80, + ResourcePath: "/", + Type: "HTTP" + } + })); + test.done(); + }, + }, + 'Errors': { + 'throws if invalid IP address'(test: Test) { + // GIVEN + const stack = new Stack(); + + // THEN + test.throws(() => { + route53.EndpointHealthCheck.ipAddress(stack, 'HealthCheck', { + ipAddress: '1', + fullyQualifiedDomainName: 'example.com', + protocol: route53.EndpointHealthCheckProtocol.http(), + }); + }, /Invalid ipAddress: expected valid IPv4 or IPv6 address/); + test.done(); + }, + 'throws if IP, TCP and domain name'(test: Test) { + // GIVEN + const stack = new Stack(); + + // THEN + test.throws(() => { + route53.EndpointHealthCheck.ipAddress(stack, 'HealthCheck', { + ipAddress: '1.1.1.1', + fullyQualifiedDomainName: 'example.com', + protocol: route53.EndpointHealthCheckProtocol.tcp(8080), + }); + }, /fullyQualifiedDomainName will be ignored with a TCP protocol/); + test.done(); + }, + 'throws if not enough regions'(test: Test) { + // GIVEN + const stack = new Stack(); + + // THEN + test.throws(() => { + route53.EndpointHealthCheck.ipAddress(stack, 'HealthCheck', { + ipAddress: '1.1.1.1', + protocol: route53.EndpointHealthCheckProtocol.http(), + regions: ['foo', 'bar'], + }); + }, 'If set, regions must contain at least 3, got 2 ([foo, bar])'); + test.done(); + }, + 'throws if not empty regions'(test: Test) { + // GIVEN + const stack = new Stack(); + + // THEN + test.throws(() => { + route53.EndpointHealthCheck.ipAddress(stack, 'HealthCheck', { + ipAddress: '1.1.1.1', + protocol: route53.EndpointHealthCheckProtocol.http(), + regions: [], + }); + }, 'If set, regions must contain at least 3, got 0 ([])'); + test.done(); + }, + 'throws if searchString is too long'(test: Test) { + // GIVEN + const stack = new Stack(); + + // THEN + test.throws(() => { + route53.EndpointHealthCheck.ipAddress(stack, 'HealthCheck', { + ipAddress: '1.1.1.1', + protocol: route53.EndpointHealthCheckProtocol.http({ + searchString: Array(256).fill(' ').join('') + }), + regions: [], + }); + }, 'searchString cannot be over 255 characters long, got 256'); + test.done(); + }, + }, + 'Protocols': { + basic: { + HTTP(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + route53.EndpointHealthCheck.ipAddress(stack, 'HealthCheck', { + ipAddress: '1.1.1.1', + protocol: route53.EndpointHealthCheckProtocol.http(), + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::HealthCheck', { + HealthCheckConfig: { + IPAddress: "1.1.1.1", + Port: 80, + ResourcePath: "/", + Type: "HTTP" + } + })); + test.done(); + }, + HTTPS(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + route53.EndpointHealthCheck.ipAddress(stack, 'HealthCheck', { + ipAddress: '1.1.1.1', + fullyQualifiedDomainName: 'example.com', + protocol: route53.EndpointHealthCheckProtocol.https(), + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::HealthCheck', { + HealthCheckConfig: { + EnableSNI: true, + FullyQualifiedDomainName: "example.com", + IPAddress: "1.1.1.1", + Port: 443, + ResourcePath: "/", + Type: "HTTPS" + } + })); + test.done(); + }, + TCP(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + route53.EndpointHealthCheck.ipAddress(stack, 'HealthCheck', { + ipAddress: '1.1.1.1', + protocol: route53.EndpointHealthCheckProtocol.tcp(5000), + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::HealthCheck', { + HealthCheckConfig: { + IPAddress: "1.1.1.1", + Port: 5000, + Type: "TCP" + } + })); + test.done(); + }, + }, + advanced: { + HTTP(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + route53.EndpointHealthCheck.ipAddress(stack, 'HealthCheck', { + ipAddress: '1.1.1.1', + protocol: route53.EndpointHealthCheckProtocol.http({ + resourcePath: '/test', + searchString: 'Test String', + port: 8080, + }), + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::HealthCheck', { + HealthCheckConfig: { + IPAddress: "1.1.1.1", + Port: 8080, + ResourcePath: "/test", + SearchString: "Test String", + Type: "HTTP_STR_MATCH" + } + })); + test.done(); + }, + HTTPS(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + route53.EndpointHealthCheck.ipAddress(stack, 'HealthCheck', { + ipAddress: '1.1.1.1', + protocol: route53.EndpointHealthCheckProtocol.https({ + resourcePath: '/test', + searchString: 'Test String', + port: 8080, + enableSni: false, + }), + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::HealthCheck', { + HealthCheckConfig: { + EnableSNI: false, + IPAddress: "1.1.1.1", + Port: 8080, + ResourcePath: "/test", + SearchString: "Test String", + Type: "HTTPS_STR_MATCH" + } + })); + test.done(); + }, + }, + } + }, + Calculated: { + 'basic'(test: Test) { + // GIVEN + const stack = new Stack(); + + const healthCheck1 = route53.EndpointHealthCheck.ipAddress(stack, 'HealthCheckHTTP', { + ipAddress: '1.1.1.1', + protocol: route53.EndpointHealthCheckProtocol.http(), + }); + const healthCheck2 = route53.EndpointHealthCheck.ipAddress(stack, 'HealthCheckTCP', { + ipAddress: '1.1.1.1', + protocol: route53.EndpointHealthCheckProtocol.tcp(5000), + }); + + // WHEN + new route53.CalculatedHealthCheck(stack, 'CalculatedHealthCheck', { + childHealthChecks: [healthCheck1, healthCheck2], + healthThreshold: 1, + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::HealthCheck', { + HealthCheckConfig: { + ChildHealthChecks: [ + { Ref: "HealthCheckHTTP36AA2522" }, + { Ref: "HealthCheckTCP6D68A1D5" } + ], + HealthThreshold: 1, + Type: "CALCULATED" + } + })); + test.done(); + }, + 'cross-stack'(test: Test) { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1'); + const stack2 = new Stack(app, 'Stack2'); + + const healthCheck1 = route53.EndpointHealthCheck.ipAddress(stack1, 'HealthCheckHTTP', { + ipAddress: '1.1.1.1', + protocol: route53.EndpointHealthCheckProtocol.http(), + }); + const healthCheck2 = route53.EndpointHealthCheck.ipAddress(stack1, 'HealthCheckTCP', { + ipAddress: '1.1.1.1', + protocol: route53.EndpointHealthCheckProtocol.tcp(5000), + }); + + // WHEN + new route53.CalculatedHealthCheck(stack2, 'CalculatedHealthCheck', { + childHealthChecks: [healthCheck1, healthCheck2], + healthThreshold: 1, + inverted: true + }); + + // THEN + expect(stack2).to(haveResource('AWS::Route53::HealthCheck', { + HealthCheckConfig: { + ChildHealthChecks: [ + { "Fn::ImportValue": "Stack1:ExportsOutputRefHealthCheckHTTP36AA2522D043599F" }, + { "Fn::ImportValue": "Stack1:ExportsOutputRefHealthCheckTCP6D68A1D593D1BECC" } + ], + HealthThreshold: 1, + Inverted: true, + Type: "CALCULATED" + } + })); + test.done(); + }, + }, + Alarm: { + basic(test: Test) { + // GIVEN + const stack = new Stack(); + + const alarm = new cloudwatch.Alarm(stack, 'Alarm', { + metric: new cloudwatch.Metric({ + metricName: 'Errors', + namespace: 'my.namespace', + }), + threshold: 1, + evaluationPeriods: 1, + }); + + // WHEN + new route53.AlarmHealthCheck(stack, 'HealthCheck', { alarm }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::HealthCheck', { + HealthCheckConfig: { + AlarmIdentifier: { + Name: { + Ref: "Alarm7103F465" + }, + Region: { + Ref: "AWS::Region" + } + }, + Type: "CALCULATED" + } + })); + test.done(); + }, + complex(test: Test) { + // GIVEN + const stack = new Stack(); + + const alarm = new cloudwatch.Alarm(stack, 'Alarm', { + metric: new cloudwatch.Metric({ + metricName: 'Errors', + namespace: 'my.namespace', + }), + threshold: 1, + evaluationPeriods: 1, + }); + + // WHEN + new route53.AlarmHealthCheck(stack, 'HealthCheck', { + alarm, + insufficientDataHealthStatus: route53.InsufficientDataHealthStatusType.UNHEALTHY, + inverted: true + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::HealthCheck', { + HealthCheckConfig: { + AlarmIdentifier: { + Name: { + Ref: "Alarm7103F465" + }, + Region: { + Ref: "AWS::Region" + } + }, + InsufficientDataHealthStatus: "Unhealthy", + Inverted: true, + Type: "CALCULATED" + } + })); + test.done(); + }, + }, + 'Tagging'(test: Test) { + // GIVEN + const stack = new Stack(); + const healthCheck = route53.EndpointHealthCheck.ipAddress(stack, 'HealthCheck', { + ipAddress: '1.1.1.1', + protocol: route53.EndpointHealthCheckProtocol.http(), + }); + + // WHEN + Tag.add(healthCheck, 'Foo', 'Bar'); + + // THEN + expect(stack).to(haveResource('AWS::Route53::HealthCheck', { + HealthCheckConfig: { + IPAddress: "1.1.1.1", + Port: 80, + ResourcePath: "/", + Type: "HTTP" + }, + HealthCheckTags: [ + {Key: 'Foo', Value: 'Bar'}, + ] + })); + test.done(); + }, +}; diff --git a/packages/@aws-cdk/aws-route53/test/test.record-set.ts b/packages/@aws-cdk/aws-route53/test/test.record-set.ts index 63860772628ef..de8991f88de65 100644 --- a/packages/@aws-cdk/aws-route53/test/test.record-set.ts +++ b/packages/@aws-cdk/aws-route53/test/test.record-set.ts @@ -1,6 +1,7 @@ -import { expect, haveResource } from '@aws-cdk/assert'; -import { Duration, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import {expect, haveResource} from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import {Duration, Stack} from '@aws-cdk/core'; +import {Test} from 'nodeunit'; import route53 = require('../lib'); export = { @@ -454,5 +455,347 @@ export = { TTL: "172800" })); test.done(); - } + }, + + 'Heath check'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + const healthCheck = route53.EndpointHealthCheck.domainName(stack, 'HealthCheck', { + fullyQualifiedDomainName: 'www.myzone.', + protocol: route53.EndpointHealthCheckProtocol.http(), + }); + + // WHEN + new route53.RecordSet(stack, 'GeoLocation', { + zone, + recordName: 'www', + recordType: route53.RecordType.CNAME, + target: route53.RecordTarget.fromValues('zzz'), + healthCheck, + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + "Name": "www.myzone.", + "Type": "CNAME", + "HealthCheckId": { + "Ref": "HealthCheckA1C381C7" + }, + "HostedZoneId": { + "Ref": "HostedZoneDB99F866" + }, + "ResourceRecords": [ + "zzz" + ], + "TTL": "1800" + })); + test.done(); + }, + + 'Routing policy': { + 'Geo location record'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.RecordSet(stack, 'GeoLocation', { + zone, + recordName: 'www', + recordType: route53.RecordType.CNAME, + target: route53.RecordTarget.fromValues('zzz'), + setIdentifier: 'test', + geoLocation: route53.GeoLocation.unitedStatesSubidivision('HI'), + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "www.myzone.", + Type: "CNAME", + GeoLocation: { + CountryCode: "US", + SubdivisionCode: "HI" + }, + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + "zzz" + ], + TTL: "1800" + })); + test.done(); + }, + + 'Region record'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.RecordSet(stack, 'GeoLocation', { + zone, + recordName: 'www', + recordType: route53.RecordType.CNAME, + target: route53.RecordTarget.fromValues('zzz'), + setIdentifier: 'test', + region: 'us-east-1', + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "www.myzone.", + Type: "CNAME", + Region: 'us-east-1', + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + "zzz" + ], + TTL: "1800" + })); + test.done(); + }, + + 'Weighted record'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.RecordSet(stack, 'GeoLocation', { + zone, + recordName: 'www', + recordType: route53.RecordType.CNAME, + target: route53.RecordTarget.fromValues('zzz'), + setIdentifier: 'test', + weight: 255, + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "www.myzone.", + Type: "CNAME", + Weight: 255, + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + "zzz" + ], + TTL: "1800" + })); + test.done(); + }, + + 'Zero weighted record'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.RecordSet(stack, 'GeoLocation', { + zone, + recordName: 'www', + recordType: route53.RecordType.CNAME, + target: route53.RecordTarget.fromValues('zzz'), + setIdentifier: 'test', + weight: 0, + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "www.myzone.", + Type: "CNAME", + Weight: 0, + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + "zzz" + ], + TTL: "1800" + })); + test.done(); + }, + + 'Multi value answer record'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.RecordSet(stack, 'GeoLocation', { + zone, + recordName: 'www', + recordType: route53.RecordType.CNAME, + target: route53.RecordTarget.fromValues('zzz'), + setIdentifier: 'test', + multiValueAnswer: true, + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "www.myzone.", + Type: "CNAME", + MultiValueAnswer: true, + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + "zzz" + ], + TTL: "1800" + })); + test.done(); + }, + + 'Failover record'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.RecordSet(stack, 'GeoLocation', { + zone, + recordName: 'www', + recordType: route53.RecordType.CNAME, + target: route53.RecordTarget.fromValues('zzz'), + setIdentifier: 'test', + failover: route53.FailoverType.PRIMARY + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "www.myzone.", + Type: "CNAME", + Failover: 'PRIMARY', + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + "zzz" + ], + TTL: "1800" + })); + test.done(); + }, + + 'Throws if weight is over 255'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // THEN + test.throws(() => { + new route53.RecordSet(stack, 'GeoLocation', { + zone, + recordName: 'www', + recordType: route53.RecordType.CNAME, + target: route53.RecordTarget.fromValues('zzz'), + setIdentifier: 'test', + weight: 256 + }); + }, /weight property cannot negative or over 255/); + test.done(); + }, + + 'Throws if routing policy record in private zone'(test: Test) { + // GIVEN + const stack = new Stack(); + + const vpc = new ec2.Vpc(stack, 'Vpc'); + const zone = new route53.PrivateHostedZone(stack, 'HostedZone', { + zoneName: 'myzone', + vpc + }); + + // THEN + test.throws(() => { + new route53.RecordSet(stack, 'GeoLocation', { + zone, + recordName: 'www', + recordType: route53.RecordType.CNAME, + target: route53.RecordTarget.fromValues('zzz'), + setIdentifier: 'test', + geoLocation: route53.GeoLocation.unitedStatesSubidivision('HI') + }); + }, /Cannot create routing record sets in private hosted zones/); + test.done(); + }, + + 'Throws if more than routing record in private zone'(test: Test) { + // GIVEN + const stack = new Stack(); + + const vpc = new ec2.Vpc(stack, 'Vpc'); + const zone = new route53.PrivateHostedZone(stack, 'HostedZone', { + zoneName: 'myzone', + vpc + }); + + // THEN + test.throws(() => { + new route53.RecordSet(stack, 'GeoLocation', { + zone, + recordName: 'www', + recordType: route53.RecordType.CNAME, + target: route53.RecordTarget.fromValues('zzz'), + geoLocation: route53.GeoLocation.unitedStatesSubidivision('HI'), + setIdentifier: 'test', + region: 'us-east-1', + }); + }, /Cannot set more than 1 routing policy property/); + test.done(); + }, + + 'Throws if missing setIdentifier'(test: Test) { + // GIVEN + const stack = new Stack(); + + const vpc = new ec2.Vpc(stack, 'Vpc'); + const zone = new route53.PrivateHostedZone(stack, 'HostedZone', { + zoneName: 'myzone', + vpc + }); + + // THEN + test.throws(() => { + new route53.RecordSet(stack, 'GeoLocation', { + zone, + recordName: 'www', + recordType: route53.RecordType.CNAME, + target: route53.RecordTarget.fromValues('zzz'), + region: 'us-east-1', + }); + }, /Cannot create routing record sets without setIdentifier property/); + test.done(); + }, + }, };