DevOps

Service Mesh sin Istio: construcción de red entre servicios en Go con Envoy y Kubernetes

Ruslan Ismailov Publicado 12 min de lectura
S

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_fail y la latencia P99 a través de envoy_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í →