Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions commands/types/dnscontrol.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3389,6 +3389,55 @@ 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 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
* 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")),
* );
* ```
*
* @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. It distributes traffic across multiple resources based on the weights you assign.
*
* `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.
*
* `set_identifier` is a unique string that differentiates this record from other weighted records with the same name and type.
*
* 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_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")),
* );
* ```
*
* @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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
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 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
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 %}
41 changes: 41 additions & 0 deletions documentation/language-reference/record-modifiers/R53_WEIGHT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
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. It distributes traffic across multiple resources based on the weights you assign.

`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.

`set_identifier` is a unique string that differentiates this record from other weighted records with the same name and type.

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_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 %}
39 changes: 38 additions & 1 deletion documentation/provider/route53.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ 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.

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.

## Usage
An example configuration:
Expand Down Expand Up @@ -120,6 +125,38 @@ 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. Use the [`R53_WEIGHT()`](../language-reference/record-modifiers/R53_WEIGHT.md) record modifier to configure weighted routing.

{% 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, "web-east")),
A("www", "5.6.7.8", R53_WEIGHT(30, "web-west")),
);
```
{% endcode %}

## 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`.

Expand Down
9 changes: 9 additions & 0 deletions integrationTest/helpers_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,15 @@ 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 rp(name string, m, t string) *models.RecordConfig {
rec, err := rtypecontrol.NewRecordConfigFromRaw(rtypecontrol.FromRawOpts{
Type: "RP",
Expand Down
55 changes: 55 additions & 0 deletions integrationTest/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions models/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}

Expand Down
31 changes: 31 additions & 0 deletions pkg/js/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
35 changes: 35 additions & 0 deletions pkg/normalize/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand Down
Loading