Self-hosted service
kubernetes-sigs/external-dns avatar
kubernetes-sigs/external-dns

ExternalDNS: Kubernetes DNS Records Without a DNS Server

Configure external DNS servers dynamically from Kubernetes resources

9,099 stars2,929 forksGoApache-2.0

At a glance

What is it?
ExternalDNS is a Kubernetes SIG Network controller that reads Services, Ingresses and other sources and writes matching records into Route 53, Cloud DNS, Azure DNS or a webhook provider. It is not a DNS server, and the ownership model that keeps it safe is also its most misunderstood part.
Who is it for?
Adopt ExternalDNS when your records should follow Kubernetes objects and you can name a single owner ID per cluster and zone. Do not adopt it if you need a resolver, or if you cannot accept that record deletion requires --policy=sync while --policy=upsert-only never deletes.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What ExternalDNS Actually Replaces

The project describes itself as synchronizing exposed Kubernetes Services and Ingresses with DNS providers. The README states it was inspired by Kubernetes DNS, the cluster-internal server, and that it makes Kubernetes resources discoverable via public DNS servers. Then it draws the line that matters: unlike KubeDNS, ExternalDNS is not a DNS server itself. It configures other DNS providers, naming AWS Route 53 and Google Cloud DNS as examples.

That distinction decides who the tool is for. If you want something that answers queries, this is the wrong repository. If you want the record for nginx.example.org to appear in Route 53 because a Service exists and carries an annotation, this is the repository. The audience is platform teams running clusters where the set of hostnames changes with deployments, and where editing a zone by hand after every rollout has stopped being reasonable.

The scope is broader than Ingress. The README says ExternalDNS keeps selected zones synchronized with Ingresses, Services of type=LoadBalancer, and nodes. Zone selection happens through --domain-filter, so the controller can be pointed at a slice of a zone rather than all of it. Records can also come from CRDs, which the related search terms around External-dns CRD point at.

The Sync Loop, the Registry, and Why --txt-owner-id Matters

ExternalDNS runs as a controller in the cluster. It retrieves a list of resources from the Kubernetes API to determine a desired list of DNS records, then compares that with what the provider currently holds. The README says ExternalDNS is by default aware of the records it is managing, which is what lets it run against a hosted zone that already contains records it did not create.

Ownership is tracked through TXT records. The README is explicit: TXT records will have the my-cluster-id value embedded, and those are used to ensure ExternalDNS is aware of the records it manages. It strongly encourages setting --txt-owner-id to a unique value that does not change for the lifetime of the cluster. That is a real operational constraint, not a suggestion. Change the owner ID and the controller no longer recognises its own prior records.

The registry has its own trap. The README notes that when using a txt registry and attempting to use a CNAME, --txt-prefix must be set to avoid conflicts, and that changing --txt-prefix results in lost ownership over previously created records. So the prefix is effectively part of the cluster's identity too. Pick both values once, before the first sync, and treat them as immutable.

Deletion behaviour depends on policy. Record deletion requires --policy=sync. With --policy=upsert-only, records are never deleted. A team that expects a removed Service to clean up its hostname and leaves the default policy in place will find stale records and no error.

Running ExternalDNS Locally Before It Touches a Zone

The README documents a local path, which is the cheapest way to see the plan before a controller starts writing. First create a workload and expose it, then annotate it with the hostname you want.

bash
kubectl run nginx --image=nginx --port=80
kubectl expose pod nginx --port=80 --target-port=80 --type=LoadBalancer
kubectl annotate service nginx "external-dns.kubernetes.io/hostname=nginx.example.org."

The annotation is the desired record. Change example.org to a domain you control. The README also shows a TTL annotation, external-dns.kubernetes.io/ttl, and an internal-hostname annotation that creates records with ClusterIP as the target. For the internal case the README notes that if the service is not of type LoadBalancer you need the --publish-internal-services flag.

With the Service annotated, run a single sync loop rather than a control loop. The README gives this example, which assumes the default namespace:

bash
external-dns --txt-owner-id my-cluster-id --provider google --google-project example-project --source service --once --dry-run

According to the README, this should output the DNS records it will modify to match the managed zone with the records you desire. Read that output before removing --dry-run. When you are satisfied, drop --once and --dry-run and run it as a control loop. Then verify the record resolves:

bash
dig +short nginx.example.org.

All flags can be replaced with environment variables. The README gives the example that --dry-run could be replaced with EXTERNAL_DNS_DRY_RUN=1, which is how the dry run is usually expressed in a manifest.

Installing It in a Cluster and the Helm Chart Question

The README points to provider-specific setup through the in-tree providers table, where each row links its tutorial, and to webhook providers for anything not built in. It also points at the full documentation site and the charts directory in the repository. The repository ships a charts/ entry, and the recent releases include external-dns-helm-chart-1.22.0, so the chart is versioned separately from the binary.

That separation is worth knowing before you pin anything. The binary reached v0.23.0 and the chart reached 1.22.0 on different dates, so a chart version does not map to a binary version by number. Check both when you upgrade.

The minimum viable configuration is small: a provider, credentials for that provider, a source, a domain filter, an owner ID, and a policy. Everything else is tuning. Because all flags can be replaced with environment variables, the same settings can live as args or as env entries, and the README's EXTERNAL_DNS_DRY_RUN=1 example shows the mapping convention.

One setup detail from the README matters for bare metal and NAT setups. If an externalIPs list is defined for a LoadBalancer service, that list will be used instead of an assigned load balancer IP to create a DNS record. The README says this is useful when you run bare metal Kubernetes clusters behind NAT or in a similar setup where the load balancer IP differs from the public one. Without that field the record points at an address nobody can reach.

Where ExternalDNS Is the Wrong Tool

