Skip to content
Merged
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
41 changes: 41 additions & 0 deletions docs/annotations/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,47 @@ Specifies the set identifier for DNS records generated by the resource.
A set identifier differentiates among multiple DNS record sets that have the same combination of domain and type.
Which record set or sets are returned to queries is then determined by the configured routing policy.

Required for AWS Route53 routing policies (weighted, latency, failover, geolocation, geoproximity, multi-value).
See the [AWS tutorial — Routing policies](../tutorials/aws.md#routing-policies) for the full list of annotations
and examples.

Notes:

- The annotation is provider-agnostic in design but is primarily used with AWS Route53 routing policies.
- The value is arbitrary but must be **unique per record set** for the same domain and type combination.
- For Gateway API sources, this annotation must be placed on **Route resources** (e.g., `HTTPRoute`), not on
the `Gateway` resource itself. See [Gateway API Annotation Placement](#gateway-api-annotation-placement).

### Gateway API with HTTPRoute

When using Gateway API, place `set-identifier` on the Route resource, not the Gateway:

```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
annotations:
# target goes on the Gateway
external-dns.alpha.kubernetes.io/target: "alb-123.us-east-1.elb.amazonaws.com"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-route
annotations:
# set-identifier and routing policy go on the Route
external-dns.alpha.kubernetes.io/set-identifier: backend-v1
external-dns.alpha.kubernetes.io/aws-weight: "100"
spec:
parentRefs:
- name: my-gateway
hostnames:
- app.example.com
```

> Placing `set-identifier` on the Gateway instead of the Route is a common mistake — the Gateway source only reads the `target` annotation.

## Gateway API Annotation Placement

When using Gateway API sources (`gateway-httproute`, `gateway-grpcroute`, `gateway-tlsroute`, etc.), annotations
Expand Down
84 changes: 84 additions & 0 deletions docs/tutorials/aws.md
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,90 @@ For any given DNS name, only **one** of the following routing policies can be us
- `external-dns.alpha.kubernetes.io/aws-geoproximity-bias`
- Multi-value answer:`external-dns.alpha.kubernetes.io/aws-multi-value-answer`

#### Weighted Routing

Route traffic across two Services by weight. Both share the same hostname but carry different identifiers and weights:

```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service-v1
annotations:
external-dns.alpha.kubernetes.io/hostname: app.example.com
external-dns.alpha.kubernetes.io/set-identifier: app-v1
external-dns.alpha.kubernetes.io/aws-weight: "80"
spec:
type: LoadBalancer
---
apiVersion: v1
kind: Service
metadata:
name: my-service-v2
annotations:
external-dns.alpha.kubernetes.io/hostname: app.example.com
external-dns.alpha.kubernetes.io/set-identifier: app-v2
external-dns.alpha.kubernetes.io/aws-weight: "20"
spec:
type: LoadBalancer
```

> ExternalDNS will create two Route53 weighted record sets for `app.example.com`, sending 80% of traffic to `my-service-v1` and 20% to `my-service-v2`.

#### Failover Routing

Designate a primary and secondary record for active/passive failover:

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress-primary
annotations:
external-dns.alpha.kubernetes.io/set-identifier: my-app-primary
external-dns.alpha.kubernetes.io/aws-failover: PRIMARY
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress-secondary
annotations:
external-dns.alpha.kubernetes.io/set-identifier: my-app-secondary
external-dns.alpha.kubernetes.io/aws-failover: SECONDARY
```

> Route53 will serve the `PRIMARY` record when healthy, and automatically fall back to `SECONDARY` when the health check fails.

#### Latency-Based Routing

Route users to the nearest region by latency:

```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service-us
annotations:
external-dns.alpha.kubernetes.io/hostname: api.example.com
external-dns.alpha.kubernetes.io/set-identifier: api-us-east-1
external-dns.alpha.kubernetes.io/aws-region: us-east-1
spec:
type: LoadBalancer
---
apiVersion: v1
kind: Service
metadata:
name: my-service-eu
annotations:
external-dns.alpha.kubernetes.io/hostname: api.example.com
external-dns.alpha.kubernetes.io/set-identifier: api-eu-west-1
external-dns.alpha.kubernetes.io/aws-region: eu-west-1
spec:
type: LoadBalancer
```

> Route53 will direct each user to the region with the lowest latency.

### Associating DNS records with healthchecks

You can configure Route53 to associate DNS records with healthchecks for automated DNS failover using
Expand Down
21 changes: 21 additions & 0 deletions source/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,27 @@ func TestMergeEndpoints(t *testing.T) {
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, 600, "5.6.7.8"),
},
},
{
name: "same DNSName and RecordType with different SetIdentifier not merged",
input: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("us-east-1"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "5.6.7.8").WithSetIdentifier("eu-west-1"),
},
expected: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("us-east-1"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "5.6.7.8").WithSetIdentifier("eu-west-1"),
},
},
{
name: "same DNSName, RecordType and SetIdentifier targets are merged",
input: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("us-east-1"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "5.6.7.8").WithSetIdentifier("us-east-1"),
},
expected: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4", "5.6.7.8").WithSetIdentifier("us-east-1"),
},
},
}

for _, tt := range tests {
Expand Down
21 changes: 21 additions & 0 deletions source/wrappers/dedupsource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,27 @@ func testDedupEndpoints(t *testing.T) {
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
"two endpoints with same dnsname, same type, same target but different SetIdentifier return two endpoints",
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, SetIdentifier: "us-east-1"},
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, SetIdentifier: "eu-west-1"},
},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, SetIdentifier: "us-east-1"},
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, SetIdentifier: "eu-west-1"},
},
},
{
"two endpoints with same dnsname, same type, same target and same SetIdentifier return one endpoint",
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, SetIdentifier: "us-east-1"},
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, SetIdentifier: "us-east-1"},
},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, SetIdentifier: "us-east-1"},
},
},
{
"no endpoints returns empty endpoints",
[]*endpoint.Endpoint{},
Expand Down
Loading