mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-05 17:16:59 +02:00
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>
This commit is contained in:
parent
36e3e53190
commit
17fa4b4e7a
@ -31,20 +31,70 @@ Both examples result in the same value of 60 seconds TTL.
|
||||
|
||||
TTL must be a positive value.
|
||||
|
||||
## Providers
|
||||
## TTL annotation support
|
||||
|
||||
- [x] AWS (Route53)
|
||||
- [x] Azure
|
||||
- [x] Cloudflare
|
||||
- [x] DigitalOcean
|
||||
- [x] DNSimple
|
||||
- [x] Google
|
||||
- [ ] InMemory
|
||||
- [x] Linode
|
||||
- [x] TransIP
|
||||
- [x] RFC2136
|
||||
> 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.
|
||||
|
||||
PRs welcome!
|
||||
### Providers
|
||||
|
||||
| Provider | Supported |
|
||||
|:---------------|:---------:|
|
||||
| `Akamai` | ✅ |
|
||||
| `AlibabaCloud` | ✅ |
|
||||
| `AWS` | ✅ |
|
||||
| `AWSSD` | ✅ |
|
||||
| `Azure` | ✅ |
|
||||
| `Civo` | ❌ |
|
||||
| `Cloudflare` | ✅ |
|
||||
| `CoreDNS` | ❌ |
|
||||
| `DigitalOcean` | ✅ |
|
||||
| `DNSSimple` | ✅ |
|
||||
| `Exoscale` | ✅ |
|
||||
| `Gandi` | ✅ |
|
||||
| `GoDaddy` | ✅ |
|
||||
| `Google GCP` | ✅ |
|
||||
| `InMemory` | ❌ |
|
||||
| `Linode` | ❌ |
|
||||
| `NS1` | ❌ |
|
||||
| `OCI` | ✅ |
|
||||
| `OVH` | ❌ |
|
||||
| `PDNS` | ❌ |
|
||||
| `PiHole` | ✅ |
|
||||
| `Plural` | ❌ |
|
||||
| `RFC2136` | ✅ |
|
||||
| `Scaleway` | ✅ |
|
||||
| `Transip` | ✅ |
|
||||
| `Webhook` | ✅ |
|
||||
|
||||
### Sources
|
||||
|
||||
| Source | Supported |
|
||||
|:-----------------------|:---------:|
|
||||
| `ambassador-host` | ✅ |
|
||||
| `cloudfoundry` | ❌ |
|
||||
| `connector` | ❌ |
|
||||
| `contour-httpproxy` | ✅ |
|
||||
| `crd` | ❌ |
|
||||
| `empty` | ❌ |
|
||||
| `f5-transportserver` | ✅ |
|
||||
| `f5-virtualserver` | ✅ |
|
||||
| `fake` | ❌ |
|
||||
| `gateway-grpcroute` | ✅ |
|
||||
| `gateway-httproute` | ✅ |
|
||||
| `gateway-tcproute` | ✅ |
|
||||
| `gateway-tlsroute` | ✅ |
|
||||
| `gateway-udproute` | ✅ |
|
||||
| `gloo-proxy` | ✅ |
|
||||
| `ingress` | ✅ |
|
||||
| `istio-gateway` | ✅ |
|
||||
| `istio-virtualservice` | ✅ |
|
||||
| `kong-tcpingress` | ✅ |
|
||||
| `node` | ✅ |
|
||||
| `openshift-route` | ✅ |
|
||||
| `pod` | ✅ |
|
||||
| `service` | ✅ |
|
||||
| `skipper-routegroup` | ✅ |
|
||||
| `traefik-proxy` | ✅ |
|
||||
|
||||
## Notes
|
||||
|
||||
@ -89,3 +139,82 @@ The Linode Provider default TTL is used when the TTL is 0. The default is 24 hou
|
||||
### TransIP Provider
|
||||
|
||||
The TransIP Provider minimal TTL is used when the TTL is 0. The minimal TTL is 60s.
|
||||
|
||||
## Use Cases for `external-dns.alpha.kubernetes.io/ttl` annotation
|
||||
|
||||
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`.
|
||||
|
||||
Use the `external-dns.alpha.kubernetes.io/tt` annotation to fine-tune DNS caching behavior per record, balancing between update frequency and performance.
|
||||
|
||||
This is useful in several real-world scenarios depending on how frequently DNS records are expected to change.
|
||||
|
||||
---
|
||||
|
||||
### Fast Failover for Critical Services
|
||||
|
||||
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:
|
||||
|
||||
- Node failures
|
||||
- Region failovers
|
||||
- Blue/green deployments
|
||||
|
||||
```yaml
|
||||
annotations:
|
||||
external-dns.alpha.kubernetes.io/ttl: "30s"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Long TTL for Static Services
|
||||
|
||||
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:
|
||||
|
||||
- Reduce DNS query load
|
||||
- Improve cache performance
|
||||
- Lower cost with some DNS providers
|
||||
|
||||
```yml
|
||||
annotations:
|
||||
external-dns.alpha.kubernetes.io/ttl: "24h"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Canary or Experimental Services
|
||||
|
||||
Use a short TTL for services under test or experimentation to allow fast DNS propagation when making changes, allowing easy rollback and testing.
|
||||
|
||||
---
|
||||
|
||||
### Provider-Specific Optimization
|
||||
|
||||
Some DNS providers charge per query or have query rate limits. Adjusting the TTL lets you:
|
||||
|
||||
- Reduce costs
|
||||
- Avoid throttling
|
||||
- Manage DNS traffic load efficiently
|
||||
|
||||
---
|
||||
|
||||
### Regulatory or Contractual SLAs
|
||||
|
||||
Certain environments may require TTL values to align with:
|
||||
|
||||
- Regulatory guidelines
|
||||
- Legacy system compatibility
|
||||
- Contractual service-level agreements
|
||||
|
||||
---
|
||||
|
||||
### Autoscaling Node Pools in GCP (or Other Cloud Providers)
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
By using the TTL annotation you can:
|
||||
|
||||
- Reduce TTL to allow faster DNS propagation
|
||||
- Ensure quicker routing updates when node IPs change
|
||||
- Improve resiliency during frequent cluster topology changes
|
||||
- Fine-grained TTL control helps avoid downtime or misrouting in dynamic, autoscaling environments.
|
||||
|
@ -212,6 +212,7 @@ type EndpointKey struct {
|
||||
DNSName string
|
||||
RecordType string
|
||||
SetIdentifier string
|
||||
RecordTTL TTL
|
||||
}
|
||||
|
||||
// Endpoint is a high-level way of a connection between a service and an IP
|
||||
|
@ -129,7 +129,7 @@ func (ps *podSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, error)
|
||||
|
||||
var endpoints []*endpoint.Endpoint
|
||||
for key, targets := range endpointMap {
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint(key.DNSName, key.RecordType, targets...))
|
||||
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(key.DNSName, key.RecordType, key.RecordTTL, targets...))
|
||||
}
|
||||
return endpoints, nil
|
||||
}
|
||||
@ -153,9 +153,9 @@ func (ps *podSource) addInternalHostnameAnnotationEndpoints(endpointMap map[endp
|
||||
domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
|
||||
for _, domain := range domainList {
|
||||
if len(targets) == 0 {
|
||||
addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
|
||||
addToEndpointMap(endpointMap, pod, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
|
||||
} else {
|
||||
addTargetsToEndpointMap(endpointMap, targets, domain)
|
||||
addTargetsToEndpointMap(endpointMap, pod, targets, domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -167,7 +167,7 @@ func (ps *podSource) addHostnameAnnotationEndpoints(endpointMap map[endpoint.End
|
||||
if len(targets) == 0 {
|
||||
ps.addPodNodeEndpointsToEndpointMap(endpointMap, pod, domainList)
|
||||
} else {
|
||||
addTargetsToEndpointMap(endpointMap, targets, domainList...)
|
||||
addTargetsToEndpointMap(endpointMap, pod, targets, domainList...)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -177,7 +177,7 @@ func (ps *podSource) addKopsDNSControllerEndpoints(endpointMap map[endpoint.Endp
|
||||
if domainAnnotation, ok := pod.Annotations[kopsDNSControllerInternalHostnameAnnotationKey]; ok {
|
||||
domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
|
||||
for _, domain := range domainList {
|
||||
addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
|
||||
addToEndpointMap(endpointMap, pod, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,9 +192,9 @@ func (ps *podSource) addPodSourceDomainEndpoints(endpointMap map[endpoint.Endpoi
|
||||
if ps.podSourceDomain != "" {
|
||||
domain := pod.Name + "." + ps.podSourceDomain
|
||||
if len(targets) == 0 {
|
||||
addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
|
||||
addToEndpointMap(endpointMap, pod, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
|
||||
}
|
||||
addTargetsToEndpointMap(endpointMap, targets, domain)
|
||||
addTargetsToEndpointMap(endpointMap, pod, targets, domain)
|
||||
}
|
||||
}
|
||||
|
||||
@ -209,7 +209,7 @@ func (ps *podSource) addPodNodeEndpointsToEndpointMap(endpointMap map[endpoint.E
|
||||
recordType := suitableType(address.Address)
|
||||
// IPv6 addresses are labeled as NodeInternalIP despite being usable externally as well.
|
||||
if address.Type == corev1.NodeExternalIP || (address.Type == corev1.NodeInternalIP && recordType == endpoint.RecordTypeAAAA) {
|
||||
addToEndpointMap(endpointMap, domain, recordType, address.Address)
|
||||
addToEndpointMap(endpointMap, pod, domain, recordType, address.Address)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -231,6 +231,7 @@ func (ps *podSource) hostsFromTemplate(pod *corev1.Pod) (map[endpoint.EndpointKe
|
||||
key := endpoint.EndpointKey{
|
||||
DNSName: target,
|
||||
RecordType: suitableType(address.IP),
|
||||
RecordTTL: annotations.TTLFromAnnotations(pod.Annotations, fmt.Sprintf("pod/%s", pod.Name)),
|
||||
}
|
||||
result[key] = append(result[key], address.IP)
|
||||
}
|
||||
@ -239,18 +240,19 @@ func (ps *podSource) hostsFromTemplate(pod *corev1.Pod) (map[endpoint.EndpointKe
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func addTargetsToEndpointMap(endpointMap map[endpoint.EndpointKey][]string, targets []string, domainList ...string) {
|
||||
func addTargetsToEndpointMap(endpointMap map[endpoint.EndpointKey][]string, pod *corev1.Pod, targets []string, domainList ...string) {
|
||||
for _, domain := range domainList {
|
||||
for _, target := range targets {
|
||||
addToEndpointMap(endpointMap, domain, suitableType(target), target)
|
||||
addToEndpointMap(endpointMap, pod, domain, suitableType(target), target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addToEndpointMap(endpointMap map[endpoint.EndpointKey][]string, domain string, recordType string, address string) {
|
||||
func addToEndpointMap(endpointMap map[endpoint.EndpointKey][]string, pod *corev1.Pod, domain string, recordType string, address string) {
|
||||
key := endpoint.EndpointKey{
|
||||
DNSName: domain,
|
||||
RecordType: recordType,
|
||||
RecordTTL: annotations.TTLFromAnnotations(pod.Annotations, fmt.Sprintf("pod/%s", pod.Name)),
|
||||
}
|
||||
if _, ok := endpointMap[key]; !ok {
|
||||
endpointMap[key] = []string{}
|
||||
|
@ -302,8 +302,8 @@ func TestPodSource(t *testing.T) {
|
||||
true,
|
||||
"",
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"54.10.11.1"}, RecordType: endpoint.RecordTypeA},
|
||||
{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"2001:DB8::1"}, RecordType: endpoint.RecordTypeAAAA},
|
||||
{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"54.10.11.1"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(5400)},
|
||||
{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"2001:DB8::1"}, RecordType: endpoint.RecordTypeAAAA, RecordTTL: endpoint.TTL(5400)},
|
||||
{DNSName: "b.foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA},
|
||||
},
|
||||
false,
|
||||
@ -339,6 +339,7 @@ func TestPodSource(t *testing.T) {
|
||||
Namespace: "kube-system",
|
||||
Annotations: map[string]string{
|
||||
hostnameAnnotationKey: "a.foo.example.org",
|
||||
ttlAnnotationKey: "1h30m",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
@ -374,8 +375,8 @@ func TestPodSource(t *testing.T) {
|
||||
true,
|
||||
"",
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"54.10.11.1"}, RecordType: endpoint.RecordTypeA},
|
||||
{DNSName: "internal.a.foo.example.org", Targets: endpoint.Targets{"10.0.1.1"}, RecordType: endpoint.RecordTypeA},
|
||||
{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"54.10.11.1"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(1)},
|
||||
{DNSName: "internal.a.foo.example.org", Targets: endpoint.Targets{"10.0.1.1"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(1)},
|
||||
},
|
||||
false,
|
||||
nodesFixturesIPv4(),
|
||||
@ -387,6 +388,7 @@ func TestPodSource(t *testing.T) {
|
||||
Annotations: map[string]string{
|
||||
internalHostnameAnnotationKey: "internal.a.foo.example.org",
|
||||
hostnameAnnotationKey: "a.foo.example.org",
|
||||
ttlAnnotationKey: "1s",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
@ -453,6 +455,7 @@ func TestPodSource(t *testing.T) {
|
||||
Annotations: map[string]string{
|
||||
internalHostnameAnnotationKey: "internal.a.foo.example.org",
|
||||
hostnameAnnotationKey: "a.foo.example.org",
|
||||
ttlAnnotationKey: "1s",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
@ -514,7 +517,7 @@ func TestPodSource(t *testing.T) {
|
||||
false,
|
||||
"example.org",
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "my-pod1.example.org", Targets: endpoint.Targets{"192.168.1.1"}, RecordType: endpoint.RecordTypeA},
|
||||
{DNSName: "my-pod1.example.org", Targets: endpoint.Targets{"192.168.1.1"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(60)},
|
||||
{DNSName: "my-pod2.example.org", Targets: endpoint.Targets{"192.168.1.2"}, RecordType: endpoint.RecordTypeA},
|
||||
},
|
||||
false,
|
||||
@ -522,9 +525,11 @@ func TestPodSource(t *testing.T) {
|
||||
[]*corev1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-pod1",
|
||||
Namespace: "kube-system",
|
||||
Annotations: map[string]string{},
|
||||
Name: "my-pod1",
|
||||
Namespace: "kube-system",
|
||||
Annotations: map[string]string{
|
||||
ttlAnnotationKey: "1m",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
HostNetwork: false,
|
||||
|
Loading…
Reference in New Issue
Block a user