Kubernetes no siempre contiene todos tus workloads.
Una plataforma puede ejecutar la mayoría de sus aplicaciones dentro de Kubernetes y seguir dependiendo de:
- aplicaciones legacy en máquinas virtuales
- servidores de aplicaciones que todavía no pueden migrarse
- bases de datos alojadas fuera del cluster
- appliances internos
- workloads sobre bare metal
- servicios que viven en otro entorno de infraestructura
El problema de red normalmente no es difícil. Si el cluster puede enrutar hacia una IP externa, sus workloads pueden alcanzarla.
El problema operativo aparece después:
¿Quién mantiene Kubernetes sincronizado
cuando cambian esos endpoints externos?
Ese es el problema que quise resolver con External Service Discovery Operator.
Es un operador ligero de Kubernetes que descubre workloads externos mediante DNS o direcciones IPv4 estáticas y los proyecta como recursos nativos:
DiscoveredService
↓
Service sin selector
↓
EndpointSlice
↓
consumidores nativos de Kubernetes
No introduce un proxy, sidecar, agente ni service mesh en el tráfico. Solo mantiene sincronizado el estado de Kubernetes con los endpoints externos descubiertos.
La idea central es sencilla: los workloads externos pueden seguir viviendo fuera del cluster y, al mismo tiempo, presentarse a los consumidores como endpoints nativos de Kubernetes.
La pregunta obvia: ¿por qué no usar ExternalName?
Kubernetes ya dispone de Services ExternalName:
apiVersion: v1
kind: Service
metadata:
name: legacy-payments
spec:
type: ExternalName
externalName: payments.internal.example.com
Para muchos casos es la solución correcta. Kubernetes publica un alias DNS y la resolución permanece en el camino del consumidor.
Con ExternalName, las direcciones resueltas no se materializan como backends de un EndpointSlice.
La diferencia es arquitectónica. Un ExternalName puede resolver varios registros A, pero Kubernetes no expone esos registros como backends individuales de EndpointSlice; la resolución y el comportamiento de las conexiones quedan fuera del modelo de endpoints del Service.
| Necesidad | ExternalName | DiscoveredService |
|---|---|---|
| Alias DNS de Kubernetes hacia un hostname externo | Sí | No es su objetivo principal |
| IP externas concretas representadas en EndpointSlice | No | Sí |
| Varios backends visibles como endpoints de Kubernetes | No | Sí |
| Destinos mediante IP externas estáticas | No | Sí |
| Sincronización periódica DNS → EndpointSlice | No | Sí |
| Requiere un controlador adicional | No | Sí |
ExternalName suele ser la opción más sencilla y adecuada. DiscoveredService resulta útil cuando las máquinas externas deben participar en el modelo de endpoints de un Service de Kubernetes.
¿Por qué querría EndpointSlices concretos?
Supongamos que la misma aplicación Tomcat se ejecuta en tres VMs externas y un hostname estable resuelve las tres direcciones:
tomcat.internal.example.com
├── 10.140.0.11
├── 10.140.0.12
└── 10.140.0.13
El operador materializa esas direcciones como backends individuales detrás de un Service. Un consumidor compatible con Kubernetes puede utilizar ese conjunto de endpoints al distribuir conexiones entre las réplicas.
El operador no actúa como proxy ni balancea el tráfico. Solo descubre, normaliza y publica los endpoints.
El caso de uso original
El proyecto nació de un escenario híbrido: aplicaciones nuevas dentro de Kubernetes consumiendo un sistema legacy ejecutado en VMs.
La infraestructura externa podía cambiar direcciones, pero ofrecía nombres DNS estables. Mantener listas de IP manuales en manifiestos era frágil:
la infraestructura cambia
↓
el manifiesto queda obsoleto
↓
el Service apunta a endpoints antiguos
↓
la aplicación falla
Lo que necesitaba era una declaración estable:
apiVersion: discovery.k8sready.com/v1alpha1
kind: DiscoveredService
metadata:
name: legacy-payments
spec:
discovery:
dns:
names:
- payments.internal.example.com
ports:
- name: http
port: 80
targetPort: 8080
protocol: TCP
El operador resuelve los nombres y mantiene un Service sin selector y un EndpointSlice con las direcciones IPv4 descubiertas.
Descubrimiento DNS
El proveedor DNS permite declarar uno o varios nombres:
spec:
discovery:
dns:
names:
- app-01.internal.example.com
- app-02.internal.example.com
ports:
- name: http
port: 80
targetPort: 8080
El flujo es:
nombres DNS
↓
resolución A/AAAA
↓
filtrado a IPv4 utilizable
↓
deduplicación y orden estable
↓
EndpointSlice
Actualmente el operador materializa solo IPv4 y crea EndpointSlice con addressType: IPv4. Esta limitación es deliberadamente explícita.
La resolución DNS se refresca cada minuto por defecto. El intervalo es configurable para todo el controlador mediante --discovery-refresh-interval.
Un fallo no debería eliminar endpoints que funcionaban
Los fallos DNS transitorios son normales. Si una resolución falla, borrar inmediatamente endpoints válidos puede convertir un problema temporal en una caída.
Por eso el operador conserva el último estado conocido como válido:
resolución correcta
↓
Service + EndpointSlice actualizados
↓
fallo DNS temporal
↓
se conservan los endpoints existentes
↓
Ready=False / DiscoveryFailed
El estado informa del fallo, pero el Service, el EndpointSlice y el número observado de endpoints permanecen sin cambios hasta que vuelva a existir un resultado válido.
Descubrimiento estático
También pueden declararse direcciones IPv4 fijas:
apiVersion: discovery.k8sready.com/v1alpha1
kind: DiscoveredService
metadata:
name: external-database
spec:
discovery:
static:
addresses:
- 10.140.0.21
- 10.140.0.22
ports:
- name: postgres
port: 5432
protocol: TCP
El proveedor valida, normaliza, deduplica y ordena las direcciones. Es útil cuando las IP son estables o cuando otro sistema genera los manifiestos mediante GitOps.
¿Por qué EndpointSlice y no Endpoints?
EndpointSlice es la API moderna de Kubernetes para representar endpoints de red. Es la base utilizada por el ecosistema actual y evita construir una nueva abstracción paralela.
El operador crea recursos deterministas con el mismo nombre que el DiscoveredService. Ambos hijos incluyen referencias de propietario y el controlador restaura cualquier drift en los campos que gestiona. Si ya existe un recurso con ese nombre que no le pertenece, informa de un conflicto y no lo adopta.
Independiente del proveedor de infraestructura
El diseño separa el descubrimiento de la reconciliación:
proveedor de descubrimiento
↓
lista normalizada de endpoints
↓
reconciliación Kubernetes
La versión actual incluye proveedores DNS y estático. En el futuro podrían añadirse inventarios de cloud, service directories o APIs internas sin cambiar el modelo consumido por Kubernetes.
Lo que el operador no intenta hacer
El alcance es intencionadamente pequeño. No es:
- una service mesh
- un proxy de tráfico
- un controlador de DNS
- un sistema de health checks externos
- un gestor de infraestructura cloud
- un sustituto de Service Discovery específico de cada aplicación
Su responsabilidad es mantener Service y EndpointSlice sincronizados con una fuente externa de endpoints.
Cuándo ExternalName sigue siendo mejor
ExternalName suele ser preferible cuando:
- solo necesitas un alias DNS
- el consumidor resuelve DNS correctamente
- no necesitas endpoints visibles en la API
- quieres la solución más pequeña posible
DiscoveredService encaja mejor cuando:
- un nombre representa varios backends
- las direcciones cambian tras un hostname estable
- los consumidores esperan
EndpointSlice - quieres observar y reconciliar las direcciones desde Kubernetes
No se trata de reemplazar ExternalName. Se trata de cubrir el espacio donde un alias DNS no es suficiente.
Un puente útil para migraciones
Durante una migración de VMs a Kubernetes, las aplicaciones internas pueden consumir el mismo nombre de Service mientras sus backends continúan fuera del cluster. La ubicación del workload y la forma de consumo quedan desacopladas.
estado deseado estable
↓
descubrimiento externo
↓
direcciones runtime cambiantes
↓
endpoints nativos de Kubernetes
Arquitectura
El proveedor de descubrimiento está separado de la reconciliación. Tanto DNS como el proveedor estático producen un conjunto normalizado de endpoints; el controlador lo proyecta después en los mismos recursos nativos de Kubernetes.
Instalación
La versión 0.1.1 está disponible como chart Helm OCI:
helm upgrade --install external-service-discovery-operator \
oci://ghcr.io/pierinho13/charts/external-service-discovery-operator \
--version 0.1.1 \
--namespace external-service-discovery-operator-system \
--create-namespace
La imagen del controlador también se publica en GitHub Container Registry.
Reflexiones finales
Resolver DNS, crear un Service o crear un EndpointSlice son tareas sencillas por separado. Lo interesante es mantenerlas conectadas de forma declarativa, observable y segura ante fallos.
A veces ExternalName es exactamente lo necesario. Otras veces, configurar el hostname directamente en la aplicación es aún más simple.
Pero cuando los consumidores necesitan que workloads externos se comporten como endpoints nativos de un Service, existe un pequeño hueco entre DNS y la gestión de EndpointSlice. Este proyecto está diseñado para cubrirlo.
El proyecto es open source:
https://github.com/pierinho13/external-service-discovery-operator
La versión actual soporta descubrimiento DNS y estático, EndpointSlices IPv4, refresco DNS periódico, reconciliación determinista y conservación del último estado válido durante fallos de descubrimiento.
Si trabajas con entornos híbridos Kubernetes/VM, migraciones de sistemas legacy o Services sin selector mantenidos manualmente, me interesa especialmente conocer tu experiencia.
Comentarios