Skip to content

AKS: publish services with ExternalDNS

ExternalDNS watches your Kubernetes services and ingresses and publishes DNS records for them. Hayami DTM speaks the standard wire protocols ExternalDNS already supports: RFC 2136 dynamic updates for writes, TSIG for authentication, and AXFR zone transfers for reads. Point ExternalDNS’s rfc2136 provider at a DTM node and your AKS services get names in a DTM zone automatically, with no controller or agent to install on the DTM side.

Throughout this page the fictional zone is aks.internal.example., the DTM nodes are 10.0.0.4 and 10.0.0.5, and the AKS cluster’s egress IP is 203.0.113.7. For the same mechanics outside Kubernetes (plain hosts, DHCP servers, and keeping zones clean with scavenging), see dynamic DNS registration.

Four admin-API steps, in order: create the zone, enable dynamic updates, create a TSIG key, and enable AXFR pinned to the cluster’s egress IP.

The target zone must exist and be Active before ExternalDNS can write to it:

Terminal window
curl -X POST https://<dtm>/api/v1/zones \
-H "Authorization: Bearer <token>" \
-H "X-DTM-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{ "name": "aks.internal.example." }'

Note the zone id in the response; you need it for the TSIG key binding below.

Terminal window
curl -X PUT https://<dtm>/api/v1/config/dynamic-dns \
-H "Authorization: Bearer <token>" \
-H "X-DTM-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{ "enabled": true, "require_tsig": true }'

Generate a strong secret and bind the key to the zone:

Terminal window
SECRET=$(openssl rand -base64 32)
curl -X POST https://<dtm>/api/v1/tsig-keys \
-H "Authorization: Bearer <token>" \
-H "X-DTM-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{
"name": "externaldns",
"algorithm": "hmac-sha256",
"secret": "'"$SECRET"'",
"zone_ids": ["<zone-id>"]
}'
  • Algorithms: hmac-sha256, hmac-sha384, and hmac-sha512 are accepted; hmac-md5 and hmac-sha1 are refused.
  • Secret strength: DTM enforces a minimum 128-bit secret; 32 random bytes (as above) gives you a comfortable margin.
  • Names: key names match with or without a trailing dot, so externaldns and externaldns. refer to the same key. Creating a key with a duplicate name returns 409 Conflict.

4. Enable AXFR, pinned to the cluster egress

Section titled “4. Enable AXFR, pinned to the cluster egress”

ExternalDNS needs to read the zone (via AXFR) as well as write to it, so it can see what already exists and delete records for services you remove:

Terminal window
curl -X PUT https://<dtm>/api/v1/config/axfr \
-H "Authorization: Bearer <token>" \
-H "X-DTM-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"allowed_cidrs": ["203.0.113.7/32"],
"require_tsig": true
}'

Deploy ExternalDNS in the cluster with the rfc2136 provider aimed at a DTM node. With the ExternalDNS Helm chart, the values look like this:

provider:
name: rfc2136
extraArgs:
- --rfc2136-host=10.0.0.4
- --rfc2136-port=53
- --rfc2136-zone=aks.internal.example
- --rfc2136-tsig-keyname=externaldns
- --rfc2136-tsig-secret-alg=hmac-sha256
- --rfc2136-tsig-secret=<the TSIG secret from step 3>
- --rfc2136-tsig-axfr
- --domain-filter=aks.internal.example
- --policy=sync
- --registry=txt
- --txt-owner-id=aks-eastus-1

The settings that matter:

  • --rfc2136-host / --rfc2136-port: a DTM node IP, port 53.
  • --rfc2136-tsig-keyname: must equal the key name you created in DTM (externaldns), with the matching algorithm and secret.
  • --rfc2136-tsig-axfr: enables signed AXFR so ExternalDNS can read the zone’s current contents. Without it, ExternalDNS is upsert-only: it can create and update records but never sees what exists, so it never cleans up.
  • --policy=sync: allows deletes. The default, upsert-only, never deletes anything, even with AXFR working.
  • --registry=txt with a unique --txt-owner-id per cluster: ExternalDNS writes TXT ownership records alongside your service records so it only ever touches records it created. Give every cluster writing into DTM its own owner ID.
  • One DNS zone per ExternalDNS instance: run one ExternalDNS deployment per zone it manages.

Once running, annotate a service to publish it:

apiVersion: v1
kind: Service
metadata:
name: web
annotations:
external-dns.alpha.kubernetes.io/hostname: web-eastus.svc.aks.internal.example
spec:
type: LoadBalancer
# ...

ExternalDNS creates web-eastus.svc.aks.internal.example in the DTM zone with the service’s load-balancer IP, and removes it if you delete the service.

Pods resolve through AKS’s CoreDNS, which does not know about your DTM zone by default. Add a stub domain that forwards the zone to the DTM nodes using the coredns-custom ConfigMap AKS provides:

apiVersion: v1
kind: ConfigMap
metadata:
name: coredns-custom
namespace: kube-system
data:
aks-internal.server: |
aks.internal.example:53 {
errors
cache 5
forward . 10.0.0.4 10.0.0.5
}

Apply it and restart CoreDNS (kubectl -n kube-system rollout restart deployment coredns), then verify from a pod:

Terminal window
kubectl run -it --rm dnstest --image=busybox --restart=Never -- \
nslookup web-eastus.svc.aks.internal.example
  • The AXFR source is the cluster’s egress IP, not a node IP. Traffic leaving the cluster is NATed through your outbound path (NAT gateway or load balancer outbound rules), so allowed_cidrs must contain that egress address. If AXFR fails while updates succeed, check which address the transfer actually arrives from.
  • Deletes need both AXFR and --policy=sync. Missing either one silently degrades ExternalDNS to upsert-only, and records for deleted services linger in the zone.
  • TXT registry records appear next to your A records. For each published name you will see one or more TXT records recording ownership. This is expected; do not delete them, or ExternalDNS loses track of what it owns.

Need a hand? Email [email protected].