From f957a76c9bc60a1f0d36a4cee13135070398fdff Mon Sep 17 00:00:00 2001 From: Povilas Ramonas Date: Wed, 18 Mar 2026 16:23:35 +0200 Subject: [PATCH 1/4] Add Route 53 weighted routing and health check support --- .../record-modifiers/R53_HEALTH_CHECK_ID.md | 31 ++++++++ .../record-modifiers/R53_WEIGHT.md | 70 +++++++++++++++++ documentation/provider/route53.md | 38 +++++++++- integrationTest/helpers_integration_test.go | 19 +++++ integrationTest/integration_test.go | 55 ++++++++++++++ models/record.go | 6 ++ pkg/js/helpers.js | 31 ++++++++ providers/route53/auditrecords.go | 35 ++++++++- providers/route53/route53Provider.go | 75 +++++++++++++++++-- 9 files changed, 353 insertions(+), 7 deletions(-) create mode 100644 documentation/language-reference/record-modifiers/R53_HEALTH_CHECK_ID.md create mode 100644 documentation/language-reference/record-modifiers/R53_WEIGHT.md diff --git a/documentation/language-reference/record-modifiers/R53_HEALTH_CHECK_ID.md b/documentation/language-reference/record-modifiers/R53_HEALTH_CHECK_ID.md new file mode 100644 index 0000000000..d2a44af952 --- /dev/null +++ b/documentation/language-reference/record-modifiers/R53_HEALTH_CHECK_ID.md @@ -0,0 +1,31 @@ +--- +name: R53_HEALTH_CHECK_ID +parameters: + - health_check_id +parameter_types: + health_check_id: string +ts_return: RecordModifier +provider: ROUTE53 +--- + +`R53_HEALTH_CHECK_ID` associates a [Route 53 health check](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/health-checks-creating.html) with a record. This is typically used in combination with [`R53_WEIGHT()`](R53_WEIGHT.md) for weighted routing, so that Route 53 stops routing traffic to unhealthy endpoints. + +The `health_check_id` is the ID of a Route 53 health check that you create separately (e.g. via the AWS Console, CLI, or Terraform). DNSControl does not manage the health checks themselves, only their association with DNS records. + +{% code title="dnsconfig.js" %} +```javascript +var REG_NONE = NewRegistrar("none"); +var DSP_R53 = NewDnsProvider("r53_main"); + +D("example.com", REG_NONE, DnsProvider(DSP_R53), + A("www", "1.2.3.4", + R53_WEIGHT(70, "primary"), + R53_HEALTH_CHECK_ID("12345678-1234-1234-1234-123456789012"), + ), + A("www", "5.6.7.8", + R53_WEIGHT(30, "secondary"), + R53_HEALTH_CHECK_ID("87654321-4321-4321-4321-210987654321"), + ), +); +``` +{% endcode %} diff --git a/documentation/language-reference/record-modifiers/R53_WEIGHT.md b/documentation/language-reference/record-modifiers/R53_WEIGHT.md new file mode 100644 index 0000000000..d8d17bc6b4 --- /dev/null +++ b/documentation/language-reference/record-modifiers/R53_WEIGHT.md @@ -0,0 +1,70 @@ +--- +name: R53_WEIGHT +parameters: + - weight + - set_identifier +parameter_types: + weight: number + set_identifier: string +ts_return: RecordModifier +provider: ROUTE53 +--- + +`R53_WEIGHT` configures [Route 53 weighted routing](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-weighted.html) for a record. + +Weighted routing lets you associate multiple resources with a single domain name and control the proportion of traffic that is routed to each resource. + +- `weight`: An integer between 0 and 255. Route 53 distributes traffic proportionally based on the weights assigned to each record with the same name and type. +- `set_identifier`: A unique string that differentiates this record from other records with the same name and type. Each weighted record in a group must have a unique set identifier. + +{% code title="dnsconfig.js" %} +```javascript +var REG_NONE = NewRegistrar("none"); +var DSP_R53 = NewDnsProvider("r53_main"); + +D("example.com", REG_NONE, DnsProvider(DSP_R53), + // 70% of traffic goes to 1.2.3.4, 30% to 5.6.7.8 + A("www", "1.2.3.4", R53_WEIGHT(70, "web-east")), + A("www", "5.6.7.8", R53_WEIGHT(30, "web-west")), +); +``` +{% endcode %} + +`R53_WEIGHT` can be used with any record type supported by Route 53 weighted routing, including `A`, `AAAA`, `CNAME`, `TXT`, and [`R53_ALIAS()`](../domain-modifiers/R53_ALIAS.md). + +{% code title="dnsconfig.js" %} +```javascript +D("example.com", REG_NONE, DnsProvider(DSP_R53), + // Weighted CNAME records + CNAME("cdn", "east.cdn.example.com.", R53_WEIGHT(70, "cdn-east")), + CNAME("cdn", "west.cdn.example.com.", R53_WEIGHT(30, "cdn-west")), + + // Weighted R53_ALIAS records + R53_ALIAS("api", "A", "alb-east.us-east-1.elb.amazonaws.com.", + R53_WEIGHT(60, "api-east"), + R53_ZONE("Z35SXDOTRQ7X7K"), + ), + R53_ALIAS("api", "A", "alb-west.us-west-2.elb.amazonaws.com.", + R53_WEIGHT(40, "api-west"), + R53_ZONE("Z1H1FL5HABSF5"), + ), +); +``` +{% endcode %} + +You can optionally add a health check using [`R53_HEALTH_CHECK_ID()`](R53_HEALTH_CHECK_ID.md) to remove unhealthy endpoints from the rotation. + +{% code title="dnsconfig.js" %} +```javascript +D("example.com", REG_NONE, DnsProvider(DSP_R53), + A("www", "1.2.3.4", + R53_WEIGHT(70, "web-east"), + R53_HEALTH_CHECK_ID("12345678-1234-1234-1234-123456789012"), + ), + A("www", "5.6.7.8", + R53_WEIGHT(30, "web-west"), + R53_HEALTH_CHECK_ID("87654321-4321-4321-4321-210987654321"), + ), +); +``` +{% endcode %} diff --git a/documentation/provider/route53.md b/documentation/provider/route53.md index fd058ad17b..69463f87df 100644 --- a/documentation/provider/route53.md +++ b/documentation/provider/route53.md @@ -78,7 +78,14 @@ Example: You can find some other ways to authenticate to Route53 in the [go sdk configuration](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html). ## Metadata -This provider does not recognize any special metadata fields unique to route 53. + +Record-level metadata: + +- `r53_weight` (0-255): Route 53 weighted routing weight. Must be used with `r53_set_identifier`. +- `r53_set_identifier` (string): Unique identifier for a weighted routing record set. Required when using `r53_weight`. +- `r53_health_check_id` (string): Route 53 health check ID to associate with the record. + +These are typically set using the [`R53_WEIGHT()`](../language-reference/record-modifiers/R53_WEIGHT.md) and [`R53_HEALTH_CHECK_ID()`](../language-reference/record-modifiers/R53_HEALTH_CHECK_ID.md) record modifiers. ## Usage An example configuration: @@ -120,6 +127,35 @@ D("testzone.net!public", REG_NONE, ``` {% endcode %} +## Weighted routing + +Route 53 [weighted routing](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-weighted.html) distributes traffic across multiple endpoints based on weights you assign. + +{% code title="dnsconfig.js" %} +```javascript +var REG_NONE = NewRegistrar("none"); +var DSP_R53 = NewDnsProvider("r53_main"); + +D("example.com", REG_NONE, DnsProvider(DSP_R53), + // 70% of traffic goes to 1.2.3.4, 30% to 5.6.7.8 + A("www", "1.2.3.4", R53_WEIGHT(70, "web-east")), + A("www", "5.6.7.8", R53_WEIGHT(30, "web-west")), + + // With health checks + A("api", "10.0.1.1", + R53_WEIGHT(50, "api-primary"), + R53_HEALTH_CHECK_ID("12345678-1234-1234-1234-123456789012"), + ), + A("api", "10.0.2.1", + R53_WEIGHT(50, "api-secondary"), + R53_HEALTH_CHECK_ID("87654321-4321-4321-4321-210987654321"), + ), +); +``` +{% endcode %} + +See [`R53_WEIGHT()`](../language-reference/record-modifiers/R53_WEIGHT.md) and [`R53_HEALTH_CHECK_ID()`](../language-reference/record-modifiers/R53_HEALTH_CHECK_ID.md) for full documentation. + ## Activation DNSControl depends on a standard [AWS access key](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) with permission to list, create and update hosted zones. If you do not have the permissions required you will receive the following error message `Check your credentials, your not authorized to perform actions on Route 53 AWS Service`. diff --git a/integrationTest/helpers_integration_test.go b/integrationTest/helpers_integration_test.go index c81ce60b53..f9bafabe0b 100644 --- a/integrationTest/helpers_integration_test.go +++ b/integrationTest/helpers_integration_test.go @@ -604,6 +604,25 @@ func r53alias(name, aliasType, target, evalTargetHealth string) *models.RecordCo return r } +func r53weighted(name, target, rtype string, weight int, setID string) *models.RecordConfig { + r := makeRec(name, target, rtype) + r.Metadata = map[string]string{ + "r53_weight": fmt.Sprintf("%d", weight), + "r53_set_identifier": setID, + } + return r +} + +func r53weightedHealthCheck(name, target, rtype string, weight int, setID, healthCheckID string) *models.RecordConfig { + r := makeRec(name, target, rtype) + r.Metadata = map[string]string{ + "r53_weight": fmt.Sprintf("%d", weight), + "r53_set_identifier": setID, + "r53_health_check_id": healthCheckID, + } + return r +} + func rp(name string, m, t string) *models.RecordConfig { rec, err := rtypecontrol.NewRecordConfigFromRaw(rtypecontrol.FromRawOpts{ Type: "RP", diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 4321de7e36..3f7c93eabf 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -1167,6 +1167,61 @@ func makeTests() []*TestGroup { ), ), + // Route 53 weighted routing + testgroup("R53_WEIGHT", + only("ROUTE53"), + tc("create weighted A records", + r53weighted("weighted", "1.2.3.4", "A", 70, "web1"), + r53weighted("weighted", "5.6.7.8", "A", 30, "web2"), + ), + tc("change weight", + r53weighted("weighted", "1.2.3.4", "A", 50, "web1"), + r53weighted("weighted", "5.6.7.8", "A", 50, "web2"), + ), + tc("change target of one weighted record", + r53weighted("weighted", "9.10.11.12", "A", 50, "web1"), + r53weighted("weighted", "5.6.7.8", "A", 50, "web2"), + ), + tc("delete one weighted record", + r53weighted("weighted", "5.6.7.8", "A", 50, "web2"), + ), + tc("add back and change set identifier", + r53weighted("weighted", "9.10.11.12", "A", 50, "primary"), + r53weighted("weighted", "5.6.7.8", "A", 50, "secondary"), + ), + ), + + testgroup("R53_WEIGHT_CNAME", + only("ROUTE53"), + tc("create weighted CNAME records", + r53weighted("cdn", "east.cdn.example.com.", "CNAME", 70, "east"), + r53weighted("cdn", "west.cdn.example.com.", "CNAME", 30, "west"), + ), + tc("modify weighted CNAME", + r53weighted("cdn", "east.cdn.example.com.", "CNAME", 50, "east"), + r53weighted("cdn", "west.cdn.example.com.", "CNAME", 50, "west"), + ), + ), + + testgroup("R53_WEIGHT_MIXED", + only("ROUTE53"), + tc("create weighted and non-weighted records", + a("normal", "1.2.3.4"), + r53weighted("weighted", "5.6.7.8", "A", 70, "web1"), + r53weighted("weighted", "9.10.11.12", "A", 30, "web2"), + ), + tc("modify weighted, keep non-weighted", + a("normal", "1.2.3.4"), + r53weighted("weighted", "5.6.7.8", "A", 50, "web1"), + r53weighted("weighted", "9.10.11.12", "A", 50, "web2"), + ), + ), + + // R53_WEIGHT_HEALTH_CHECK: Not included as an integration test because + // health checks are external AWS resources that must be pre-provisioned. + // The R53_HEALTH_CHECK_ID modifier is tested implicitly through the + // provider code and audit validation. + // CLOUDFLAREAPI features // CLOUDFLAREAPI: Redirects: diff --git a/models/record.go b/models/record.go index 4635d6eb8e..3a5ead6f94 100644 --- a/models/record.go +++ b/models/record.go @@ -527,6 +527,12 @@ func (rc *RecordConfig) Key() RecordKey { t = fmt.Sprintf("%s_%s", t, v) } } + // Route 53 weighted/failover routing: records with different + // SetIdentifiers are separate ResourceRecordSets in the R53 API, + // so they must have distinct keys for the diff engine. + if sid, ok := rc.Metadata["r53_set_identifier"]; ok && sid != "" { + t = fmt.Sprintf("%s!%s", t, sid) + } return RecordKey{rc.NameFQDN, t} } diff --git a/pkg/js/helpers.js b/pkg/js/helpers.js index 6fec6e92df..491cd56a5e 100644 --- a/pkg/js/helpers.js +++ b/pkg/js/helpers.js @@ -429,6 +429,37 @@ function R53_EVALUATE_TARGET_HEALTH(enabled) { }; } +// R53_WEIGHT(weight, set_identifier) configures Route 53 weighted routing. +// weight: integer 0-255, set_identifier: unique string within the weighted group. +function R53_WEIGHT(weight, set_identifier) { + if (!_.isNumber(weight) || weight < 0 || weight > 255) { + throw 'R53_WEIGHT: weight must be a number between 0 and 255'; + } + if (!_.isString(set_identifier) || set_identifier === '') { + throw 'R53_WEIGHT: set_identifier must be a non-empty string'; + } + return function (r) { + if (!_.isObject(r.meta)) { + r.meta = {}; + } + r.meta['r53_weight'] = weight.toString(); + r.meta['r53_set_identifier'] = set_identifier; + }; +} + +// R53_HEALTH_CHECK_ID(health_check_id) associates a Route 53 health check with the record. +function R53_HEALTH_CHECK_ID(health_check_id) { + if (!_.isString(health_check_id) || health_check_id === '') { + throw 'R53_HEALTH_CHECK_ID: health_check_id must be a non-empty string'; + } + return function (r) { + if (!_.isObject(r.meta)) { + r.meta = {}; + } + r.meta['r53_health_check_id'] = health_check_id; + }; +} + function validateR53AliasType(value) { if (!_.isString(value)) { return false; diff --git a/providers/route53/auditrecords.go b/providers/route53/auditrecords.go index 4a5d50f432..c00fe189bb 100644 --- a/providers/route53/auditrecords.go +++ b/providers/route53/auditrecords.go @@ -2,6 +2,8 @@ package route53 import ( "errors" + "fmt" + "strconv" "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/rejectif" @@ -14,6 +16,7 @@ func AuditRecords(records []*models.RecordConfig) []error { a := rejectif.Auditor{} a.Add("R53_ALIAS", rejectifTargetEqualsLabel) // Last verified 2023-03-01 + a.Add("*", rejectifInvalidR53Weight) return a.Audit(records) } @@ -22,10 +25,40 @@ func AuditRecords(records []*models.RecordConfig) []error { // since this is ROUTE53-specific, we'll include it here. // rejectifTargetEqualsLabel rejects an ALIAS that would create a loop. - func rejectifTargetEqualsLabel(rc *models.RecordConfig) error { if (rc.GetLabelFQDN() + ".") == rc.GetTargetField() { return errors.New("alias target loop") } return nil } + +// rejectifInvalidR53Weight validates Route 53 weighted routing metadata. +func rejectifInvalidR53Weight(rc *models.RecordConfig) error { + weight := rc.Metadata["r53_weight"] + setID := rc.Metadata["r53_set_identifier"] + + if weight == "" && setID == "" { + return nil + } + + if weight != "" && setID == "" { + return fmt.Errorf("r53_weight is set but r53_set_identifier is missing on %s %s", rc.Type, rc.GetLabelFQDN()) + } + if weight == "" && setID != "" { + return fmt.Errorf("r53_set_identifier is set but r53_weight is missing on %s %s", rc.Type, rc.GetLabelFQDN()) + } + + w, err := strconv.ParseInt(weight, 10, 64) + if err != nil { + return fmt.Errorf("r53_weight %q is not a valid integer on %s %s", weight, rc.Type, rc.GetLabelFQDN()) + } + if w < 0 || w > 255 { + return fmt.Errorf("r53_weight %d must be between 0 and 255 on %s %s", w, rc.Type, rc.GetLabelFQDN()) + } + + if len(setID) > 128 { + return fmt.Errorf("r53_set_identifier must be 128 characters or fewer on %s %s", rc.Type, rc.GetLabelFQDN()) + } + + return nil +} diff --git a/providers/route53/route53Provider.go b/providers/route53/route53Provider.go index 5c466e7dbb..04021bb6ba 100644 --- a/providers/route53/route53Provider.go +++ b/providers/route53/route53Provider.go @@ -335,7 +335,7 @@ func (r *route53Provider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi // Amazon Route53 is a "ByRecordSet" API. // At each label:rtype pair, we either delete all records or UPSERT the desired records. - instructions, actualChangeCount, err := diff2.ByRecordSet(existingRecords, dc, nil) + instructions, actualChangeCount, err := diff2.ByRecordSet(existingRecords, dc, r53ComparableFunc) if err != nil { return nil, 0, err } @@ -345,6 +345,14 @@ func (r *route53Provider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi for _, inst := range instructions { instNameFQDN := inst.Key.NameFQDN instType := inst.Key.Type + + // Strip set identifier suffix added by Key() for weighted routing. + setIdentifier := "" + if idx := strings.Index(instType, "!"); idx != -1 { + setIdentifier = instType[idx+1:] + instType = instType[:idx] + } + var chg r53Types.Change switch inst.Type { @@ -386,6 +394,13 @@ func (r *route53Provider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi rrset.TTL = &i } } + + // Apply weighted routing policy fields from record metadata. + if setIdentifier != "" { + rrset.SetIdentifier = aws.String(setIdentifier) + applyR53RoutingFieldsToRRSet(rrset, inst.New[0]) + } + chg = r53Types.Change{ Action: r53Types.ChangeActionUpsert, ResourceRecordSet: rrset, @@ -456,6 +471,7 @@ func nativeToRecords(set r53Types.ResourceRecordSet, origin string) ([]*models.R if err := rc.SetTarget(aws.ToString(set.AliasTarget.DNSName)); err != nil { return nil, err } + applyR53RoutingMeta(rc, set) // rc.Original stores a pointer to the original set for use by // r53Types.ChangeActionDelete and anything else that needs the // native record verbatim. @@ -507,6 +523,7 @@ func nativeToRecords(set r53Types.ResourceRecordSet, origin string) ([]*models.R if err := rc.PopulateFromStringFunc(rtypeString, val, origin, txtutil.ParseQuoted); err != nil { return nil, fmt.Errorf("unparsable record type=%q received from ROUTE53: %w", rtypeString, err) } + applyR53RoutingMeta(rc, set) results = append(results, rc) } @@ -515,6 +532,51 @@ func nativeToRecords(set r53Types.ResourceRecordSet, origin string) ([]*models.R return results, nil } +// applyR53RoutingMeta populates RecordConfig metadata from native Route 53 +// routing-policy fields (SetIdentifier, Weight, HealthCheckId). +func applyR53RoutingMeta(rc *models.RecordConfig, set r53Types.ResourceRecordSet) { + if set.SetIdentifier == nil { + return + } + if rc.Metadata == nil { + rc.Metadata = map[string]string{} + } + rc.Metadata["r53_set_identifier"] = aws.ToString(set.SetIdentifier) + if set.Weight != nil { + rc.Metadata["r53_weight"] = strconv.FormatInt(*set.Weight, 10) + } + if set.HealthCheckId != nil { + rc.Metadata["r53_health_check_id"] = aws.ToString(set.HealthCheckId) + } +} + +// r53ComparableFunc includes Route 53 routing-policy metadata in record +// comparison so that changes to weight or health check are detected by the diff. +func r53ComparableFunc(rc *models.RecordConfig) string { + var parts []string + if w, ok := rc.Metadata["r53_weight"]; ok && w != "" { + parts = append(parts, "r53_weight="+w) + } + if hc, ok := rc.Metadata["r53_health_check_id"]; ok && hc != "" { + parts = append(parts, "r53_health_check_id="+hc) + } + return strings.Join(parts, ",") +} + +// applyR53RoutingFieldsToRRSet sets the Route 53 weighted routing fields on a +// ResourceRecordSet based on the RecordConfig metadata. +func applyR53RoutingFieldsToRRSet(rrset *r53Types.ResourceRecordSet, rc *models.RecordConfig) { + if w, ok := rc.Metadata["r53_weight"]; ok && w != "" { + weight, err := strconv.ParseInt(w, 10, 64) + if err == nil { + rrset.Weight = &weight + } + } + if hc, ok := rc.Metadata["r53_health_check_id"]; ok && hc != "" { + rrset.HealthCheckId = aws.String(hc) + } +} + func aliasToRRSet(zone r53Types.HostedZone, r *models.RecordConfig) *r53Types.ResourceRecordSet { target := r.GetTargetField() zoneID := getZoneID(zone, r) @@ -623,13 +685,15 @@ func (r *route53Provider) fetchRecordSets(zoneID *string) ([]r53Types.ResourceRe } var next *string var nextType r53Types.RRType + var nextIdentifier *string var records []r53Types.ResourceRecordSet for { listInput := &r53.ListResourceRecordSetsInput{ - HostedZoneId: zoneID, - StartRecordName: next, - StartRecordType: nextType, - MaxItems: aws.Int32(100), + HostedZoneId: zoneID, + StartRecordName: next, + StartRecordType: nextType, + StartRecordIdentifier: nextIdentifier, + MaxItems: aws.Int32(100), } var list *r53.ListResourceRecordSetsOutput var err error @@ -645,6 +709,7 @@ func (r *route53Provider) fetchRecordSets(zoneID *string) ([]r53Types.ResourceRe if list.NextRecordName != nil { next = list.NextRecordName nextType = list.NextRecordType + nextIdentifier = list.NextRecordIdentifier } else { break } From 9dc7acc7aa115c6a38a026b21236b8bde0a3364b Mon Sep 17 00:00:00 2001 From: Povilas Ramonas Date: Wed, 18 Mar 2026 16:34:32 +0200 Subject: [PATCH 2/4] bin/generate-all.sh linter changes --- commands/types/dnscontrol.d.ts | 83 +++++++++++++++++++++ integrationTest/helpers_integration_test.go | 6 +- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/commands/types/dnscontrol.d.ts b/commands/types/dnscontrol.d.ts index f81a2f5238..287cc30317 100644 --- a/commands/types/dnscontrol.d.ts +++ b/commands/types/dnscontrol.d.ts @@ -3389,6 +3389,89 @@ declare function R53_ALIAS(name: string, target: string, zone_idModifier: Domain */ declare function R53_EVALUATE_TARGET_HEALTH(enabled: boolean): RecordModifier; +/** + * `R53_HEALTH_CHECK_ID` associates a [Route 53 health check](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/health-checks-creating.html) with a record. This is typically used in combination with [`R53_WEIGHT()`](R53_WEIGHT.md) for weighted routing, so that Route 53 stops routing traffic to unhealthy endpoints. + * + * The `health_check_id` is the ID of a Route 53 health check that you create separately (e.g. via the AWS Console, CLI, or Terraform). DNSControl does not manage the health checks themselves, only their association with DNS records. + * + * ```javascript + * var REG_NONE = NewRegistrar("none"); + * var DSP_R53 = NewDnsProvider("r53_main"); + * + * D("example.com", REG_NONE, DnsProvider(DSP_R53), + * A("www", "1.2.3.4", + * R53_WEIGHT(70, "primary"), + * R53_HEALTH_CHECK_ID("12345678-1234-1234-1234-123456789012"), + * ), + * A("www", "5.6.7.8", + * R53_WEIGHT(30, "secondary"), + * R53_HEALTH_CHECK_ID("87654321-4321-4321-4321-210987654321"), + * ), + * ); + * ``` + * + * @see https://docs.dnscontrol.org/language-reference/record-modifiers/service-provider-specific/amazon-route-53/r53_health_check_id + */ +declare function R53_HEALTH_CHECK_ID(health_check_id: string): RecordModifier; + +/** + * `R53_WEIGHT` configures [Route 53 weighted routing](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-weighted.html) for a record. + * + * Weighted routing lets you associate multiple resources with a single domain name and control the proportion of traffic that is routed to each resource. + * + * - `weight`: An integer between 0 and 255. Route 53 distributes traffic proportionally based on the weights assigned to each record with the same name and type. + * - `set_identifier`: A unique string that differentiates this record from other records with the same name and type. Each weighted record in a group must have a unique set identifier. + * + * ```javascript + * var REG_NONE = NewRegistrar("none"); + * var DSP_R53 = NewDnsProvider("r53_main"); + * + * D("example.com", REG_NONE, DnsProvider(DSP_R53), + * // 70% of traffic goes to 1.2.3.4, 30% to 5.6.7.8 + * A("www", "1.2.3.4", R53_WEIGHT(70, "web-east")), + * A("www", "5.6.7.8", R53_WEIGHT(30, "web-west")), + * ); + * ``` + * + * `R53_WEIGHT` can be used with any record type supported by Route 53 weighted routing, including `A`, `AAAA`, `CNAME`, `TXT`, and [`R53_ALIAS()`](../domain-modifiers/R53_ALIAS.md). + * + * ```javascript + * D("example.com", REG_NONE, DnsProvider(DSP_R53), + * // Weighted CNAME records + * CNAME("cdn", "east.cdn.example.com.", R53_WEIGHT(70, "cdn-east")), + * CNAME("cdn", "west.cdn.example.com.", R53_WEIGHT(30, "cdn-west")), + * + * // Weighted R53_ALIAS records + * R53_ALIAS("api", "A", "alb-east.us-east-1.elb.amazonaws.com.", + * R53_WEIGHT(60, "api-east"), + * R53_ZONE("Z35SXDOTRQ7X7K"), + * ), + * R53_ALIAS("api", "A", "alb-west.us-west-2.elb.amazonaws.com.", + * R53_WEIGHT(40, "api-west"), + * R53_ZONE("Z1H1FL5HABSF5"), + * ), + * ); + * ``` + * + * You can optionally add a health check using [`R53_HEALTH_CHECK_ID()`](R53_HEALTH_CHECK_ID.md) to remove unhealthy endpoints from the rotation. + * + * ```javascript + * D("example.com", REG_NONE, DnsProvider(DSP_R53), + * A("www", "1.2.3.4", + * R53_WEIGHT(70, "web-east"), + * R53_HEALTH_CHECK_ID("12345678-1234-1234-1234-123456789012"), + * ), + * A("www", "5.6.7.8", + * R53_WEIGHT(30, "web-west"), + * R53_HEALTH_CHECK_ID("87654321-4321-4321-4321-210987654321"), + * ), + * ); + * ``` + * + * @see https://docs.dnscontrol.org/language-reference/record-modifiers/service-provider-specific/amazon-route-53/r53_weight + */ +declare function R53_WEIGHT(weight: number, set_identifier: string): RecordModifier; + /** * `R53_ZONE` lets you specify the AWS Zone ID for an entire domain ([`D()`](../top-level-functions/D.md)) or a specific [`R53_ALIAS()`](../domain-modifiers/R53_ALIAS.md) record. * diff --git a/integrationTest/helpers_integration_test.go b/integrationTest/helpers_integration_test.go index f9bafabe0b..416ca43d36 100644 --- a/integrationTest/helpers_integration_test.go +++ b/integrationTest/helpers_integration_test.go @@ -616,9 +616,9 @@ func r53weighted(name, target, rtype string, weight int, setID string) *models.R func r53weightedHealthCheck(name, target, rtype string, weight int, setID, healthCheckID string) *models.RecordConfig { r := makeRec(name, target, rtype) r.Metadata = map[string]string{ - "r53_weight": fmt.Sprintf("%d", weight), - "r53_set_identifier": setID, - "r53_health_check_id": healthCheckID, + "r53_weight": fmt.Sprintf("%d", weight), + "r53_set_identifier": setID, + "r53_health_check_id": healthCheckID, } return r } From ebfec854be28b7505972e211cb64481a7d30b016 Mon Sep 17 00:00:00 2001 From: Povilas Ramonas Date: Thu, 19 Mar 2026 13:14:33 +0200 Subject: [PATCH 3/4] validate Route 53 weighted record consistency --- integrationTest/helpers_integration_test.go | 10 --- pkg/normalize/validate.go | 35 ++++++++ pkg/normalize/validate_test.go | 96 +++++++++++++++++++++ 3 files changed, 131 insertions(+), 10 deletions(-) diff --git a/integrationTest/helpers_integration_test.go b/integrationTest/helpers_integration_test.go index 416ca43d36..2779c00086 100644 --- a/integrationTest/helpers_integration_test.go +++ b/integrationTest/helpers_integration_test.go @@ -613,16 +613,6 @@ func r53weighted(name, target, rtype string, weight int, setID string) *models.R return r } -func r53weightedHealthCheck(name, target, rtype string, weight int, setID, healthCheckID string) *models.RecordConfig { - r := makeRec(name, target, rtype) - r.Metadata = map[string]string{ - "r53_weight": fmt.Sprintf("%d", weight), - "r53_set_identifier": setID, - "r53_health_check_id": healthCheckID, - } - return r -} - func rp(name string, m, t string) *models.RecordConfig { rec, err := rtypecontrol.NewRecordConfigFromRaw(rtypecontrol.FromRawOpts{ Type: "RP", diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go index 7431e56750..988ed875b4 100644 --- a/pkg/normalize/validate.go +++ b/pkg/normalize/validate.go @@ -567,6 +567,8 @@ func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) { errs = append(errs, checkDuplicates(d.Records)...) // Check for different TTLs under the same label errs = append(errs, checkRecordSetHasMultipleTTLs(d.Records)...) + // Check for inconsistent R53 weighted routing metadata within a group + errs = append(errs, checkR53WeightedGroupConsistency(d.Records)...) // Validate FQDN consistency for _, r := range d.Records { if r.NameFQDN == "" || !strings.HasSuffix(r.NameFQDN, d.Name) { @@ -755,6 +757,39 @@ func commaSepInts(list []int) string { return strings.Join(slist, ",") } +// checkR53WeightedGroupConsistency validates that all records sharing the same +// label+type+set_identifier have identical weight and health_check_id, since +// they map to a single Route 53 ResourceRecordSet. +func checkR53WeightedGroupConsistency(records []*models.RecordConfig) (errs []error) { + type groupMeta struct { + weight string + healthCheck string + } + groups := map[string]groupMeta{} + + for _, rc := range records { + sid := rc.Metadata["r53_set_identifier"] + if sid == "" { + continue + } + key := rc.GetLabelFQDN() + ":" + rc.Type + "!" + sid + w := rc.Metadata["r53_weight"] + hc := rc.Metadata["r53_health_check_id"] + + if existing, ok := groups[key]; ok { + if existing.weight != w { + errs = append(errs, fmt.Errorf("R53 weighted group %q at %s %s has inconsistent weights (%s vs %s)", sid, rc.Type, rc.GetLabelFQDN(), existing.weight, w)) + } + if existing.healthCheck != hc { + errs = append(errs, fmt.Errorf("R53 weighted group %q at %s %s has inconsistent health check IDs (%s vs %s)", sid, rc.Type, rc.GetLabelFQDN(), existing.healthCheck, hc)) + } + } else { + groups[key] = groupMeta{weight: w, healthCheck: hc} + } + } + return errs +} + // We pull this out of checkProviderCapabilities() so that it's visible within // the package elsewhere, so that our test suite can look at the list of // capabilities we're checking and make sure that it's up-to-date. diff --git a/pkg/normalize/validate_test.go b/pkg/normalize/validate_test.go index 383f74b72f..a09a273e58 100644 --- a/pkg/normalize/validate_test.go +++ b/pkg/normalize/validate_test.go @@ -581,6 +581,102 @@ func Test_DSChecks(t *testing.T) { }) } +func TestCheckR53WeightedGroupConsistency_noerr_consistent(t *testing.T) { + records := []*models.RecordConfig{ + makeRC("@", "example.com", "1.2.3.4", models.RecordConfig{ + Type: "A", + Metadata: map[string]string{"r53_weight": "70", "r53_set_identifier": "primary", "r53_health_check_id": "hc-1"}, + }), + makeRC("@", "example.com", "2.3.4.5", models.RecordConfig{ + Type: "A", + Metadata: map[string]string{"r53_weight": "70", "r53_set_identifier": "primary", "r53_health_check_id": "hc-1"}, + }), + } + errs := checkR53WeightedGroupConsistency(records) + if len(errs) != 0 { + t.Errorf("Expected 0 errors but got %d: %v", len(errs), errs) + } +} + +func TestCheckR53WeightedGroupConsistency_noerr_different_set_ids(t *testing.T) { + records := []*models.RecordConfig{ + makeRC("@", "example.com", "1.2.3.4", models.RecordConfig{ + Type: "A", + Metadata: map[string]string{"r53_weight": "70", "r53_set_identifier": "primary"}, + }), + makeRC("@", "example.com", "5.6.7.8", models.RecordConfig{ + Type: "A", + Metadata: map[string]string{"r53_weight": "30", "r53_set_identifier": "secondary"}, + }), + } + errs := checkR53WeightedGroupConsistency(records) + if len(errs) != 0 { + t.Errorf("Expected 0 errors but got %d: %v", len(errs), errs) + } +} + +func TestCheckR53WeightedGroupConsistency_noerr_no_metadata(t *testing.T) { + records := []*models.RecordConfig{ + makeRC("@", "example.com", "1.2.3.4", models.RecordConfig{Type: "A"}), + makeRC("@", "example.com", "5.6.7.8", models.RecordConfig{Type: "A"}), + } + errs := checkR53WeightedGroupConsistency(records) + if len(errs) != 0 { + t.Errorf("Expected 0 errors but got %d: %v", len(errs), errs) + } +} + +func TestCheckR53WeightedGroupConsistency_err_different_weights(t *testing.T) { + records := []*models.RecordConfig{ + makeRC("@", "example.com", "1.2.3.4", models.RecordConfig{ + Type: "A", + Metadata: map[string]string{"r53_weight": "70", "r53_set_identifier": "primary"}, + }), + makeRC("@", "example.com", "2.3.4.5", models.RecordConfig{ + Type: "A", + Metadata: map[string]string{"r53_weight": "50", "r53_set_identifier": "primary"}, + }), + } + errs := checkR53WeightedGroupConsistency(records) + if len(errs) != 1 { + t.Errorf("Expected 1 error for inconsistent weights but got %d: %v", len(errs), errs) + } +} + +func TestCheckR53WeightedGroupConsistency_err_different_health_checks(t *testing.T) { + records := []*models.RecordConfig{ + makeRC("@", "example.com", "1.2.3.4", models.RecordConfig{ + Type: "A", + Metadata: map[string]string{"r53_weight": "70", "r53_set_identifier": "primary", "r53_health_check_id": "hc-1"}, + }), + makeRC("@", "example.com", "2.3.4.5", models.RecordConfig{ + Type: "A", + Metadata: map[string]string{"r53_weight": "70", "r53_set_identifier": "primary", "r53_health_check_id": "hc-2"}, + }), + } + errs := checkR53WeightedGroupConsistency(records) + if len(errs) != 1 { + t.Errorf("Expected 1 error for inconsistent health checks but got %d: %v", len(errs), errs) + } +} + +func TestCheckR53WeightedGroupConsistency_err_both_inconsistent(t *testing.T) { + records := []*models.RecordConfig{ + makeRC("@", "example.com", "1.2.3.4", models.RecordConfig{ + Type: "A", + Metadata: map[string]string{"r53_weight": "70", "r53_set_identifier": "primary", "r53_health_check_id": "hc-1"}, + }), + makeRC("@", "example.com", "2.3.4.5", models.RecordConfig{ + Type: "A", + Metadata: map[string]string{"r53_weight": "50", "r53_set_identifier": "primary", "r53_health_check_id": "hc-2"}, + }), + } + errs := checkR53WeightedGroupConsistency(records) + if len(errs) != 2 { + t.Errorf("Expected 2 errors (weight + health check) but got %d: %v", len(errs), errs) + } +} + func Test_errorRepeat(t *testing.T) { type args struct { label string From ad26f8ff6d0381a159714c4db55d7d3d7aef7321 Mon Sep 17 00:00:00 2001 From: Povilas Ramonas Date: Thu, 19 Mar 2026 13:15:46 +0200 Subject: [PATCH 4/4] document Route 53 weighted record consistency and update types --- commands/types/dnscontrol.d.ts | 76 +++++-------------- .../record-modifiers/R53_HEALTH_CHECK_ID.md | 17 +---- .../record-modifiers/R53_WEIGHT.md | 69 +++++------------ documentation/provider/route53.md | 37 ++++----- 4 files changed, 64 insertions(+), 135 deletions(-) diff --git a/commands/types/dnscontrol.d.ts b/commands/types/dnscontrol.d.ts index 287cc30317..8bdf8b9e21 100644 --- a/commands/types/dnscontrol.d.ts +++ b/commands/types/dnscontrol.d.ts @@ -3390,23 +3390,14 @@ declare function R53_ALIAS(name: string, target: string, zone_idModifier: Domain declare function R53_EVALUATE_TARGET_HEALTH(enabled: boolean): RecordModifier; /** - * `R53_HEALTH_CHECK_ID` associates a [Route 53 health check](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/health-checks-creating.html) with a record. This is typically used in combination with [`R53_WEIGHT()`](R53_WEIGHT.md) for weighted routing, so that Route 53 stops routing traffic to unhealthy endpoints. + * `R53_HEALTH_CHECK_ID` associates a [Route 53 health check](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/health-checks-creating.html) with a record. This is typically used with [`R53_WEIGHT()`](R53_WEIGHT.md) so that Route 53 stops routing traffic to unhealthy endpoints. * * The `health_check_id` is the ID of a Route 53 health check that you create separately (e.g. via the AWS Console, CLI, or Terraform). DNSControl does not manage the health checks themselves, only their association with DNS records. * * ```javascript - * var REG_NONE = NewRegistrar("none"); - * var DSP_R53 = NewDnsProvider("r53_main"); - * - * D("example.com", REG_NONE, DnsProvider(DSP_R53), - * A("www", "1.2.3.4", - * R53_WEIGHT(70, "primary"), - * R53_HEALTH_CHECK_ID("12345678-1234-1234-1234-123456789012"), - * ), - * A("www", "5.6.7.8", - * R53_WEIGHT(30, "secondary"), - * R53_HEALTH_CHECK_ID("87654321-4321-4321-4321-210987654321"), - * ), + * D("example.com", REG_MY_PROVIDER, DnsProvider("ROUTE53"), + * A("www", "1.2.3.4", R53_WEIGHT(70, "primary"), R53_HEALTH_CHECK_ID("12345678-1234-1234-1234-123456789012")), + * A("www", "5.6.7.8", R53_WEIGHT(30, "secondary"), R53_HEALTH_CHECK_ID("87654321-4321-4321-4321-210987654321")), * ); * ``` * @@ -3415,56 +3406,31 @@ declare function R53_EVALUATE_TARGET_HEALTH(enabled: boolean): RecordModifier; declare function R53_HEALTH_CHECK_ID(health_check_id: string): RecordModifier; /** - * `R53_WEIGHT` configures [Route 53 weighted routing](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-weighted.html) for a record. + * `R53_WEIGHT` configures [Route 53 weighted routing](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-weighted.html) for a record. It distributes traffic across multiple resources based on the weights you assign. * - * Weighted routing lets you associate multiple resources with a single domain name and control the proportion of traffic that is routed to each resource. + * `weight` is an integer between 0 and 255. Route 53 distributes traffic proportionally based on the weights assigned to each record with the same name and type. A weight of 0 means no traffic is routed to that resource unless all other records also have weight 0. * - * - `weight`: An integer between 0 and 255. Route 53 distributes traffic proportionally based on the weights assigned to each record with the same name and type. - * - `set_identifier`: A unique string that differentiates this record from other records with the same name and type. Each weighted record in a group must have a unique set identifier. + * `set_identifier` is a unique string that differentiates this record from other weighted records with the same name and type. * - * ```javascript - * var REG_NONE = NewRegistrar("none"); - * var DSP_R53 = NewDnsProvider("r53_main"); - * - * D("example.com", REG_NONE, DnsProvider(DSP_R53), - * // 70% of traffic goes to 1.2.3.4, 30% to 5.6.7.8 - * A("www", "1.2.3.4", R53_WEIGHT(70, "web-east")), - * A("www", "5.6.7.8", R53_WEIGHT(30, "web-west")), - * ); - * ``` - * - * `R53_WEIGHT` can be used with any record type supported by Route 53 weighted routing, including `A`, `AAAA`, `CNAME`, `TXT`, and [`R53_ALIAS()`](../domain-modifiers/R53_ALIAS.md). + * You can optionally associate a health check using [`R53_HEALTH_CHECK_ID()`](R53_HEALTH_CHECK_ID.md) to remove unhealthy endpoints from the rotation. * * ```javascript - * D("example.com", REG_NONE, DnsProvider(DSP_R53), - * // Weighted CNAME records - * CNAME("cdn", "east.cdn.example.com.", R53_WEIGHT(70, "cdn-east")), - * CNAME("cdn", "west.cdn.example.com.", R53_WEIGHT(30, "cdn-west")), + * D("example.com", REG_MY_PROVIDER, DnsProvider("ROUTE53"), + * // 70% of traffic to east, 30% to west + * A("www", "1.2.3.4", R53_WEIGHT(70, "web-east")), + * A("www", "5.6.7.8", R53_WEIGHT(30, "web-west")), * - * // Weighted R53_ALIAS records - * R53_ALIAS("api", "A", "alb-east.us-east-1.elb.amazonaws.com.", - * R53_WEIGHT(60, "api-east"), - * R53_ZONE("Z35SXDOTRQ7X7K"), - * ), - * R53_ALIAS("api", "A", "alb-west.us-west-2.elb.amazonaws.com.", - * R53_WEIGHT(40, "api-west"), - * R53_ZONE("Z1H1FL5HABSF5"), - * ), - * ); - * ``` + * // Weighted CNAME records + * CNAME("cdn", "east.cdn.example.com.", R53_WEIGHT(70, "cdn-east")), + * CNAME("cdn", "west.cdn.example.com.", R53_WEIGHT(30, "cdn-west")), * - * You can optionally add a health check using [`R53_HEALTH_CHECK_ID()`](R53_HEALTH_CHECK_ID.md) to remove unhealthy endpoints from the rotation. + * // Weighted R53_ALIAS records + * R53_ALIAS("api", "A", "alb-east.us-east-1.elb.amazonaws.com.", R53_ZONE("Z35SXDOTRQ7X7K"), R53_WEIGHT(60, "api-east")), + * R53_ALIAS("api", "A", "alb-west.us-west-2.elb.amazonaws.com.", R53_ZONE("Z1H1FL5HABSF5"), R53_WEIGHT(40, "api-west")), * - * ```javascript - * D("example.com", REG_NONE, DnsProvider(DSP_R53), - * A("www", "1.2.3.4", - * R53_WEIGHT(70, "web-east"), - * R53_HEALTH_CHECK_ID("12345678-1234-1234-1234-123456789012"), - * ), - * A("www", "5.6.7.8", - * R53_WEIGHT(30, "web-west"), - * R53_HEALTH_CHECK_ID("87654321-4321-4321-4321-210987654321"), - * ), + * // With health checks + * A("api", "10.0.1.1", R53_WEIGHT(50, "api-primary"), R53_HEALTH_CHECK_ID("12345678-1234-1234-1234-123456789012")), + * A("api", "10.0.2.1", R53_WEIGHT(50, "api-secondary"), R53_HEALTH_CHECK_ID("87654321-4321-4321-4321-210987654321")), * ); * ``` * diff --git a/documentation/language-reference/record-modifiers/R53_HEALTH_CHECK_ID.md b/documentation/language-reference/record-modifiers/R53_HEALTH_CHECK_ID.md index d2a44af952..dd446a1625 100644 --- a/documentation/language-reference/record-modifiers/R53_HEALTH_CHECK_ID.md +++ b/documentation/language-reference/record-modifiers/R53_HEALTH_CHECK_ID.md @@ -8,24 +8,15 @@ ts_return: RecordModifier provider: ROUTE53 --- -`R53_HEALTH_CHECK_ID` associates a [Route 53 health check](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/health-checks-creating.html) with a record. This is typically used in combination with [`R53_WEIGHT()`](R53_WEIGHT.md) for weighted routing, so that Route 53 stops routing traffic to unhealthy endpoints. +`R53_HEALTH_CHECK_ID` associates a [Route 53 health check](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/health-checks-creating.html) with a record. This is typically used with [`R53_WEIGHT()`](R53_WEIGHT.md) so that Route 53 stops routing traffic to unhealthy endpoints. The `health_check_id` is the ID of a Route 53 health check that you create separately (e.g. via the AWS Console, CLI, or Terraform). DNSControl does not manage the health checks themselves, only their association with DNS records. {% code title="dnsconfig.js" %} ```javascript -var REG_NONE = NewRegistrar("none"); -var DSP_R53 = NewDnsProvider("r53_main"); - -D("example.com", REG_NONE, DnsProvider(DSP_R53), - A("www", "1.2.3.4", - R53_WEIGHT(70, "primary"), - R53_HEALTH_CHECK_ID("12345678-1234-1234-1234-123456789012"), - ), - A("www", "5.6.7.8", - R53_WEIGHT(30, "secondary"), - R53_HEALTH_CHECK_ID("87654321-4321-4321-4321-210987654321"), - ), +D("example.com", REG_MY_PROVIDER, DnsProvider("ROUTE53"), + A("www", "1.2.3.4", R53_WEIGHT(70, "primary"), R53_HEALTH_CHECK_ID("12345678-1234-1234-1234-123456789012")), + A("www", "5.6.7.8", R53_WEIGHT(30, "secondary"), R53_HEALTH_CHECK_ID("87654321-4321-4321-4321-210987654321")), ); ``` {% endcode %} diff --git a/documentation/language-reference/record-modifiers/R53_WEIGHT.md b/documentation/language-reference/record-modifiers/R53_WEIGHT.md index d8d17bc6b4..744403ab33 100644 --- a/documentation/language-reference/record-modifiers/R53_WEIGHT.md +++ b/documentation/language-reference/record-modifiers/R53_WEIGHT.md @@ -10,61 +10,32 @@ ts_return: RecordModifier provider: ROUTE53 --- -`R53_WEIGHT` configures [Route 53 weighted routing](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-weighted.html) for a record. +`R53_WEIGHT` configures [Route 53 weighted routing](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-weighted.html) for a record. It distributes traffic across multiple resources based on the weights you assign. -Weighted routing lets you associate multiple resources with a single domain name and control the proportion of traffic that is routed to each resource. +`weight` is an integer between 0 and 255. Route 53 distributes traffic proportionally based on the weights assigned to each record with the same name and type. A weight of 0 means no traffic is routed to that resource unless all other records also have weight 0. -- `weight`: An integer between 0 and 255. Route 53 distributes traffic proportionally based on the weights assigned to each record with the same name and type. -- `set_identifier`: A unique string that differentiates this record from other records with the same name and type. Each weighted record in a group must have a unique set identifier. +`set_identifier` is a unique string that differentiates this record from other weighted records with the same name and type. -{% code title="dnsconfig.js" %} -```javascript -var REG_NONE = NewRegistrar("none"); -var DSP_R53 = NewDnsProvider("r53_main"); - -D("example.com", REG_NONE, DnsProvider(DSP_R53), - // 70% of traffic goes to 1.2.3.4, 30% to 5.6.7.8 - A("www", "1.2.3.4", R53_WEIGHT(70, "web-east")), - A("www", "5.6.7.8", R53_WEIGHT(30, "web-west")), -); -``` -{% endcode %} - -`R53_WEIGHT` can be used with any record type supported by Route 53 weighted routing, including `A`, `AAAA`, `CNAME`, `TXT`, and [`R53_ALIAS()`](../domain-modifiers/R53_ALIAS.md). - -{% code title="dnsconfig.js" %} -```javascript -D("example.com", REG_NONE, DnsProvider(DSP_R53), - // Weighted CNAME records - CNAME("cdn", "east.cdn.example.com.", R53_WEIGHT(70, "cdn-east")), - CNAME("cdn", "west.cdn.example.com.", R53_WEIGHT(30, "cdn-west")), - - // Weighted R53_ALIAS records - R53_ALIAS("api", "A", "alb-east.us-east-1.elb.amazonaws.com.", - R53_WEIGHT(60, "api-east"), - R53_ZONE("Z35SXDOTRQ7X7K"), - ), - R53_ALIAS("api", "A", "alb-west.us-west-2.elb.amazonaws.com.", - R53_WEIGHT(40, "api-west"), - R53_ZONE("Z1H1FL5HABSF5"), - ), -); -``` -{% endcode %} - -You can optionally add a health check using [`R53_HEALTH_CHECK_ID()`](R53_HEALTH_CHECK_ID.md) to remove unhealthy endpoints from the rotation. +You can optionally associate a health check using [`R53_HEALTH_CHECK_ID()`](R53_HEALTH_CHECK_ID.md) to remove unhealthy endpoints from the rotation. {% code title="dnsconfig.js" %} ```javascript -D("example.com", REG_NONE, DnsProvider(DSP_R53), - A("www", "1.2.3.4", - R53_WEIGHT(70, "web-east"), - R53_HEALTH_CHECK_ID("12345678-1234-1234-1234-123456789012"), - ), - A("www", "5.6.7.8", - R53_WEIGHT(30, "web-west"), - R53_HEALTH_CHECK_ID("87654321-4321-4321-4321-210987654321"), - ), +D("example.com", REG_MY_PROVIDER, DnsProvider("ROUTE53"), + // 70% of traffic to east, 30% to west + A("www", "1.2.3.4", R53_WEIGHT(70, "web-east")), + A("www", "5.6.7.8", R53_WEIGHT(30, "web-west")), + + // Weighted CNAME records + CNAME("cdn", "east.cdn.example.com.", R53_WEIGHT(70, "cdn-east")), + CNAME("cdn", "west.cdn.example.com.", R53_WEIGHT(30, "cdn-west")), + + // Weighted R53_ALIAS records + R53_ALIAS("api", "A", "alb-east.us-east-1.elb.amazonaws.com.", R53_ZONE("Z35SXDOTRQ7X7K"), R53_WEIGHT(60, "api-east")), + R53_ALIAS("api", "A", "alb-west.us-west-2.elb.amazonaws.com.", R53_ZONE("Z1H1FL5HABSF5"), R53_WEIGHT(40, "api-west")), + + // With health checks + A("api", "10.0.1.1", R53_WEIGHT(50, "api-primary"), R53_HEALTH_CHECK_ID("12345678-1234-1234-1234-123456789012")), + A("api", "10.0.2.1", R53_WEIGHT(50, "api-secondary"), R53_HEALTH_CHECK_ID("87654321-4321-4321-4321-210987654321")), ); ``` {% endcode %} diff --git a/documentation/provider/route53.md b/documentation/provider/route53.md index 69463f87df..d6ca88077c 100644 --- a/documentation/provider/route53.md +++ b/documentation/provider/route53.md @@ -79,14 +79,12 @@ You can find some other ways to authenticate to Route53 in the [go sdk configura ## Metadata -Record-level metadata: +This provider supports the following record-level metadata, typically set via the [`R53_WEIGHT()`](../language-reference/record-modifiers/R53_WEIGHT.md) and [`R53_HEALTH_CHECK_ID()`](../language-reference/record-modifiers/R53_HEALTH_CHECK_ID.md) record modifiers: - `r53_weight` (0-255): Route 53 weighted routing weight. Must be used with `r53_set_identifier`. - `r53_set_identifier` (string): Unique identifier for a weighted routing record set. Required when using `r53_weight`. - `r53_health_check_id` (string): Route 53 health check ID to associate with the record. -These are typically set using the [`R53_WEIGHT()`](../language-reference/record-modifiers/R53_WEIGHT.md) and [`R53_HEALTH_CHECK_ID()`](../language-reference/record-modifiers/R53_HEALTH_CHECK_ID.md) record modifiers. - ## Usage An example configuration: @@ -129,7 +127,7 @@ D("testzone.net!public", REG_NONE, ## Weighted routing -Route 53 [weighted routing](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-weighted.html) distributes traffic across multiple endpoints based on weights you assign. +Route 53 [weighted routing](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-weighted.html) distributes traffic across multiple endpoints based on weights you assign. Use the [`R53_WEIGHT()`](../language-reference/record-modifiers/R53_WEIGHT.md) record modifier to configure weighted routing. {% code title="dnsconfig.js" %} ```javascript @@ -137,24 +135,27 @@ var REG_NONE = NewRegistrar("none"); var DSP_R53 = NewDnsProvider("r53_main"); D("example.com", REG_NONE, DnsProvider(DSP_R53), - // 70% of traffic goes to 1.2.3.4, 30% to 5.6.7.8 - A("www", "1.2.3.4", R53_WEIGHT(70, "web-east")), - A("www", "5.6.7.8", R53_WEIGHT(30, "web-west")), - - // With health checks - A("api", "10.0.1.1", - R53_WEIGHT(50, "api-primary"), - R53_HEALTH_CHECK_ID("12345678-1234-1234-1234-123456789012"), - ), - A("api", "10.0.2.1", - R53_WEIGHT(50, "api-secondary"), - R53_HEALTH_CHECK_ID("87654321-4321-4321-4321-210987654321"), - ), + A("www", "1.2.3.4", R53_WEIGHT(70, "web-east")), + A("www", "5.6.7.8", R53_WEIGHT(30, "web-west")), ); ``` {% endcode %} -See [`R53_WEIGHT()`](../language-reference/record-modifiers/R53_WEIGHT.md) and [`R53_HEALTH_CHECK_ID()`](../language-reference/record-modifiers/R53_HEALTH_CHECK_ID.md) for full documentation. +## Health checks + +Use the [`R53_HEALTH_CHECK_ID()`](../language-reference/record-modifiers/R53_HEALTH_CHECK_ID.md) record modifier to associate a [Route 53 health check](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/health-checks-creating.html) with a record. Health checks must be created separately (e.g. via the AWS Console, CLI, or Terraform). DNSControl only manages the association. + +{% code title="dnsconfig.js" %} +```javascript +var REG_NONE = NewRegistrar("none"); +var DSP_R53 = NewDnsProvider("r53_main"); + +D("example.com", REG_NONE, DnsProvider(DSP_R53), + A("api", "10.0.1.1", R53_WEIGHT(50, "api-primary"), R53_HEALTH_CHECK_ID("12345678-1234-1234-1234-123456789012")), + A("api", "10.0.2.1", R53_WEIGHT(50, "api-secondary"), R53_HEALTH_CHECK_ID("87654321-4321-4321-4321-210987654321")), +); +``` +{% endcode %} ## Activation DNSControl depends on a standard [AWS access key](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) with permission to list, create and update hosted zones. If you do not have the permissions required you will receive the following error message `Check your credentials, your not authorized to perform actions on Route 53 AWS Service`.