ExternalDNS
ExternalDNS keeps records in your Frostmoln Managed DNS zones in sync with Kubernetes Ingress, Service, and Gateway API resources — from any Kubernetes cluster. When you expose a workload, its hostname's records are created, updated, and cleaned up automatically; you never edit DNS by hand.
Frostmoln ships a webhook provider that runs as a sidecar in the external-dns pod and translates ExternalDNS's plans into calls against the Frostmoln DNS API.
Pilot — DNS access is granted per tenant
ExternalDNS builds on Managed DNS, which is a pilot feature enabled per tenant. If the dns entitlement isn't on your tenant yet, see Managed DNS for how to request access.
Prerequisites
- A Frostmoln DNS zone for the domain you want records in — see Managed DNS to create one.
- The
dnsentitlement on your tenant (the same pilot access as Managed DNS). - A tenant API key (
fmk_...) held by a user in that tenant. Create one in the portal under Settings → API Keys, or with the CLI — seefm account api-key.
How it works
- Records — the webhook lists your tenant's zones and recordsets and returns them to ExternalDNS as endpoints (one recordset = one
(name, type)with all its values). - Apply — creates and updates go through the DNS API's idempotent, collection-level upsert; deletes resolve the recordset by its stable ID.
- Domain filter — by default the webhook fetches your tenant's zone names at startup and manages only those. Create a new zone → restart the pod so it is picked up (or set
DOMAIN_FILTERexplicitly). - Ownership — use ExternalDNS's default TXT registry (
--registry=txtwith--txt-owner-id=<cluster-id>) so a record is only ever changed by the cluster that created it. TXT recordsets round-trip through the API unmodified.
Install
On Frostmoln Managed Kubernetes, ExternalDNS with the Frostmoln webhook is a one-click cluster add-on. Select external-dns when you create the cluster (portal → Add-ons, or addons: ["external-dns"] on the API / Terraform resource) and the platform installs everything for you — the external-dns namespace, RBAC, the ExternalDNS controller, and the Frostmoln webhook sidecar, with --txt-owner-id already set to your cluster's id. The webhook image is provided by the platform and preloaded on the cluster nodes; you never build, pull, or reference it yourself.
The only step left to you is the API-key Secret (next section): until it exists the webhook container waits in CreateContainerConfigError. Create your DNS zone first (or restart the external-dns Deployment after creating it), and make sure your tenant holds the dns entitlement — without it the webhook logs 403 FEATURE_NOT_ENABLED.
Selected at cluster creation
The add-on is chosen when the cluster is created. To add ExternalDNS to an existing cluster, recreate it with the add-on selected — day-2 addition is planned. ExternalDNS with the Frostmoln webhook is currently available on Frostmoln Managed Kubernetes only.
Managed Kubernetes: enable the addon
On Frostmoln Managed Kubernetes the easiest path is the external-dns cluster addon: select it when creating the cluster (portal → Add-ons, or addons: ["external-dns"] on the API / Terraform resource) and the platform installs everything in this guide — the external-dns namespace, RBAC, and the Deployment, with --txt-owner-id already set to the cluster's id.
The only manual step is the API-key Secret (next section): until it exists the webhook container waits in CreateContainerConfigError. Create your DNS zone first (or restart the external-dns Deployment after creating it), and make sure your tenant holds the dns entitlement — without it the webhook logs 403 FEATURE_NOT_ENABLED.
Cluster create only
The addon can only be selected when the cluster is created. On an existing cluster, apply the manifest under Deploy instead.
Store the API key
Put the tenant API key in a Secret the webhook can read:
kubectl create namespace external-dns # skip if the add-on already created it
kubectl create secret generic frostmoln-dns \
--namespace external-dns \
--from-literal=api-key=fmk_...Deploy
On Managed Kubernetes with the addon enabled you can skip this section — only the Secret above is needed.
The manifest below runs external-dns with the Frostmoln webhook as a sidecar, plus the ServiceAccount and RBAC external-dns needs. Replace <cluster-id> with a value unique to this cluster — it stamps the TXT ownership records so several clusters can safely share a zone. The pod declares the full restricted Pod Security contract (Managed Kubernetes enforces it cluster-wide; the explicit runAsUser matters because the upstream external-dns image runs as root).
On Frostmoln Managed Kubernetes the webhook image is preloaded on your nodes, and it lives in a Frostmoln-internal registry your cluster cannot reach — so <webhook-image> is not yours to choose. It must be a ref that is already on the nodes; any other value leaves the sidecar in ImagePullBackOff. Read it off the cluster and paste it in verbatim:
kubectl get nodes -o json \
| jq -r '.items[].status.images[]?.names[]?' \
| grep 'external-dns-frostmoln-webhook:' | sort -uMore than one ref means your node pool spans two node-image generations: use the one present on every node, or roll the pool so they match. No output at all means the image is not listed on a busy node — try one node at a time (kubectl get node <name> -o json), or ask support.
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
namespace: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: ['']
resources: ['services', 'endpoints', 'pods', 'nodes']
verbs: ['get', 'watch', 'list']
- apiGroups: ['discovery.k8s.io']
resources: ['endpointslices']
verbs: ['get', 'watch', 'list']
- apiGroups: ['extensions', 'networking.k8s.io']
resources: ['ingresses']
verbs: ['get', 'watch', 'list']
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-dns
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: external-dns
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
namespace: external-dns
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: external-dns
image: registry.k8s.io/external-dns/external-dns:v0.21.0
args:
- --source=ingress
- --source=service
- --provider=webhook
- --registry=txt
- --txt-owner-id=<cluster-id>
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
- name: frostmoln-webhook
image: <webhook-image>
imagePullPolicy: IfNotPresent
env:
- name: FROSTMOLN_API_KEY
valueFrom:
secretKeyRef:
name: frostmoln-dns
key: api-key
ports:
- containerPort: 8080
name: http
livenessProbe:
httpGet:
path: /healthz
port: http
readinessProbe:
httpGet:
path: /healthz
port: http
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]Apply it:
kubectl apply -f external-dns.yamlThe webhook API listens on the pod loopback only — it is unauthenticated by contract and refuses any other bind address — and FROSTMOLN_API_URL must be https. Both are enforced at startup.
Configuration
The add-on sets sensible defaults. To tune the webhook sidecar, edit the external-dns Deployment and set these environment variables:
| Variable | Default | Description |
|---|---|---|
FROSTMOLN_API_KEY | — (required) | Tenant API key (fmk_...); the tenant must hold the dns entitlement |
FROSTMOLN_API_KEY_FILE | — | Read the key from a mounted file instead, to keep it out of the pod env |
FROSTMOLN_API_URL | https://api.frostmoln.cloud/api | Customer API base URL (must be https) |
FROSTMOLN_TENANT_ID | resolved from the key | Tenant override |
DOMAIN_FILTER | your zone names | Comma-separated domain filter override |
ZONE_CACHE_TTL | 1m | How long the zone catalog is cached |
DRY_RUN | false | Log the changes ExternalDNS would make without applying them |
Notes and caveats
- Public zones reject private and loopback targets. An A/AAAA record in a public zone must point at a routable address — a load balancer or a public IP, not a cluster-internal IP. For internal names, use a private (
vpcId) zone. - Delegate the zone before you expect public resolution. A zone answers on the internet only once your domain is delegated to its name servers — see Managed DNS → Delegate your domain.
- New zones need a webhook restart. The domain filter is fetched once at startup, so restart the external-dns pod after creating a zone you want ExternalDNS to manage.
- TXT ownership is supported natively — keep
--registry=txtwith a stable--txt-owner-idso multiple clusters can safely share a zone.