Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 17fa4b4

Browse files
authoredJun 20, 2025··
feat(source): support ttl annotation on pod (#5527)
* feat(source/pod): add support ttl annotation Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * feat(source/pod): add support ttl annotation Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> --------- Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
1 parent 36e3e53 commit 17fa4b4

File tree

4 files changed

+170
-33
lines changed

4 files changed

+170
-33
lines changed
 

‎docs/advanced/ttl.md

Lines changed: 143 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,70 @@ Both examples result in the same value of 60 seconds TTL.
3131

3232
TTL must be a positive value.
3333

34-
## Providers
35-
36-
- [x] AWS (Route53)
37-
- [x] Azure
38-
- [x] Cloudflare
39-
- [x] DigitalOcean
40-
- [x] DNSimple
41-
- [x] Google
42-
- [ ] InMemory
43-
- [x] Linode
44-
- [x] TransIP
45-
- [x] RFC2136
46-
47-
PRs welcome!
34+
## TTL annotation support
35+
36+
> Note: For TTL annotations to work, the `external-dns.alpha.kubernetes.io/hostname` annotation must be set on the resource and be supported by the provider as well as the source.
37+
38+
### Providers
39+
40+
| Provider | Supported |
41+
|:---------------|:---------:|
42+
| `Akamai` ||
43+
| `AlibabaCloud` ||
44+
| `AWS` ||
45+
| `AWSSD` ||
46+
| `Azure` ||
47+
| `Civo` ||
48+
| `Cloudflare` ||
49+
| `CoreDNS` ||
50+
| `DigitalOcean` ||
51+
| `DNSSimple` ||
52+
| `Exoscale` ||
53+
| `Gandi` ||
54+
| `GoDaddy` ||
55+
| `Google GCP` ||
56+
| `InMemory` ||
57+
| `Linode` ||
58+
| `NS1` ||
59+
| `OCI` ||
60+
| `OVH` ||
61+
| `PDNS` ||
62+
| `PiHole` ||
63+
| `Plural` ||
64+
| `RFC2136` ||
65+
| `Scaleway` ||
66+
| `Transip` ||
67+
| `Webhook` ||
68+
69+
### Sources
70+
71+
| Source | Supported |
72+
|:-----------------------|:---------:|
73+
| `ambassador-host` ||
74+
| `cloudfoundry` ||
75+
| `connector` ||
76+
| `contour-httpproxy` ||
77+
| `crd` ||
78+
| `empty` ||
79+
| `f5-transportserver` ||
80+
| `f5-virtualserver` ||
81+
| `fake` ||
82+
| `gateway-grpcroute` ||
83+
| `gateway-httproute` ||
84+
| `gateway-tcproute` ||
85+
| `gateway-tlsroute` ||
86+
| `gateway-udproute` ||
87+
| `gloo-proxy` ||
88+
| `ingress` ||
89+
| `istio-gateway` ||
90+
| `istio-virtualservice` ||
91+
| `kong-tcpingress` ||
92+
| `node` ||
93+
| `openshift-route` ||
94+
| `pod` ||
95+
| `service` ||
96+
| `skipper-routegroup` ||
97+
| `traefik-proxy` ||
4898

4999
## Notes
50100

@@ -89,3 +139,82 @@ The Linode Provider default TTL is used when the TTL is 0. The default is 24 hou
89139
### TransIP Provider
90140

91141
The TransIP Provider minimal TTL is used when the TTL is 0. The minimal TTL is 60s.
142+
143+
## Use Cases for `external-dns.alpha.kubernetes.io/ttl` annotation
144+
145+
The `external-dns.alpha.kubernetes.io/ttl` annotation allows you to set a custom **TTL (Time To Live)** for DNS records managed by `external-dns`.
146+
147+
Use the `external-dns.alpha.kubernetes.io/tt` annotation to fine-tune DNS caching behavior per record, balancing between update frequency and performance.
148+
149+
This is useful in several real-world scenarios depending on how frequently DNS records are expected to change.
150+
151+
---
152+
153+
### Fast Failover for Critical Services
154+
155+
For services that must be highly available—like APIs, databases, or external load balancers—set a **low TTL** (e.g., 30 seconds) so DNS clients quickly update to new IPs during:
156+
157+
- Node failures
158+
- Region failovers
159+
- Blue/green deployments
160+
161+
```yaml
162+
annotations:
163+
external-dns.alpha.kubernetes.io/ttl: "30s"
164+
```
165+
166+
---
167+
168+
### Long TTL for Static Services
169+
170+
If your service’s IP or endpoint rarely changes (e.g., static websites, internal dashboards), you can set a long TTL (e.g., 86400 seconds = 24 hours) to:
171+
172+
- Reduce DNS query load
173+
- Improve cache performance
174+
- Lower cost with some DNS providers
175+
176+
```yml
177+
annotations:
178+
external-dns.alpha.kubernetes.io/ttl: "24h"
179+
```
180+
181+
---
182+
183+
### Canary or Experimental Services
184+
185+
Use a short TTL for services under test or experimentation to allow fast DNS propagation when making changes, allowing easy rollback and testing.
186+
187+
---
188+
189+
### Provider-Specific Optimization
190+
191+
Some DNS providers charge per query or have query rate limits. Adjusting the TTL lets you:
192+
193+
- Reduce costs
194+
- Avoid throttling
195+
- Manage DNS traffic load efficiently
196+
197+
---
198+
199+
### Regulatory or Contractual SLAs
200+
201+
Certain environments may require TTL values to align with:
202+
203+
- Regulatory guidelines
204+
- Legacy system compatibility
205+
- Contractual service-level agreements
206+
207+
---
208+
209+
### Autoscaling Node Pools in GCP (or Other Cloud Providers)
210+
211+
In environments like Google Cloud Platform (GCP) using private node IPs for DNS resolution, ExternalDNS may register node IPs with a default TTL of 300 seconds.
212+
213+
During autoscaling events (e.g., node addition/removal or upgrades), DNS records may remain stale for several minutes, causing traffic to be routed to non-existent nodes.
214+
215+
By using the TTL annotation you can:
216+
217+
- Reduce TTL to allow faster DNS propagation
218+
- Ensure quicker routing updates when node IPs change
219+
- Improve resiliency during frequent cluster topology changes
220+
- Fine-grained TTL control helps avoid downtime or misrouting in dynamic, autoscaling environments.

‎endpoint/endpoint.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ type EndpointKey struct {
212212
DNSName string
213213
RecordType string
214214
SetIdentifier string
215+
RecordTTL TTL
215216
}
216217

217218
// Endpoint is a high-level way of a connection between a service and an IP

‎source/pod.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ func (ps *podSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, error)
129129

130130
var endpoints []*endpoint.Endpoint
131131
for key, targets := range endpointMap {
132-
endpoints = append(endpoints, endpoint.NewEndpoint(key.DNSName, key.RecordType, targets...))
132+
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(key.DNSName, key.RecordType, key.RecordTTL, targets...))
133133
}
134134
return endpoints, nil
135135
}
@@ -153,9 +153,9 @@ func (ps *podSource) addInternalHostnameAnnotationEndpoints(endpointMap map[endp
153153
domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
154154
for _, domain := range domainList {
155155
if len(targets) == 0 {
156-
addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
156+
addToEndpointMap(endpointMap, pod, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
157157
} else {
158-
addTargetsToEndpointMap(endpointMap, targets, domain)
158+
addTargetsToEndpointMap(endpointMap, pod, targets, domain)
159159
}
160160
}
161161
}
@@ -167,7 +167,7 @@ func (ps *podSource) addHostnameAnnotationEndpoints(endpointMap map[endpoint.End
167167
if len(targets) == 0 {
168168
ps.addPodNodeEndpointsToEndpointMap(endpointMap, pod, domainList)
169169
} else {
170-
addTargetsToEndpointMap(endpointMap, targets, domainList...)
170+
addTargetsToEndpointMap(endpointMap, pod, targets, domainList...)
171171
}
172172
}
173173
}
@@ -177,7 +177,7 @@ func (ps *podSource) addKopsDNSControllerEndpoints(endpointMap map[endpoint.Endp
177177
if domainAnnotation, ok := pod.Annotations[kopsDNSControllerInternalHostnameAnnotationKey]; ok {
178178
domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
179179
for _, domain := range domainList {
180-
addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
180+
addToEndpointMap(endpointMap, pod, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
181181
}
182182
}
183183

@@ -192,9 +192,9 @@ func (ps *podSource) addPodSourceDomainEndpoints(endpointMap map[endpoint.Endpoi
192192
if ps.podSourceDomain != "" {
193193
domain := pod.Name + "." + ps.podSourceDomain
194194
if len(targets) == 0 {
195-
addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
195+
addToEndpointMap(endpointMap, pod, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
196196
}
197-
addTargetsToEndpointMap(endpointMap, targets, domain)
197+
addTargetsToEndpointMap(endpointMap, pod, targets, domain)
198198
}
199199
}
200200

@@ -209,7 +209,7 @@ func (ps *podSource) addPodNodeEndpointsToEndpointMap(endpointMap map[endpoint.E
209209
recordType := suitableType(address.Address)
210210
// IPv6 addresses are labeled as NodeInternalIP despite being usable externally as well.
211211
if address.Type == corev1.NodeExternalIP || (address.Type == corev1.NodeInternalIP && recordType == endpoint.RecordTypeAAAA) {
212-
addToEndpointMap(endpointMap, domain, recordType, address.Address)
212+
addToEndpointMap(endpointMap, pod, domain, recordType, address.Address)
213213
}
214214
}
215215
}
@@ -231,6 +231,7 @@ func (ps *podSource) hostsFromTemplate(pod *corev1.Pod) (map[endpoint.EndpointKe
231231
key := endpoint.EndpointKey{
232232
DNSName: target,
233233
RecordType: suitableType(address.IP),
234+
RecordTTL: annotations.TTLFromAnnotations(pod.Annotations, fmt.Sprintf("pod/%s", pod.Name)),
234235
}
235236
result[key] = append(result[key], address.IP)
236237
}
@@ -239,18 +240,19 @@ func (ps *podSource) hostsFromTemplate(pod *corev1.Pod) (map[endpoint.EndpointKe
239240
return result, nil
240241
}
241242

242-
func addTargetsToEndpointMap(endpointMap map[endpoint.EndpointKey][]string, targets []string, domainList ...string) {
243+
func addTargetsToEndpointMap(endpointMap map[endpoint.EndpointKey][]string, pod *corev1.Pod, targets []string, domainList ...string) {
243244
for _, domain := range domainList {
244245
for _, target := range targets {
245-
addToEndpointMap(endpointMap, domain, suitableType(target), target)
246+
addToEndpointMap(endpointMap, pod, domain, suitableType(target), target)
246247
}
247248
}
248249
}
249250

250-
func addToEndpointMap(endpointMap map[endpoint.EndpointKey][]string, domain string, recordType string, address string) {
251+
func addToEndpointMap(endpointMap map[endpoint.EndpointKey][]string, pod *corev1.Pod, domain string, recordType string, address string) {
251252
key := endpoint.EndpointKey{
252253
DNSName: domain,
253254
RecordType: recordType,
255+
RecordTTL: annotations.TTLFromAnnotations(pod.Annotations, fmt.Sprintf("pod/%s", pod.Name)),
254256
}
255257
if _, ok := endpointMap[key]; !ok {
256258
endpointMap[key] = []string{}

‎source/pod_test.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -302,8 +302,8 @@ func TestPodSource(t *testing.T) {
302302
true,
303303
"",
304304
[]*endpoint.Endpoint{
305-
{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"54.10.11.1"}, RecordType: endpoint.RecordTypeA},
306-
{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"2001:DB8::1"}, RecordType: endpoint.RecordTypeAAAA},
305+
{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"54.10.11.1"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(5400)},
306+
{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"2001:DB8::1"}, RecordType: endpoint.RecordTypeAAAA, RecordTTL: endpoint.TTL(5400)},
307307
{DNSName: "b.foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA},
308308
},
309309
false,
@@ -339,6 +339,7 @@ func TestPodSource(t *testing.T) {
339339
Namespace: "kube-system",
340340
Annotations: map[string]string{
341341
hostnameAnnotationKey: "a.foo.example.org",
342+
ttlAnnotationKey: "1h30m",
342343
},
343344
},
344345
Spec: corev1.PodSpec{
@@ -374,8 +375,8 @@ func TestPodSource(t *testing.T) {
374375
true,
375376
"",
376377
[]*endpoint.Endpoint{
377-
{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"54.10.11.1"}, RecordType: endpoint.RecordTypeA},
378-
{DNSName: "internal.a.foo.example.org", Targets: endpoint.Targets{"10.0.1.1"}, RecordType: endpoint.RecordTypeA},
378+
{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"54.10.11.1"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(1)},
379+
{DNSName: "internal.a.foo.example.org", Targets: endpoint.Targets{"10.0.1.1"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(1)},
379380
},
380381
false,
381382
nodesFixturesIPv4(),
@@ -387,6 +388,7 @@ func TestPodSource(t *testing.T) {
387388
Annotations: map[string]string{
388389
internalHostnameAnnotationKey: "internal.a.foo.example.org",
389390
hostnameAnnotationKey: "a.foo.example.org",
391+
ttlAnnotationKey: "1s",
390392
},
391393
},
392394
Spec: corev1.PodSpec{
@@ -453,6 +455,7 @@ func TestPodSource(t *testing.T) {
453455
Annotations: map[string]string{
454456
internalHostnameAnnotationKey: "internal.a.foo.example.org",
455457
hostnameAnnotationKey: "a.foo.example.org",
458+
ttlAnnotationKey: "1s",
456459
},
457460
},
458461
Spec: corev1.PodSpec{
@@ -514,17 +517,19 @@ func TestPodSource(t *testing.T) {
514517
false,
515518
"example.org",
516519
[]*endpoint.Endpoint{
517-
{DNSName: "my-pod1.example.org", Targets: endpoint.Targets{"192.168.1.1"}, RecordType: endpoint.RecordTypeA},
520+
{DNSName: "my-pod1.example.org", Targets: endpoint.Targets{"192.168.1.1"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(60)},
518521
{DNSName: "my-pod2.example.org", Targets: endpoint.Targets{"192.168.1.2"}, RecordType: endpoint.RecordTypeA},
519522
},
520523
false,
521524
nodesFixturesIPv4(),
522525
[]*corev1.Pod{
523526
{
524527
ObjectMeta: metav1.ObjectMeta{
525-
Name: "my-pod1",
526-
Namespace: "kube-system",
527-
Annotations: map[string]string{},
528+
Name: "my-pod1",
529+
Namespace: "kube-system",
530+
Annotations: map[string]string{
531+
ttlAnnotationKey: "1m",
532+
},
528533
},
529534
Spec: corev1.PodSpec{
530535
HostNetwork: false,

0 commit comments

Comments
 (0)
Please sign in to comment.