From 61908636e464bc8166ebe7f727375c4a3bffd956 Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Fri, 27 Feb 2026 08:45:08 +0000 Subject: [PATCH] chore(set-identifier): expand annotation docs and add test coverage for dedup and merge behaviour Signed-off-by: ivan katliarchuk --- docs/annotations/annotations.md | 41 ++++++++++++++ docs/tutorials/aws.md | 84 +++++++++++++++++++++++++++++ source/utils_test.go | 21 ++++++++ source/wrappers/dedupsource_test.go | 21 ++++++++ 4 files changed, 167 insertions(+) diff --git a/docs/annotations/annotations.md b/docs/annotations/annotations.md index 4275c15686..19695a0d07 100644 --- a/docs/annotations/annotations.md +++ b/docs/annotations/annotations.md @@ -320,6 +320,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 diff --git a/docs/tutorials/aws.md b/docs/tutorials/aws.md index 7389542087..2de2e8b2e9 100644 --- a/docs/tutorials/aws.md +++ b/docs/tutorials/aws.md @@ -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 diff --git a/source/utils_test.go b/source/utils_test.go index 540b0a8022..46fa9d6b08 100644 --- a/source/utils_test.go +++ b/source/utils_test.go @@ -293,6 +293,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 { diff --git a/source/wrappers/dedupsource_test.go b/source/wrappers/dedupsource_test.go index 9ebaa2f8a7..ff1ea3e1d0 100644 --- a/source/wrappers/dedupsource_test.go +++ b/source/wrappers/dedupsource_test.go @@ -125,6 +125,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{},