Service Mesh en Kubernetes: Istio vs Linkerd vs Cilium

Comparativa práctica de Istio, Linkerd y Cilium Service Mesh: arquitectura, instalación real, mTLS, traffic splitting, overhead y cuándo NO necesitas un service mesh

Imagina que tienes 40 microservicios hablando entre ellos en texto plano. De repente, un equipo te pide hacer un canary deployment del 5% para una nueva versión; otro pregunta por qué el proceso de checkout tarda 300 ms; y el equipo de seguridad exige cifrado en tránsito dentro de todo el clúster.

¿Cómo lo resuelves? Podrías intentar meter lógica de red en cada librería de cada servicio (y repetirlo para Go, Python y Java…), o puedes mover toda esa responsabilidad a la capa de red.

Eso es, en esencia, un Service Mesh. Sin embargo, también es la pieza de infraestructura que más veces he visto instalar en entornos de producción sin que nadie sepa exactamente qué problema está resolviendo. En esta guía vamos a comparar las tres opciones serias que quedan en 2026 y, lo más importante, dedicaremos una sección entera a la pregunta más rentable que te puedes hacer: cuándo NO instalarlo.

🎯 Qué problema resuelve realmente

Un service mesh intercepta el tráfico entre pods y aplica políticas sin que tengas que tocar ni una sola línea de código de tu aplicación. Básicamente, estas cuatro capacidades son las que justifican su existencia:

mTLS automático

Cifrado y autenticación mutua entre todos los pods, con rotación de certificados gestionada automáticamente. Sin un mesh, cada servicio necesitaría su propia gestión de certificados. Ojo: esto complementa —no sustituye— a las NetworkPolicies y RBAC.

Observabilidad L7

Obtienes las golden signals (tasa de peticiones, errores, latencia p50/p95/p99) por cada par origen-destino sin instrumentar la app. Mientras que una NetworkPolicy solo ve IPs y puertos, un mesh entiende códigos HTTP y rutas.

Traffic splitting

Te permite enviar, por ejemplo, el 5% del tráfico a una versión nueva. Sin un mesh, esto es un juego de azar con las réplicas de tus Deployments (si tienes 3 pods, lo mínimo que puedes enviar es un 33%).

Retries, timeouts y circuit breaking

Reintentos inteligentes, timeouts por ruta y eyección de instancias que fallan, aplicados de forma consistente en todos los lenguajes de tu stack.

Lo que un service mesh NO resuelve

No arregla una aplicación lenta, no sustituye a un balanceador de carga de entrada, no elimina la necesidad de trazas distribuidas con contexto de negocio (el mesh propaga headers, pero tu app debe reenviarlos) y no compensa una arquitectura de microservicios mal diseñada.

🏗️ Arquitectura: sidecar vs sidecarless

La gran diferencia técnica entre las tres opciones actuales es, fundamentalmente, dónde vive el proxy.

Diagrama Mermaid

Istio — Envoy, dos modos

  • Sidecar: Un contenedor Envoy inyectado en cada pod. Tienes máxima potencia (todo el filtro L7 de Envoy), pero también el máximo coste: un proxy por cada pod.
  • Ambient mode: La respuesta de Istio al coste del sidecar. Aquí no hay sidecars; un agente Rust llamado ztunnel gestiona el mTLS y L4 a nivel de nodo, y los waypoint proxies (Envoy) se despliegan solo donde realmente necesitas capa L7.
  • Plano de control: istiod.

Linkerd — micro-proxy en Rust

  • Usa sidecars, pero con un proxy propio escrito en Rust (linkerd2-proxy) en lugar de Envoy. Su filosofía es: «menos es más». Menos superficie de ataque, menos memoria y menos configuración que romper.
  • No te llena de CRDs masivos; usa el estándar HTTPRoute de la Gateway API para el routing.
  • Plano de control: linkerd-destination, linkerd-identity, linkerd-proxy-injector.

Cilium Service Mesh — eBPF primero

  • Su datapath es eBPF directamente en el kernel: no hay proxy en el camino para tráfico L3/L4. Como Cilium suele ser ya tu CNI, el mesh no añade una capa extra de complejidad.
  • Ofrece cifrado con WireGuard o IPsec a nivel de nodo y autenticación basada en SPIFFE/SPIRE.
  • Si necesitas L7 (routing HTTP), delega el trabajo a un Envoy por nodo, activándolo solo cuando la política lo requiere.

Cilium no es un service mesh completo

Cilium es increíble en L3/L4, cifrado y observabilidad (Hubble), pero no tiene el equivalente a retries con presupuesto, circuit breaking por outlier detection ni traffic splitting interno (pod a pod) al nivel de Istio o Linkerd. Su traffic splitting pasa por la Gateway API (el borde). Si buscas canary entre servicios internos, esto es un factor decisivo.

🚀 Instalación real

Si estás listo para probarlos, aquí tienes los comandos clave para cada uno.

Istio — ambient mode (recomendado en despliegues nuevos)

# Instalar Istio con el perfil ambient
istioctl install --set profile=ambient --skip-confirmation

# Enrolar un namespace en ambient (el CNI plugin usará ztunnel
# para los pods nuevos y los que se reinicien)
kubectl label namespace mi-app istio.io/dataplane-mode=ambient

# Verificar que los workloads están enrolados
istioctl ztunnel-config workloads

Istio — modo sidecar

istioctl install --skip-confirmation

# Habilitar inyección de sidecar por namespace
kubectl label --overwrite namespace mi-app istio-injection=enabled

# Los pods existentes NO se inyectan solos: hay que reiniciarlos
kubectl rollout restart deployment -n mi-app

Migración de sidecar a ambient

Al pasar un namespace a ambient, recuerda quitar la etiqueta de inyección (istio-injection o la de revisión) y añadir istio.io/dataplane-mode=ambient. Si dejas ambas, acabarás con un sidecar y un ztunnel funcionando a la vez.

Linkerd

# Comprobar el CLI (la versión de servidor aparecerá tras instalar el control plane)
linkerd version

# 1) CRDs, 2) control plane
linkerd install --crds | kubectl apply -f -
linkerd install | kubectl apply -f -

# Validar la instalación completa
linkerd check

Para la inyección por namespace, usa esta anotación:

apiVersion: v1
kind: Namespace
metadata:
  name: mi-app
  annotations:
    linkerd.io/inject: enabled

Cilium Service Mesh

# Gateway API (requiere kube-proxy replacement)
cilium install \
    --set kubeProxyReplacement=true \
    --set gatewayAPI.enabled=true

# Mutual authentication basada en SPIRE
cilium install \
    --set authentication.mutual.spire.enabled=true \
    --set authentication.mutual.spire.install.enabled=true

# Balanceo L7 con Envoy
cilium install \
    --set kubeProxyReplacement=true \
    --set envoyConfig.enabled=true \
    --set loadBalancer.l7.backend=envoy

# Observabilidad
cilium hubble enable

🔐 mTLS y políticas de autorización

Una vez instalado, ¿cómo aplicamos la seguridad? Cada uno tiene su forma de entender el mundo.

Istio

Para aplicar mTLS estricto en todo un namespace:

apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: mi-app
spec:
  mtls:
    mode: STRICT

Y para aislar un namespace (que solo acepte tráfico de su propio entorno):

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: mi-app-isolation
  namespace: mi-app
spec:
  action: ALLOW
  rules:
    - from:
        - source:
            namespaces: ["mi-app"]

Migra a STRICT con PERMISSIVE de por medio

Nunca apliques STRICT directamente sobre un namespace con clientes que aún no tengan mesh; cortarás el tráfico al instante. Usa primero el modo PERMISSIVE para verificar en tus métricas que todo va cifrado y, una vez confirmado, endurece la política.

Linkerd

Aquí la vida es más fácil: el mTLS entre pods inyectados está activo por defecto. Las políticas se gestionan por ruta:

apiVersion: policy.linkerd.io/v1alpha1
kind: AuthorizationPolicy
metadata:
  name: authors-get-policy
  namespace: booksapp
spec:
  targetRef:
    group: policy.linkerd.io
    kind: HTTPRoute
    name: authors-get-route
  requiredAuthenticationRefs:
    - name: authors-get-authn
      kind: MeshTLSAuthentication
      group: policy.linkerd.io
---
apiVersion: policy.linkerd.io/v1alpha1
kind: MeshTLSAuthentication
metadata:
  name: authors-get-authn
  namespace: booksapp
spec:
  identities:
    - "books.booksapp.serviceaccount.identity.linkerd.cluster.local"
    - "webapp.booksapp.serviceaccount.identity.linkerd.cluster.local"

Cilium

En Cilium, la autenticación mutua vive dentro de la propia CiliumNetworkPolicy:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: api-requiere-auth
  namespace: mi-app
spec:
  endpointSelector:
    matchLabels:
      app: api
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: frontend
      authentication:
        mode: "required"
      toPorts:
        - ports:
            - port: "8080"
              protocol: TCP
          rules:
            http:
              - method: "GET"
                path: "/api/v1/.*"

Con mode: "required", Cilium solo permitirá la conexión si ambos workloads se han autenticado mediante SPIRE.

🔀 Canary y traffic splitting

¿Cómo gestionamos el despliegue gradual de nuevas versiones?

Istio — VirtualService con pesos

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: reviews-route
spec:
  hosts:
    - reviews.prod.svc.cluster.local
  http:
    - route:
        - destination:
            host: reviews.prod.svc.cluster.local
            subset: v1
          weight: 75
        - destination:
            host: reviews.prod.svc.cluster.local
            subset: v2
          weight: 25

Linkerd — Gateway API HTTPRoute

apiVersion: policy.linkerd.io/v1beta2
kind: HTTPRoute
metadata:
  name: bb-route
  namespace: traffic-shift-demo
spec:
  parentRefs:
    - name: bb
      kind: Service
      group: core
      port: 8080
  rules:
    - backendRefs:
        - name: bb
          port: 8080
          weight: 90
        - name: bb-v2
          port: 8080
          weight: 10

Cilium — Gateway API en el borde

apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: echo-route
spec:
  parentRefs:
    - name: cilium-gw
  hostnames:
    - "*"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /echo
      backendRefs:
        - name: echo-1
          port: 8080
          weight: 99
        - name: echo-2
          port: 8090
          weight: 1

No shiftees tráfico a mano

Editar pesos manualmente en producción es la receta perfecta para un incidente un viernes por la tarde. Tanto Istio como Linkerd se integran con Flagger, que automatiza el proceso de canary basándose en métricas reales y hace rollback automático si algo falla.

📊 Comparativa

Para que lo veas de un vistazo:

Aspecto Istio (ambient) Istio (sidecar) Linkerd Cilium
Datapath ztunnel (Rust) + waypoint Envoy Envoy por pod Micro-proxy Rust por pod eBPF + Envoy por nodo
mTLS por workload ✅ ✅ ✅ (por defecto) ⚠️ SPIFFE auth + WireGuard/IPsec
Traffic split interno ✅ ✅ ✅ ❌ solo vía gateway
Retries / circuit breaking ✅ (waypoint) ✅ completo ✅ básico ❌
Observabilidad Prometheus + Kiali Prometheus + Kiali linkerd viz Hubble
Multi-cluster ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ Cluster Mesh
Superficie de config Alta Muy alta Baja Media
Curva de aprendizaje ⭐⭐ dura ⭐ muy dura ⭐⭐⭐⭐⭐ suave ⭐⭐⭐ (asume Cilium CNI)
Requisito de CNI Cualquiera Cualquiera Cualquiera Cilium obligatorio
Gobernanza CNCF Graduated CNCF Graduated CNCF Graduated CNCF Graduated

⚡ Overhead: órdenes de magnitud, no promesas

Sobre estas cifras

Las siguientes cifras son órdenes de magnitud basadas en benchmarks del Service Mesh Performance de la CNCF. No son mediciones propias. El overhead real dependerá de tu RPS, el tamaño de tus payloads y de si tienes filtros L7 activos. Mide siempre en tu propio entorno.

Modelo Latencia añadida (p99, orden) CPU/memoria por pod (orden)
Istio sidecar (Envoy) Unidades de ms (~2-5 ms) ~50-100 MB y decenas de milicores por pod
Istio ambient (solo ztunnel, L4) Menos de 1 ms Coste por nodo, no por pod
Istio ambient + waypoint L7 Unidades de ms en el salto L7 Coste por namespace/servicio
Linkerd micro-proxy Sub-milisegundo a ~1 ms ~10-20 MB por pod
Cilium eBPF (L4) Prácticamente nulo (sin proxy) Coste por nodo (agente)
Cilium con Envoy L7 Unidades de ms en flujos L7 Coste por nodo

Lo que sí podemos concluir con seguridad es esto:
1. El modelo por pod no escala bien. 1.000 pods con Envoy son 1.000 proxies consumiendo memoria base. Ambient y eBPF mueven ese coste al nivel del nodo.
2. L7 tiene un precio. Parsear HTTP siempre será más caro que reenviar bytes. Si solo necesitas mTLS y L4, no pagues por L7.
3. La latencia rara vez es el problema real. El verdadero cuello de botella suele ser el consumo de recursos y la carga operativa.

🔭 Observabilidad

Los tres se llevan de maravilla con Prometheus y encajan en cualquier stack de observabilidad.

# Istio: dashboard Kiali (topología + tráfico en vivo)
istioctl dashboard kiali

# Linkerd: extensión viz, métricas golden signals en vivo
linkerd viz install | kubectl apply -f -
linkerd viz dashboard

# Cilium: flujos L3/L4/L7 con Hubble
cilium hubble enable
hubble observe --namespace mi-app --protocol http

Métricas != trazas

Ningún mesh genera trazas distribuidas completas por sí solo. Propagan los headers, pero si tu aplicación no reenvía los headers traceparent / b3 entre peticiones, las trazas saldrán rotas. Ese es un trabajo de la aplicación, no del mesh.

🛑 Cuándo NO necesitas un service mesh

Esta es la sección más importante de esta guía. Un service mesh es una base de datos de configuración de red distribuida delante de todo tu tráfico; si falla, falla todo.

No lo instales si:

  • Tienes menos de ~10 servicios. Con 5 servicios te conoces todas las llamadas de memoria. El mesh te dará más dolores de cabeza que soluciones.
  • Todo tu stack usa un solo lenguaje. Si todo es Go o todo es Java, una librería compartida (o gRPC con interceptores) te da lo mismo con mucha menos maquinaria.
  • Tu problema es de entrada, no de «este-oeste». Si solo necesitas TLS y routing por host, un Ingress Controller o Gateway API es suficiente.
  • Solo necesitas «cifrado en tránsito» sin identidad por workload. WireGuard a nivel de CNI (como Cilium) lo cubre con un coste ínfimo. Si el auditor pide identidad criptográfica por servicio, entonces sí ve a por el mTLS del mesh.
  • Tu segmentación se resuelve con NetworkPolicies. «Frontend habla con API» es una NetworkPolicy, no requiere un mesh.
  • No tienes un equipo de plataforma. Un mesh requiere alguien que entienda su modelo de datos y sepa depurarlo a las 3 de la mañana. Sin ese dueño, el mesh es deuda técnica con un dashboard bonito.
  • Aún no tienes métricas básicas. Si ni siquiera tienes Prometheus funcionando, estás intentando construir la casa empezando por el tejado.

El coste oculto: depuración

Con un mesh, un error 503 puede venir de la app, del proxy origen, del proxy destino, de un timeout de política, de un certificado caducado o de una política de autorización mal escrita. La superficie de depuración se multiplica exponencialmente.

🧭 Guía de decisión

Para terminar, te dejo este resumen rápido:

  • Ya usas Cilium y solo quieres mTLS + observabilidad L4/L7 y Gateway API $\rightarrow$ Cilium Service Mesh. Sin componentes nuevos y coste casi nulo.
  • Quieres mTLS y golden signals mañana, con el mínimo de conceptos nuevos $\rightarrow$ Linkerd. Es el mesh que se instala en una tarde y no te despierta por la noche.
  • Necesitas routing L7 complejo, multi-cluster o políticas de autorización muy finas $\rightarrow$ Istio en ambient mode. Potencia máxima sin el impuesto del sidecar.
  • Tienes una inversión enorme en filtros Envoy a medida $\rightarrow$ Istio sidecar, hasta que el modo ambient esté maduro para tu caso.
  • Ninguna de las anteriores encaja $\rightarrow$ Probablemente no necesites un service mesh todavía. Vuelve cuando el problema sea real.

🔗 Enlaces relacionados

  • Kubernetes – Orquestación de contenedores
  • Probes en Kubernetes
  • Seguridad en Kubernetes: RBAC y NetworkPolicies
  • Comparación de balanceadores de carga
  • Stack de observabilidad
  • Comparación VPN Overlay

Documentación oficial

Suscríbete al blog por correo electrónico

Introduce tu correo electrónico para suscribirte a este blog y recibir avisos de nuevas entradas.

Deja un comentario