Service Mesh sin Istio: construcción de red entre servicios en Go con Envoy y Kubernetes
Introducción: cuando Istio se convierte en una carga
Istio es una herramienta poderosa, pero su complejidad suele ser inversamente proporcional al tamaño del equipo. Un control plane formado por múltiples componentes (istiod, ingress gateway, egress gateway), el overhead de recursos en el clúster, un debugging nada trivial y una curva de aprendizaje pronunciada lo convierten en algo excesivo para muchos sistemas en producción. Según la CNCF Survey 2024, una parte significativa de los equipos que probaron Istio lo abandonan durante el primer año precisamente por su complejidad operacional.
Si tu equipo trabaja con microservicios Go en Kubernetes y necesita mTLS, trazado distribuido, health checks y balanceo de tráfico, todo eso se puede obtener sin Istio configurando Envoy manualmente como proxy sidecar. De eso trata exactamente este artículo.
Qué es un service mesh y por qué es necesario en una arquitectura Go
Un service mesh es una capa de infraestructura que gestiona la comunicación entre servicios: enrutamiento de tráfico, balanceo de carga, autenticación, cifrado y observabilidad. En una arquitectura de microservicios con Go, estas tareas se resolvían tradicionalmente a nivel de aplicación mediante bibliotecas (por ejemplo, go-kit, grpc-go con interceptors). Sin embargo, este enfoque tiene desventajas:
- La lógica de red queda dispersa por el código de cada servicio
- Actualizar las políticas requiere recompilar y redesplegar
- Implementaciones heterogéneas entre equipos
- Dificultad para aplicar mTLS de forma uniforme
El service mesh extrae esa lógica a un proceso proxy separado (sidecar) que se ejecuta junto a cada pod. Tu servicio Go no sabe nada sobre cifrado o circuit breaking: todo eso lo gestiona Envoy de forma transparente.
Envoy como data plane: capacidades clave
Envoy Proxy es un proxy L4/L7 de alto rendimiento escrito en C++. Es precisamente el que se utiliza como data plane en Istio, Linkerd v2 (parcialmente) y otras soluciones mesh. Las capacidades clave de Envoy relevantes para nuestro caso:
- Configuración dinámica mediante xDS API — la configuración se actualiza sin necesidad de reiniciar
- Balanceo de carga HTTP/gRPC — round-robin, least-request, ring-hash
- Terminación y originación de mTLS — cifrado entre servicios
- Trazado distribuido — integración con Zipkin, Jaeger, OpenTelemetry
- Métricas Prometheus — exposición de métricas incorporada
- Circuit breaking y reintentos — tolerancia a fallos a nivel de proxy
- Health checking — comprobaciones activas de servicios upstream
En el modo «sin Istio» gestionamos Envoy mediante configuraciones YAML estáticas o nuestro propio control plane xDS. Para la mayoría de los equipos, la configuración estática es más que suficiente.
Configuración manual del sidecar Envoy en Kubernetes
Veamos un ejemplo paso a paso del despliegue de un servicio Go con sidecar Envoy en Kubernetes. Supongamos que tenemos un servicio order-service que se comunica con payment-service.
Paso 1: ConfigMap con la configuración de Envoy
apiVersion: v1
kind: ConfigMap
metadata:
name: envoy-config
namespace: production
data:
envoy.yaml: |
static_resources:
listeners:
- name: ingress_listener
address:
socket_address:
address: 0.0.0.0
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
access_log:
- name: envoy.access_loggers.stdout
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: local_app
clusters:
- name: local_app
connect_timeout: 0.25s
type: STATIC
load_assignment:
cluster_name: local_app
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 8080
- name: payment_service
connect_timeout: 0.5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: payment_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: payment-service.production.svc.cluster.local
port_value: 10000
admin:
address:
socket_address:
address: 0.0.0.0
port_value: 9901
Paso 2: Deployment con sidecar Envoy
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
initContainers:
- name: iptables-init
image: busybox:1.36
securityContext:
capabilities:
add: ["NET_ADMIN"]
command:
- sh
- -c
- |
iptables -t nat -A OUTPUT -p tcp --dport 8080 -j RETURN
iptables -t nat -A OUTPUT -p tcp -j REDIRECT --to-port 10000
containers:
- name: order-service
image: myregistry/order-service:1.4.2
ports:
- containerPort: 8080
env:
- name: PAYMENT_SERVICE_URL
value: "http://127.0.0.1:10001"
- name: envoy
image: envoyproxy/envoy:v1.29-latest
args: ["-c", "/etc/envoy/envoy.yaml", "--log-level", "info"]
ports:
- containerPort: 10000
name: proxy
- containerPort: 9901
name: admin
volumeMounts:
- name: envoy-config
mountPath: /etc/envoy
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "200m"
volumes:
- name: envoy-config
configMap:
name: envoy-config
Presta atención al initContainer con iptables: intercepta el tráfico saliente y lo redirige a través de Envoy. Esta es la técnica estándar que también utiliza Istio.
Integración con servicios Go: health checks, métricas, trazado
Health checks
El servicio Go debe exponer un endpoint para health check. Ejemplo mínimo:
package main
import (
"net/http"
"encoding/json"
)
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", healthHandler)
mux.HandleFunc("/ready", healthHandler)
http.ListenAndServe(":8080", mux)
}
En la configuración de Envoy, añade un health check activo para el clúster upstream:
clusters:
- name: payment_service
health_checks:
- timeout: 1s
interval: 10s
unhealthy_threshold: 3
healthy_threshold: 2
http_health_check:
path: "/health"
Métricas con Prometheus
Envoy exporta automáticamente métricas en el puerto admin (por defecto 9901) en la ruta /stats/prometheus. Agrega un ServiceMonitor para el Prometheus Operator:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: envoy-sidecar-metrics
namespace: production
spec:
selector:
matchLabels:
app: order-service
endpoints:
- port: admin
path: /stats/prometheus
interval: 15s
Trazado distribuido con OpenTelemetry
Añade la configuración de trazado en Envoy para enviar spans a Jaeger:
tracing:
http:
name: envoy.tracers.zipkin
typed_config:
"@type": type.googleapis.com/envoy.config.trace.v3.ZipkinConfig
collector_cluster: jaeger
collector_endpoint: "/api/v2/spans"
shared_span_context: false
collector_endpoint_version: HTTP_JSON
En el servicio Go, usa go.opentelemetry.io/otel para crear spans y propagar el contexto a través de las cabeceras HTTP (traceparent, b3). Envoy recogerá y continuará el trace automáticamente.
mTLS entre servicios sin operador de service mesh
Configurar mTLS manualmente es la parte más laboriosa, pero totalmente factible. Necesitamos: una CA raíz, certificados para cada servicio y la configuración de Envoy en ambos extremos de la conexión.
Generación de certificados (cert-manager)
Utiliza cert-manager para la emisión y rotación automática de certificados:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: order-service-cert
namespace: production
spec:
secretName: order-service-tls
duration: 24h
renewBefore: 1h
subject:
organizations:
- mycompany
commonName: order-service.production.svc.cluster.local
dnsNames:
- order-service.production.svc.cluster.local
issuerRef:
name: internal-ca
kind: ClusterIssuer
Configuración de mTLS en Envoy (upstream — originación)
clusters:
- name: payment_service
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
common_tls_context:
tls_certificates:
- certificate_chain:
filename: /etc/certs/tls.crt
private_key:
filename: /etc/certs/tls.key
validation_context:
trusted_ca:
filename: /etc/certs/ca.crt
Configuración de mTLS (downstream — terminación)
filter_chains:
- transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
require_client_certificate: true
common_tls_context:
tls_certificates:
- certificate_chain:
filename: /etc/certs/tls.crt
private_key:
filename: /etc/certs/tls.key
validation_context:
trusted_ca:
filename: /etc/certs/ca.crt
El secreto order-service-tls se monta en el pod mediante volumeMounts. Al rotar, cert-manager actualizará el secreto, y Envoy puede configurarse para recargarlo dinámicamente mediante SDS (Secret Discovery Service).
Comparativa con Linkerd y Cilium
Linkerd
Linkerd es un service mesh considerablemente más ligero que Istio. Su data plane está escrito en Rust (linkerd2-proxy), consume menos memoria y CPU. La instalación se reduce a unos pocos comandos de CLI. Entre sus desventajas: configuración menos flexible (sin el nivel de control sobre el enrutamiento que ofrece Envoy) y soporte de protocolos limitado (principalmente HTTP/1, HTTP/2, gRPC). Para equipos que necesiten un inicio rápido sin personalizaciones profundas, es una excelente opción en 2026.
Cilium
Cilium opera a nivel de eBPF en el kernel de Linux, lo que proporciona un overhead mínimo. Combina las funciones de CNI (plugin de red de Kubernetes) y service mesh, eliminando por completo la necesidad de contenedores sidecar. Cilium Service Mesh soporta mTLS, observabilidad L7 y políticas de tráfico. Es ideal para equipos que buscan latencias mínimas y control a nivel de red. Es más complejo de depurar debido a su operación en el kernel.
Sidecar Envoy manual (nuestro enfoque)
Máxima flexibilidad y control. Sin dependencia de operadores externos. Se vuelve más difícil de mantener a medida que crece el número de servicios: las configuraciones deben versionarse y templarse (Helm, Kustomize). Es óptimo para equipos con entre 2 y 20 servicios o con requisitos especiales de enrutamiento.
Recomendaciones de monitorización y observabilidad
Sin un control plane centralizado, la observabilidad es especialmente importante. Stack recomendado:
- Prometheus + Grafana — recolección de métricas desde el endpoint admin de Envoy (
/stats/prometheus). Usa los dashboards prediseñados de Envoy para Grafana (ID: 6693). - Jaeger o Grafana Tempo — recolección de trazas distribuidas. Envoy genera spans automáticamente cuando el trazado está configurado.
- Loki — recolección de access logs de Envoy en formato JSON estructurado. Configura el formato de access log explícitamente:
access_log:
- name: envoy.access_loggers.stdout
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
log_format:
json_format:
timestamp: "%START_TIME%"
method: "%REQ(:METHOD)%"
path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
response_code: "%RESPONSE_CODE%"
duration: "%DURATION%"
upstream_host: "%UPSTREAM_HOST%"
trace_id: "%REQ(X-B3-TRACEID)%"
- Alerting: configura alertas sobre
envoy_cluster_upstream_rq_5xx,envoy_cluster_upstream_cx_connect_faily la latencia P99 a través deenvoy_cluster_upstream_rq_time_bucket.
Conclusiones: cuándo usar un service mesh completo
El enfoque de «sidecar Envoy manual» está justificado cuando:
- Tienes entre 3 y 20 servicios y un equipo DevOps pequeño
- Necesitas control total sobre la configuración sin la «magia» de un operador
- Istio es excesivo en recursos y complejidad
- Tienes requisitos específicos de enrutamiento o de protocolos
Migra a un service mesh completo (Istio, Linkerd, Cilium) cuando:
- El número de servicios supere los 30–50 y mantener configuraciones manuales se vuelva costoso
- Necesites rotación automática de certificados y gestión centralizada de políticas
- Se requiera despliegue canary o traffic shifting a nivel de mesh
- El equipo esté dispuesto a invertir tiempo en aprender y operar el control plane
Construir un service mesh sin Istio en Go con Envoy y Kubernetes es una elección pragmática para equipos maduros que valoran la transparencia y el control sobre su infraestructura. Lo fundamental es no olvidar el versionado de configuraciones, la automatización con Helm/Kustomize y la monitorización oportuna del estado de los proxies.
Tecnologías
Etiquetas
Ruslan Ismailov
Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →