Kubernetes controller that translates EndpointSlice resources into headless Services annotated for external-dns consumption.
Kubernetes maintains an EndpointSlice (default/kubernetes) with control plane node IP addresses, but external-dns cannot use EndpointSlice as a source. kuberture bridges this gap:
EndpointSlice (kubernetes) → kuberture → headless Service(s) with annotations → external-dns → DNS
- Multiple output configurations with independent hostnames, annotation prefixes, and address sources
- Four address resolution strategies (EndpointSlice direct, node internal/external/public IPs)
- Configuration hot-reload via fsnotify (no restart required for output changes)
- Server-side apply for safe, conflict-free Service management
- Prometheus metrics and health/readiness probes
- Helm chart with RBAC, ServiceMonitor, and security hardening
kuberture reads its configuration from a YAML file. The path is resolved in order:
--configCLI flagKUBERTURE_CONFIGenvironment variable/etc/kuberture/config.yaml(default)
source:
namespace: default # EndpointSlice namespace (default: "default")
serviceName: kubernetes # label filter: kubernetes.io/service-name (default: "kubernetes")
outputs:
- name: internal
hostname:
- api-internal.k8s.example.com
annotationPrefix: "internal.company.io/"
serviceName: kuberture-internal
recordTTL: 60
addressSource: endpointslice
addressType: IPv4
- name: external
hostname:
- api.k8s.example.com
annotationPrefix: "external-dns.alpha.kubernetes.io/"
serviceName: kuberture-external
recordTTL: 300
addressSource: node-external
addressType: IPv4
metricsBindAddress: ":8080" # default: ":8080"
healthProbeBindAddress: ":8081" # default: ":8081"| Source | Description |
|---|---|
endpointslice |
IP addresses directly from EndpointSlice (default) |
node-internal |
InternalIP from Node.status.addresses |
node-external |
ExternalIP from Node.status.addresses |
node-public |
First non-RFC1918/RFC4193 IP from Node.status.addresses |
| Field | Default |
|---|---|
annotationPrefix |
external-dns.alpha.kubernetes.io/ |
recordTTL |
60 (max: 86400) |
addressSource |
endpointslice |
addressType |
IPv4 |
helm install kuberture oci://ghcr.io/lexfrei/kuberture/charts/kuberture \
--namespace kuberture --create-namespace \
--values my-values.yamlconfig:
outputs:
- name: api
hostname:
- api.k8s.example.com
serviceName: kuberture-api| Key | Default | Description |
|---|---|---|
replicaCount |
1 |
Number of replicas |
image.repository |
ghcr.io/lexfrei/kuberture |
Container image repository |
image.tag |
"" (appVersion) |
Container image tag |
image.pullPolicy |
IfNotPresent |
Image pull policy |
serviceAccount.create |
true |
Create ServiceAccount |
serviceAccount.name |
"" |
Override ServiceAccount name |
config |
see above | kuberture configuration |
resources.requests.cpu |
10m |
CPU request |
resources.requests.memory |
32Mi |
Memory request |
resources.limits.memory |
64Mi |
Memory limit |
serviceMonitor.enabled |
false |
Create Prometheus ServiceMonitor |
serviceMonitor.interval |
30s |
Scrape interval |
| Metric | Type | Description |
|---|---|---|
kuberture_reconcile_total |
Counter | Total reconciliations by status (success/error) |
kuberture_endpoints_resolved |
Gauge | Current resolved addresses per output |
kuberture_last_reconcile_timestamp |
Gauge | Unix timestamp of last successful reconciliation |
- Go 1.26+
- golangci-lint v2.9+
- helm-unittest plugin (for chart tests)
make build Build binary to bin/kuberture
make test Run tests with race detector and coverage
make lint Run golangci-lint
make lint-fix Run golangci-lint with --fix
make fmt Run gofumpt and goimports
make vet Run go vet
make tidy Run go mod tidy
make image Build container image
make helm-lint Lint Helm chart
make helm-package Package Helm chart
make e2e Run E2E tests with kind
make all Run fmt, vet, lint, test, build
cmd/kuberture/ Entry point
internal/
config/ YAML parsing, validation, hot-reload watcher
controller/ Reconciler, metrics
resolver/ Address resolution from EndpointSlices and Nodes
e2e/ E2E tests with kind
chart/ Helm chart
.github/workflows/ CI/CD (lint, test, PR builds, releases)
The controller uses split RBAC for least-privilege access:
Cluster-level (ClusterRole — read-only):
discovery.k8s.io/endpointslices: get, list, watchcore/nodes: get, list, watch
Namespace-level (Role — in the controller's own namespace):
core/services: get, list, watch, create, update, patch, deletecoordination.k8s.io/leases: get, list, watch, create, update, patchcore/events: create, patch