The most common mistake is treating it as DNS infrastructure. It is a writer, not a resolver. Nothing about it serves queries to clients, and the README's comparison with KubeDNS exists precisely to head off that assumption.

A second failure mode is shared zones. The ownership model assumes the controller can tell its records apart from everyone else's, which is why the README pushes a unique --txt-owner-id and warns that a changed --txt-prefix loses ownership. In a zone edited by other automation, by a registrar's UI, or by a second ExternalDNS instance with a different owner ID, the safety property weakens. The README does not document a rollback procedure for a mistaken sync, so the practical protection is the dry run before the first real write.

A third case is deletion expectations. Teams that want hostnames to disappear when a Service is removed must set --policy=sync, and the README states plainly that with --policy=upsert-only records are never deleted. If your process depends on cleanup but your policy is upsert-only, the gap will only surface during an incident.

Finally, provider coverage is not uniform. In-tree providers each have their own tutorial, and anything outside that list goes through the webhook provider interface. If your DNS backend is not in the table, you are writing or adopting a webhook provider, not configuring a flag.

How ExternalDNS Differs from a GitOps DNS Workflow

The obvious alternative is declarative DNS through a GitOps controller: keep zone files or Terraform in a repository, and let a reconciler apply them. The difference in approach is the source of truth. In that model the desired record set lives in git and Kubernetes is downstream. In ExternalDNS the desired record set is derived live from the Kubernetes API, from Services, Ingresses, nodes and CRDs.

The trade-off is legibility against latency. A git-managed zone shows you the full record set in one review, including records that have nothing to do with Kubernetes. ExternalDNS shows you only what its sources expose, filtered by --domain-filter, and the full picture exists only in the provider. For a zone that is mostly Kubernetes-managed, that is a good fit. For a zone shared with mail, verification and vendor records, the git model gives reviewers something ExternalDNS cannot.

There is a middle path in the repository itself. ExternalDNS can read from CRDs as a source, so records can be declared as Kubernetes objects rather than inferred from Services. That keeps the live-reconcile behaviour while making the record set explicit and reviewable. The README does not document CRD sources in the excerpt, so consult the documentation before assuming the exact schema.

Maintenance, Licence and Upgrade Cost

The repository is not archived, and the last push was on 2026-09-21. Releases are frequent: v0.23.0 on 2026-09-18, the Helm chart at 1.22.0 on 2026-09-11, and v0.22.0 on 2026-08-20. That cadence means upgrades are a recurring chore rather than a rare event, and the chart and binary move on separate schedules.

The project is licensed under Apache-2.0, and the source files carry Apache headers, with a licensecheck target in the Makefile that verifies the header on Go files. Apache-2.0 is permissive and includes a patent grant. That is a description of the licence text, not legal advice; if your organisation has policies about which licences are acceptable for infrastructure components, run it past whoever owns that policy.

The dependency surface is the real upgrade cost. go.mod pulls in provider SDKs for AWS, Azure, Google, Cloudflare, Alibaba Cloud, Civo, Exoscale, Linode, DNSimple, PowerDNS and others, plus the Kubernetes libraries. A pinned dependency in that file shows the kind of care required: cloudflare-go is held at v7.7.0 because, per the comment, v7.8.0 dropped the dns.RecordResponseUnion discriminator, losing MX priority and SRV data. Upgrades can carry provider-specific regressions that have nothing to do with the core sync loop, so read release notes before bumping.

Building from source requires Go 1.27.0 per go.mod. The Makefile's default goal is build, and it also exposes cover, go-lint and licensecheck targets for contributors.

Editorial conclusion

Adopt ExternalDNS when your records should follow Kubernetes objects and you can name a single owner ID per cluster and zone. Do not adopt it if you need a resolver, or if you cannot accept that record deletion requires --policy=sync while --policy=upsert-only never deletes. Before rolling it out, confirm the provider tutorial for your DNS backend, then run the binary with --once and --dry-run against the real zone and read the planned record list.

Frequently asked questions

What is ExternalDNS in Kubernetes?

It is a controller that retrieves a list of resources from the Kubernetes API to determine a desired list of DNS records, then configures a DNS provider to match. The README states it is not a DNS server itself, unlike KubeDNS.

How do I install ExternalDNS?

The README points to provider-specific tutorials through the in-tree providers table and to webhook providers for anything not built in, with the full documentation on the project site. The repository ships a charts/ directory, and the chart is released separately, for example external-dns-helm-chart-1.22.0.

How do I use ExternalDNS?

Annotate a Service with external-dns.kubernetes.io/hostname, then run the binary with a provider, a source and --txt-owner-id. The README recommends starting with --once and --dry-run to see the records it will modify before running it as a control loop.

What is the difference between internal DNS and external DNS?

The README frames the project against Kubernetes DNS, the cluster-internal server, and says ExternalDNS makes Kubernetes resources discoverable via public DNS servers. ExternalDNS also has an internal-hostname annotation that creates records with ClusterIP as the target, which requires --publish-internal-services when the Service is not of type LoadBalancer.

What is ExternalDNS used for?

The README says it lets you control DNS records dynamically via Kubernetes resources in a DNS provider-agnostic way, keeping selected zones synchronized with Ingresses, Services of type=LoadBalancer and nodes. It configures other DNS providers rather than serving queries itself.

What is an external DNS server?

In this project's terms, the external DNS server is the provider ExternalDNS writes to, such as AWS Route 53 or Google Cloud DNS. The README states ExternalDNS is not a DNS server itself and merely configures other DNS providers accordingly.

Official sources

  1. kubernetes-sigs/external-dns on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/kubernetes-sigs-external-dns.svg)](https://hysenlabs.com/projects/kubernetes-sigs-external-dns)