Saltar a contenido

Service Mesh en Kubernetes: Istio vs Linkerd vs Cilium

Tienes 40 microservicios hablando entre ellos en texto plano. Un equipo quiere hacer canary del 5%, otro pregunta por qué el checkout tarda 300 ms y seguridad exige cifrado en tránsito dentro del cluster. Puedes resolverlo con librerías en cada servicio (y repetirlo en Go, Python y Java), o mover esa lógica a la capa de red.

Eso es un service mesh. Y también es la pieza de infraestructura que más veces he visto instalar sin que nadie supiera exactamente qué problema estaba resolviendo. Esta guía compara las tres opciones serias que quedan en 2026 — y dedica una sección entera a la pregunta más rentable: cuándo no instalarlo.

🎯 Qué problema resuelve realmente

Un service mesh intercepta el tráfico entre pods y aplica políticas sin tocar el código de la aplicación. Cuatro capacidades justifican su existencia:

mTLS automático

Cifrado y autenticación mutua entre todos los pods, con rotación de certificados gestionada. Sin mesh, cada servicio necesita su propio TLS y su propia gestión de certificados. Complementa —no sustituye— a las NetworkPolicies y RBAC.

Observabilidad L7

Métricas golden signals (tasa de peticiones, tasa de error, latencia p50/p95/p99) por par origen-destino, sin instrumentar la aplicación. Un mesh ve códigos HTTP y rutas; una NetworkPolicy solo ve IPs y puertos.

Traffic splitting

Enviar el 5% del tráfico a la versión nueva. Sin mesh esto se hace jugando con réplicas de Deployment (granularidad pésima: con 3 pods lo mínimo es 33%).

Retries, timeouts y circuit breaking

Reintentos con presupuesto, timeouts por ruta y eyección de instancias que fallan — aplicados de forma consistente en todos los lenguajes del 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 cortada.

🏗️ Arquitectura: sidecar vs sidecarless

La diferencia fundamental entre las tres opciones es dónde vive el proxy.

graph TB
    subgraph "Istio sidecar (clásico)"
        A1[App v1] --- P1[Envoy sidecar]
        A2[App v2] --- P2[Envoy sidecar]
        P1 <-->|mTLS| P2
        P1 -.config.- IS[istiod]
        P2 -.config.- IS
    end

    subgraph "Istio ambient / Linkerd"
        B1[App v1] --> Z1[ztunnel / micro-proxy nodo]
        B2[App v2] --> Z2[ztunnel / micro-proxy nodo]
        Z1 <-->|mTLS| Z2
        Z1 --> WP[waypoint L7 opcional]
    end

    subgraph "Cilium eBPF"
        C1[App v1] --> K1[eBPF en kernel]
        C2[App v2] --> K2[eBPF en kernel]
        K1 <-->|WireGuard/IPsec| K2
        K1 -.L7 solo si hace falta.- EV[Envoy por nodo]
    end

Istio — Envoy, dos modos

  • Sidecar: un contenedor Envoy inyectado en cada pod. Máxima potencia (todo el filtro L7 de Envoy), máximo coste: un proxy por pod.
  • Ambient mode: sin sidecars. ztunnel (agente Rust por nodo) hace mTLS y L4; los waypoint proxies (Envoy, por namespace o servicio) se despliegan solo donde necesitas L7. Es la respuesta de Istio al coste del sidecar.
  • Plano de control: istiod.

Linkerd — micro-proxy en Rust

  • Sidecar, pero un proxy propio escrito en Rust (linkerd2-proxy), no Envoy. Hace mucho menos que Envoy a propósito: menos superficie, menos memoria, menos configuración que romper.
  • Sin CRDs de configuración masivos: usa Gateway API estándar (HTTPRoute) para routing.
  • Plano de control: linkerd-destination, linkerd-identity, linkerd-proxy-injector.

Cilium Service Mesh — eBPF primero

  • El datapath es eBPF en el kernel: sin proxy en el camino para L3/L4. Cilium ya es tu CNI, así que el mesh no añade una capa nueva.
  • Cifrado con WireGuard o IPsec a nivel de nodo (no mTLS por identidad de workload en el sentido clásico), y mutual authentication basada en SPIFFE/SPIRE para políticas de identidad.
  • L7 (routing HTTP, Gateway API) se delega a un Envoy por nodo, activado solo cuando la política lo requiere.

Cilium no es un service mesh completo

Cilium cubre muy bien L3/L4, cifrado, observabilidad (Hubble) y Gateway API. Pero no tiene el equivalente a retries con presupuesto, circuit breaking por outlier detection ni traffic splitting mesh-interno (pod a pod) al nivel de Istio/Linkerd. El traffic splitting de Cilium pasa por Gateway API, es decir, por el gateway de entrada. Si tu caso es canary entre servicios internos, esto importa.

🚀 Instalación real

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 hay que quitar la etiqueta de inyección (istio-injection o la etiqueta de revisión) además de añadir istio.io/dataplane-mode=ambient. Si dejas ambas, tendrás sidecar y ztunnel 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

