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.
|
TTL must be a positive value.
|
||||||
|
|
||||||
## Providers
|
## TTL annotation support
|
||||||
|
|
||||||
- [x] AWS (Route53)
|
> 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.
|
||||||
- [x] Azure
|
|
||||||
- [x] Cloudflare
|
|
||||||
- [x] DigitalOcean
|
|
||||||
- [x] DNSimple
|
|
||||||
- [x] Google
|
|
||||||
- [ ] InMemory
|
|
||||||
- [x] Linode
|
|
||||||
- [x] TransIP
|
|
||||||
- [x] RFC2136
|
|
||||||
|
|
||||||
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
|
## Notes
|
||||||
|
|
||||||
@ -89,3 +139,82 @@ The Linode Provider default TTL is used when the TTL is 0. The default is 24 hou
|
|||||||
### TransIP Provider
|
### TransIP Provider
|
||||||
|
|
||||||
The TransIP Provider minimal TTL is used when the TTL is 0. The minimal TTL is 60s.
|
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
|
DNSName string
|
||||||
RecordType string
|
RecordType string
|
||||||
SetIdentifier string
|
SetIdentifier string
|
||||||
|
RecordTTL TTL
|
||||||
}
|
}
|
||||||
|
|
||||||
// Endpoint is a high-level way of a connection between a service and an IP
|
// 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
|
var endpoints []*endpoint.Endpoint
|
||||||
for key, targets := range endpointMap {
|
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
|
return endpoints, nil
|
||||||
}
|
}
|
||||||
@ -153,9 +153,9 @@ func (ps *podSource) addInternalHostnameAnnotationEndpoints(endpointMap map[endp
|
|||||||
domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
|
domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
|
||||||
for _, domain := range domainList {
|
for _, domain := range domainList {
|
||||||
if len(targets) == 0 {
|
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 {
|
} 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 {
|
if len(targets) == 0 {
|
||||||
ps.addPodNodeEndpointsToEndpointMap(endpointMap, pod, domainList)
|
ps.addPodNodeEndpointsToEndpointMap(endpointMap, pod, domainList)
|
||||||
} else {
|
} 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 {
|
if domainAnnotation, ok := pod.Annotations[kopsDNSControllerInternalHostnameAnnotationKey]; ok {
|
||||||
domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
|
domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
|
||||||
for _, domain := range domainList {
|
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 != "" {
|
if ps.podSourceDomain != "" {
|
||||||
domain := pod.Name + "." + ps.podSourceDomain
|
domain := pod.Name + "." + ps.podSourceDomain
|
||||||
if len(targets) == 0 {
|
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)
|
recordType := suitableType(address.Address)
|
||||||
// IPv6 addresses are labeled as NodeInternalIP despite being usable externally as well.
|
// IPv6 addresses are labeled as NodeInternalIP despite being usable externally as well.
|
||||||
if address.Type == corev1.NodeExternalIP || (address.Type == corev1.NodeInternalIP && recordType == endpoint.RecordTypeAAAA) {
|
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{
|
key := endpoint.EndpointKey{
|
||||||
DNSName: target,
|
DNSName: target,
|
||||||
RecordType: suitableType(address.IP),
|
RecordType: suitableType(address.IP),
|
||||||
|
RecordTTL: annotations.TTLFromAnnotations(pod.Annotations, fmt.Sprintf("pod/%s", pod.Name)),
|
||||||
}
|
}
|
||||||
result[key] = append(result[key], address.IP)
|
result[key] = append(result[key], address.IP)
|
||||||
}
|
}
|
||||||
@ -239,18 +240,19 @@ func (ps *podSource) hostsFromTemplate(pod *corev1.Pod) (map[endpoint.EndpointKe
|
|||||||
return result, nil
|
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 _, domain := range domainList {
|
||||||
for _, target := range targets {
|
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{
|
key := endpoint.EndpointKey{
|
||||||
DNSName: domain,
|
DNSName: domain,
|
||||||
RecordType: recordType,
|
RecordType: recordType,
|
||||||
|
RecordTTL: annotations.TTLFromAnnotations(pod.Annotations, fmt.Sprintf("pod/%s", pod.Name)),
|
||||||
}
|
}
|
||||||
if _, ok := endpointMap[key]; !ok {
|
if _, ok := endpointMap[key]; !ok {
|
||||||
endpointMap[key] = []string{}
|
endpointMap[key] = []string{}
|
||||||
|
@ -302,8 +302,8 @@ func TestPodSource(t *testing.T) {
|
|||||||
true,
|
true,
|
||||||
"",
|
"",
|
||||||
[]*endpoint.Endpoint{
|
[]*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{"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},
|
{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},
|
{DNSName: "b.foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA},
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
@ -339,6 +339,7 @@ func TestPodSource(t *testing.T) {
|
|||||||
Namespace: "kube-system",
|
Namespace: "kube-system",
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
hostnameAnnotationKey: "a.foo.example.org",
|
hostnameAnnotationKey: "a.foo.example.org",
|
||||||
|
ttlAnnotationKey: "1h30m",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Spec: corev1.PodSpec{
|
Spec: corev1.PodSpec{
|
||||||
@ -374,8 +375,8 @@ func TestPodSource(t *testing.T) {
|
|||||||
true,
|
true,
|
||||||
"",
|
"",
|
||||||
[]*endpoint.Endpoint{
|
[]*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{"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},
|
{DNSName: "internal.a.foo.example.org", Targets: endpoint.Targets{"10.0.1.1"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(1)},
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
nodesFixturesIPv4(),
|
nodesFixturesIPv4(),
|
||||||
@ -387,6 +388,7 @@ func TestPodSource(t *testing.T) {
|
|||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
internalHostnameAnnotationKey: "internal.a.foo.example.org",
|
internalHostnameAnnotationKey: "internal.a.foo.example.org",
|
||||||
hostnameAnnotationKey: "a.foo.example.org",
|
hostnameAnnotationKey: "a.foo.example.org",
|
||||||
|
ttlAnnotationKey: "1s",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Spec: corev1.PodSpec{
|
Spec: corev1.PodSpec{
|
||||||
@ -453,6 +455,7 @@ func TestPodSource(t *testing.T) {
|
|||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
internalHostnameAnnotationKey: "internal.a.foo.example.org",
|
internalHostnameAnnotationKey: "internal.a.foo.example.org",
|
||||||
hostnameAnnotationKey: "a.foo.example.org",
|
hostnameAnnotationKey: "a.foo.example.org",
|
||||||
|
ttlAnnotationKey: "1s",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Spec: corev1.PodSpec{
|
Spec: corev1.PodSpec{
|
||||||
@ -514,7 +517,7 @@ func TestPodSource(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
"example.org",
|
"example.org",
|
||||||
[]*endpoint.Endpoint{
|
[]*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},
|
{DNSName: "my-pod2.example.org", Targets: endpoint.Targets{"192.168.1.2"}, RecordType: endpoint.RecordTypeA},
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
@ -522,9 +525,11 @@ func TestPodSource(t *testing.T) {
|
|||||||
[]*corev1.Pod{
|
[]*corev1.Pod{
|
||||||
{
|
{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "my-pod1",
|
Name: "my-pod1",
|
||||||
Namespace: "kube-system",
|
Namespace: "kube-system",
|
||||||
Annotations: map[string]string{},
|
Annotations: map[string]string{
|
||||||
|
ttlAnnotationKey: "1m",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Spec: corev1.PodSpec{
|
Spec: corev1.PodSpec{
|
||||||
HostNetwork: false,
|
HostNetwork: false,
|
||||||
|
Loading…
Reference in New Issue
Block a user