Inyección por namespace mediante 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

Istio

mTLS estricto en todo el namespace:

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

Aislamiento por namespace (solo acepta tráfico del propio namespace):

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

Aplicar STRICT directamente sobre un namespace con clientes sin mesh corta el tráfico al instante. El modo PERMISSIVE (por defecto) acepta ambos, permite verificar en métricas que todo va cifrado y luego endurecer.

Linkerd

mTLS entre pods inyectados está activo por defecto, sin configuración. Las políticas se expresan 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

La autenticación mutua se activa dentro de una CiliumNetworkPolicy, no como recurso aparte:

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 permite la conexión si ambos workloads se han autenticado mutuamente vía SPIRE.

🔀 Canary y traffic splitting

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 cómo se producen los incidentes de viernes por la tarde. Tanto Istio como Linkerd se integran con Flagger, que promueve el canary automáticamente según métricas (tasa de éxito, latencia p99) y hace rollback solo si se degradan.

📊 Comparativa

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 cifras siguientes son órdenes de magnitud extraídas de los benchmarks publicados por los propios proyectos y del Service Mesh Performance de la CNCF. No son mediciones propias. El overhead depende brutalmente de RPS, tamaño de payload, si hay filtros L7 activos y del CPU del nodo. Mide en tu cluster antes de decidir.

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

Las conclusiones estructurales (que sí son fiables, independientemente de los números exactos):

  1. El modelo por pod escala mal. 1.000 pods con sidecar Envoy son 1.000 proxies con su memoria base. Ambient y eBPF cambian la unidad de coste de pod a nodo.
  2. L7 cuesta. Parsear HTTP siempre cuesta más que reenviar bytes. Si solo necesitas mTLS y L4, no pagues L7.
  3. La latencia añadida rara vez es el cuello de botella. Suele serlo el consumo de recursos y la carga operativa.

🔭 Observabilidad

Los tres exportan métricas a Prometheus y encajan con el stack de observabilidad del sitio.

# 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. Propaga headers de trazado, pero si tu aplicación no reenvía los headers traceparent / b3 entre peticiones entrantes y salientes, las trazas saldrán rotas. Ese trabajo es de la app, no del mesh.

🛑 Cuándo NO necesitas un service mesh

La sección más importante de esta guía. Un service mesh es una base de datos distribuida de configuración de red delante de todo tu tráfico de producción — y cuando falla, falla en todos los servicios a la vez.

No lo instales si:

  • Tienes menos de ~10 servicios. Con 5 servicios conoces todas las llamadas de memoria. El mesh añade más complejidad operativa que la que elimina.
  • Todo tu stack es un lenguaje. Si todo es Go o todo es Java, una librería compartida (o Spring Cloud, o gRPC con sus interceptores) te da retries, timeouts y mTLS con muchísima menos maquinaria.
  • Tu problema real es entrada, no este-oeste. Si solo necesitas TLS terminado y routing por host/path, un Ingress Controller o Gateway API basta. El mesh es para tráfico entre servicios.
  • Tu requisito de cifrado es "cifrado en tránsito", sin identidad por workload. WireGuard a nivel de CNI (Cilium, Calico) lo cubre con una fracción del coste. Si el auditor exige identidad criptográfica por servicio, entonces sí necesitas mTLS de mesh.
  • Tu segmentación se resuelve con NetworkPolicies. "Frontend solo habla con API" es una NetworkPolicy, no un mesh.
  • No tienes un equipo de plataforma. Un mesh necesita alguien que entienda su modelo de datos, siga sus upgrades y sepa depurarlo a las 3 de la mañana. Sin ese dueño, el mesh es deuda técnica con dashboard bonito.
  • Aún no tienes métricas básicas. Si no tienes ni Prometheus con métricas de aplicación, el mesh es empezar la casa por el tejado.

El coste oculto: depuración

Con mesh, un 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 AuthorizationPolicy mal escrita. La superficie de depuración se multiplica. Ese coste se paga en cada incidente, no solo el día de la instalación.

🧭 Guía de decisión

  • Ya usas Cilium como CNI y solo quieres mTLS+observabilidad L4/L7 y Gateway APICilium Service Mesh. Cero componentes nuevos, coste casi nulo.
  • Quieres mTLS y golden signals mañana, con el mínimo de conceptos nuevosLinkerd. Es el mesh que se instala en una tarde y no te despierta de noche.
  • Necesitas routing L7 complejo, multi-cluster, políticas de autorización finas o extensión con WASMIstio en ambient mode. Toda la potencia sin el impuesto del sidecar por pod.
  • Tienes una inversión grande en filtros Envoy o EnvoyFilter a medidaIstio sidecar, hasta que ambient cubra tu caso.
  • Ninguna de las anteriores describe tu situación → probablemente no necesites un service mesh todavía. Vuelve cuando tengas el problema.

🔗 Enlaces relacionados

Documentación oficial