Service Mesh & Infrastructure

A service mesh moves mTLS, retries, routing, circuit breaking, and telemetry into infrastructure beside your workloads— not inside every Spring bean. This chapter explains the sidecar model, Istio traffic and security CRDs, observability without app changes, Linkerd as a lightweight alternative, and when a mesh earns its operational cost.

developer lead architect

What is a service mesh?

A dedicated infrastructure layer that handles service-to-service communication—transparent to application code, implemented as proxies colocated with every workload.

Before meshes, each microservice imported its own HTTP client, TLS trust store, retry policy, and tracing headers. One team used Resilience4j with three retries; another used none; a Go service skipped trace propagation entirely. On-call incidents became archaeology: “Is the 503 from the app, the client, or the network?”

A service mesh inserts a proxy beside each pod (the sidecar pattern). All inbound and outbound traffic flows through that proxy, which enforces policies pushed by a control plane— without recompiling Java, Go, or .NET services. The mesh governs east-west traffic inside the cluster; north-south ingress still uses Gateway / load balancers, often also managed by Istio Gateway resources.

Meshes shine when you have many polyglot services on Kubernetes, ephemeral pod IPs, and a platform team that can own control plane upgrades. They complement—but do not replace—application concerns: sagas, domain authorization, and database consistency stay in code—see Data Patterns and Security.

Sidecar pattern — data plane and control plane

The data plane (Envoy proxies) moves bytes and enforces policy; the control plane (Istiod, historically Pilot) discovers services, validates config, and pushes updates to every proxy.

Data plane — Envoy sidecars

Envoy is a high-performance L4/L7 proxy originally built at Lyft, now the de facto mesh data plane. In Istio, each application pod gets an istio-proxy container injected via mutating webhook. iptables or eBPF rules redirect traffic so the app calls inventory-service:8080 but packets traverse Envoy first. Envoy terminates outbound TLS, applies retries, emits access logs, creates trace spans, and load-balances across upstream pod IPs from Kubernetes endpoints.

Control plane — Istiod (successor to Pilot)

Istiod consolidates what was once split across Pilot (xDS config push), Citadel (certificate authority), and Galley (config validation). It watches Kubernetes Services, Endpoints, and Istio CRDs; computes effective config per sidecar; pushes via xDS APIs (CDS, EDS, LDS, RDS). When you apply a VirtualService, every affected Envoy hot-reloads routing without pod restart—usually within seconds.

PlaneComponentResponsibility
Data plane Envoy sidecar per pod Route, LB, retry, timeout, mTLS, metrics, traces, access logs
Control plane Istiod Service discovery, CRD → Envoy config, CA for workload certs, validation
Ingress Istio ingress gateway (Envoy) North-south entry; same config model as sidecars
flowchart TB
  subgraph podA [Pod Order Service]
    APP1[order-app]
    ENV1[Envoy sidecar]
    APP1 --> ENV1
  end
  subgraph podB [Pod Payment Service]
    APP2[payment-app]
    ENV2[Envoy sidecar]
    APP2 --> ENV2
  end
  ENV1 -->|mTLS| ENV2
  ISTIOD[Istiod control plane] -. xDS config .-> ENV1
  ISTIOD -. xDS config .-> ENV2
🔧 Under the Hood

Integration tests that bypass the sidecar (port-forward directly to app container) miss retry, mTLS, and timeout behavior seen in production. Test through Service DNS like real callers.

What the mesh handles vs what the application handles

Meshes move cross-cutting network concerns to infrastructure—mTLS, L7 retries, outlier detection, traffic split, baseline tracing—so apps focus on business logic. They do not implement sagas or resource-level authorization.

ConcernMesh / infrastructureApplication
mTLS between services Auto cert issuance, rotation, SPIFFE identity on every hop Trust bootstrap policy; user-facing TLS at gateway still configured
Retries Envoy route retry policy (count, per-try timeout, retriable status codes) Idempotency keys; only retry safe operations; business retry limits
Circuit breaking DestinationRule outlier detection — eject unhealthy hosts Resilience4j fallbacks; degraded UX; saga compensation
Distributed tracing Generate/propagate W3C traceparent on every hop automatically Custom spans, business tags (orderId), log correlation in MDC
Traffic shifting / canary VirtualService weights, header-based routes Interpret metrics; feature flags for behavior not routing
L7 access control AuthorizationPolicy — which SA can call which path OAuth JWT validation; BOLA checks on resource IDs
Sagas, outbox, CQRS Domain workflows, transactional outbox, read models

Retry multiplication is the classic foot-gun: Resilience4j retries 3× and Envoy retries 3× → up to 9 calls to a failing Payment service. Platform runbooks should assign one primary retry layer per hop—often mesh for generic HTTP, library only for payment client with idempotency. See Resilience → Retry and Observability → Mesh vs app.

💡 Pro Tip

Document “mesh owns X, app owns Y” in each service README. New hires otherwise duplicate policies and wonder why latency doubled.

Istio architecture — Envoy, Istiod, and legacy components

Istio is the dominant Kubernetes mesh: CNCF graduated, Envoy data plane, CRD-driven configuration. Understanding historical components (Pilot, Citadel, Galley) still helps when reading older docs and migration guides.

Envoy sidecar — data plane

Every meshed pod runs Envoy (unless using Istio ambient mode with ztunnel—advanced topic). Envoy listeners bind to redirected ports; clusters map to upstream Kubernetes service endpoints; filters implement HTTP routing, JWT validation (optional), WASM extensions, and telemetry emission. Sidecar resource limits matter: 128–256 MiB memory per proxy × 200 pods adds up—capacity plan explicitly.

Istiod — unified control plane (evolution of Pilot + Citadel + Galley)

Legacy componentRoleToday
Pilot Service discovery, traffic management config, xDS push to Envoys Merged into Istiod
Citadel Certificate authority — issued workload certs for mTLS Merged into Istiod CA
Galley Validated Istio config, converted to internal format Validation in Istiod; config from K8s API + webhooks
Istiod Single control plane deployment Install via istioctl or Helm; HA replicas for production

Key CRD families

  • Networking — VirtualService, DestinationRule, Gateway, ServiceEntry, Sidecar, WorkloadEntry
  • Security — PeerAuthentication, RequestAuthentication, AuthorizationPolicy
  • Telemetry — Telemetry API (metrics, access logs, tracing exporters to Prometheus/Jaeger)

Sidecar injection: label namespace istio-injection=enabled or pod annotation sidecar.istio.io/inject: "true". Install profiles: demo for learning, default for production baseline. Upgrades require control plane / data plane version skew within supported window—coordinate with platform team and GitOps freeze windows.

📦 Real World

Enterprises run Istiod in HA across three zones, ingress gateways behind cloud LBs, and product teams submit only VirtualService/AuthorizationPolicy YAML via Argo CD—no direct kubectl on prod.

Linkerd — lightweight alternative to Istio

Linkerd targets teams that want secure-by-default east-west mTLS and core observability with minimal YAML surface and lower per-pod overhead than full Envoy sidecars.

DimensionIstioLinkerd
Data plane Envoy (C++) — rich features, larger footprint linkerd2-proxy (Rust) — minimal, fast, opinionated
Config surface Large CRD set — VirtualService, DestinationRule, etc. Simpler — ServiceProfile, TrafficSplit; less fine-grained control
mTLS Configurable PERMISSIVE → STRICT migration On by default; strong security posture out of box
Traffic splitting VirtualService weights, header matches, fault injection TrafficSplit CRD for canaries; fewer routing knobs
Observability Prometheus + Jaeger + Kiali ecosystem Built-in viz; integrates with Prometheus/Grafana
Best for Large estates, multi-cluster, complex L7 routing Small platform teams, mTLS + metrics without Istio ops burden

Choose Istio when you need header-based routing, extensive AuthorizationPolicy, multi-cluster federation, or WASM extensions. Choose Linkerd when ~20–100 services need encrypted east-west traffic and golden metrics with a two-person platform team. Always benchmark p99 latency and memory on your instance types—proxy cost is measurable on t3.small nodes. Other options: Cilium (eBPF, sidecarless L3/L4), Consul Connect (HashiCorp multi-platform).

VirtualService — fine-grained L7 routing

VirtualService defines how requests to a service host are routed: URI match, headers, weighted splits for canaries, timeouts, retries, and fault injection—without changing application code.

A VirtualService binds to one or more hosts (Kubernetes service DNS names like order-service.default.svc.cluster.local). Ordered http (or tcp) routes are evaluated top-down—first match wins. Each route specifies destinations (service + subset), weights, rewrites, redirects, timeouts, retries, and faults.

Routing capabilities

  • URI / header match — route internal QA to v2 via x-canary: true
  • Traffic shifting — 90/10 weight split between v1 and v2 subsets for canary
  • Retries — per-route retry count, timeout, retriable status codes
  • Timeouts — max request duration at proxy—complements app timeouts
  • Fault injection — delay or abort percentage for chaos testing (see Fault injection)
VirtualService — header route + 90/10 canary
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: order-service
spec:
  hosts:
    - order-service
  http:
    - match:
        - headers:
            x-canary:
              exact: "true"
      route:
        - destination:
            host: order-service
            subset: v2
    - route:
        - destination:
            host: order-service
            subset: v1
          weight: 90
        - destination:
            host: order-service
            subset: v2
          weight: 10
      timeout: 10s
      retries:
        attempts: 2
        perTryTimeout: 3s
        retryOn: connect-failure,refused-stream,503

Subset names (v1, v2) must exist in a matching DestinationRule—VirtualService cannot route to subsets that are not defined. Attach VirtualService to Gateway for north-south hosts via spec.gateways field.

DestinationRule — load balancing, pools, outlier detection

DestinationRule configures policies applied to traffic after routing decides the host—subsets, TLS mode, connection limits, load balancer algorithm, and passive circuit breaking via outlier detection.

Subsets for version routing

Map pod labels to named subsets used in canary VirtualServices. During deploy, v1 and v2 pods coexist; weights shift traffic without changing Service selector. Label convention: version: v1 on Deployment pod template.

Load balancing

Algorithms: ROUND_ROBIN (default), LEAST_REQUEST, RANDOM, PASSTHROUGH. LEAST_REQUEST helps when pod CPU varies; session affinity via consistent hash on header/cookie when sticky sessions required—prefer stateless JWT over sticky where possible.

Connection pool — mesh-level bulkhead

Cap http1MaxPendingRequests, maxRequestsPerConnection, and TCP connection limits per upstream host. Prevents one slow Inventory service from holding thousands of open connections from every Order sidecar— complements app-level bulkheads in Resilience → Bulkhead.

Outlier detection — circuit breaking at infrastructure

Envoy passively ejects hosts returning consecutive 5xx or high latency—similar to circuit breaker OPEN state. Tune consecutive5xxErrors, interval, baseEjectionTime, maxEjectionPercent to avoid ejecting all pods during rolling restart (start conservative in staging).

DestinationRule
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: order-service
spec:
  host: order-service
  trafficPolicy:
    loadBalancer:
      simple: LEAST_REQUEST
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        http1MaxPendingRequests: 50
        maxRequestsPerConnection: 2
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 10s
      baseEjectionTime: 30s
      maxEjectionPercent: 50
  subsets:
    - name: v1
      labels:
        version: v1
    - name: v2
      labels:
        version: v2

Gateway — ingress traffic management

Istio Gateway configures Envoy ingress load balancers for north-south traffic—TLS termination, host/port binding, and handoff to VirtualServices that route into the mesh.

Gateway (Istio networking CRD) is not the same object as Kubernetes Gateway API resource—similar purpose, Istio-native model. Gateway selects which ports and hosts ingress gateway pods listen on; VirtualService with gateways: [my-gateway] binds routing rules to external traffic.

Gateway + VirtualService binding
apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: public-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 443
        name: https
        protocol: HTTPS
      tls:
        mode: SIMPLE
        credentialName: api-tls-cert
      hosts:
        - api.example.com
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: api-routes
spec:
  hosts:
    - api.example.com
  gateways:
    - public-gateway
  http:
    - route:
        - destination:
            host: order-service
            port:
              number: 8080

Traffic path: public DNS → cloud load balancer → Istio ingress gateway pods (Envoy) → VirtualService → sidecar-protected service. cert-manager commonly provisions credentialName secrets for TLS. Internal admin APIs skip public Gateway—cluster-internal Service + AuthorizationPolicy only. Complements Communication → API Gateway patterns—some teams use Spring Cloud Gateway inside mesh, others terminate at Istio ingress only.

Canary deployment with Istio — gradual traffic shift

Mesh routing turns canaries into VirtualService weight edits—deploy v2 pods, send 5% traffic, watch RED metrics, increase to 50% then 100%, or rollback instantly to 100% v1.

Traditional canary needed separate load balancers or DNS tricks. With Istio:

  1. Deploy v2 Deployment with version: v2 label—pods register to same Service
  2. DestinationRule defines v1 and v2 subsets
  3. VirtualService starts at 95/5 or 90/10 weight split
  4. Prometheus compares istio_requests_total error rate and latency v1 vs v2
  5. Promote weights or revert—no DNS TTL wait
flowchart TB
  DEP[Deploy v2 pods ready] --> W5[Weight 5 percent v2]
  W5 --> OBS[Observe 15 min SLO]
  OBS -->|pass| W50[Weight 50 percent]
  OBS -->|fail| RB[Rollback 100 percent v1]
  W50 --> OBS2[Observe again]
  OBS2 --> FULL[Weight 100 percent v2]

Automate with Flagger or Argo Rollouts: analysis templates query Prometheus, auto-increment weights, abort on SLO burn. Combine with pod readiness probes and PDB from Deployment → Canary— do not shift traffic until v2 passes health checks. Gate on business metrics (checkout conversion), not only HTTP 5xx—see Observability → SLOs.

⚠️ Pitfall

Random 10% canary without sticky debugging—QA sees bug once, cannot reproduce. Use header-based route for testers before percentage-based user traffic.

Fault injection — test resilience without code changes

VirtualService can inject delays and HTTP aborts on a percentage of requests—validate timeouts, retries, and fallbacks in staging before production incidents prove they never worked.

Fault injection in VirtualService
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: inventory-fault-test
spec:
  hosts:
    - inventory-service
  http:
    - match:
        - headers:
            x-test-fault:
              exact: "enabled"
      fault:
        delay:
          percentage:
            value: 100
          fixedDelay: 5s
        abort:
          percentage:
            value: 10
          httpStatus: 503
      route:
        - destination:
            host: inventory-service

Delay — simulates slow dependency; verify Order service timeout and bulkhead release threads. Abort — returns 503 without hitting app; verify retry policy does not amplify load (retry storm). Scope with header match or dedicated namespace—never blanket fault injection on production without executive approval. Pair with game days validating Resilience4j and mesh outlier detection together.

Security in the service mesh — mTLS and zero trust

Meshes implement zero-trust east-west networking: every pod-to-pod call is encrypted and authenticated by workload identity—not by IP address inside the VPC.

Mutual TLS (mTLS) between all services

In plaintext cluster networking, any compromised pod can call any Service IP. With mesh mTLS, each sidecar presents a client certificate; receiver verifies against Istio CA. Workload identity follows SPIFFE format: spiffe://cluster.local/ns/default/sa/order-service — tied to Kubernetes service account, not pod IP (which changes on restart).

Istiod acts as CA: issues short-lived certs, rotates automatically, distributes to sidecars. Apps often still speak HTTP locally—the sidecar encrypts on the wire between proxies. User-facing OAuth/JWT at north-south gateway remains required—mTLS is service-to-service, not end-user auth. Deep dive on zero trust and JWT: Security.

sequenceDiagram
  participant A as Order sidecar
  participant B as Payment sidecar

  A->>B: ClientHello + order SA cert
  B->>A: ServerHello + payment SA cert
  A->>B: Encrypted HTTP request
  B->>A: Encrypted HTTP response

Migration path: PERMISSIVE → STRICT

PERMISSIVE accepts both plaintext and mTLS—use during rollout while legacy pods lack sidecars. STRICT rejects non-mTLS—target state for production namespaces. Monitor mix percentage in Kiali before flipping STRICT cluster-wide.

PeerAuthentication — enforce mTLS mode

PeerAuthentication CRD sets mTLS mode per mesh, namespace, or workload selector—platform teams use it to roll out encryption without breaking unmigrated services.

STRICT mTLS for production namespace
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT

More specific policies override broader: workload-level PeerAuthentication beats namespace default beats mesh-wide default. Pair STRICT with sidecar injection on every pod in namespace—otherwise legitimate traffic fails. VMs and external services use WorkloadEntry + ServiceEntry with appropriate mTLS config.

⚠️ Pitfall

STRICT in namespace before all workloads have sidecars—mysterious connection resets, blame on “Istio being flaky.” Migrate PERMISSIVE first, verify Kiali lock icons at 100%.

AuthorizationPolicy — L7 access control

AuthorizationPolicy defines who can call what—by source service account, namespace, JWT claims, IP, HTTP method, and path—implementing deny-by-default east-west firewalling at the proxy.

After mTLS proves identity, AuthorizationPolicy decides permission: “checkout-service SA may POST /api/v1/orders; inventory-service SA may GET /api/v1/orders/{id}; everyone else DENY.” Reduces lateral movement—attacker in compromised marketing pod cannot reach admin APIs.

This is not a replacement for application authorization: mesh cannot know if user A owns order 123—BOLA checks stay in Java with JWT subject validation. Mesh blocks wrong services; app blocks wrong users.

ALLOW policy for checkout → order
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: order-service-policy
  namespace: production
spec:
  selector:
    matchLabels:
      app: order-service
  action: ALLOW
  rules:
    - from:
        - source:
            principals:
              - cluster.local/ns/production/sa/checkout-bff
      to:
        - operation:
            methods: ["GET", "POST"]
            paths: ["/api/v1/orders", "/api/v1/orders/*"]
    - from:
        - source:
            principals:
              - cluster.local/ns/production/sa/inventory-service
      to:
        - operation:
            methods: ["GET"]
            paths: ["/api/v1/orders/*"]

Deny policies explicitly block (e.g. block POST from legacy SA during deprecation). Default deny requires enabling authorization on mesh—document break-glass for debugging. Audit policies in GitOps PRs like application code—one overly broad ALLOW undoes zero trust.

Observability via the mesh — tracing and RED metrics

Sidecars see every byte in and out—uniform telemetry across Java, Go, and legacy .NET without importing OpenTelemetry SDK in every repository (though business spans still help).

Automatic distributed tracing — no code changes

Envoy generates spans for each request hop and propagates W3C traceparent headers automatically. Configure export via Istio Telemetry API to Jaeger, Tempo, Zipkin, or Datadog. Trace shows sidecar-to-sidecar latency breakdown—identify whether slowness is network, upstream app, or retry amplification.

Limitations: mesh spans are generic HTTP—no orderId unless app adds OpenTelemetry annotations or baggage. Double instrumentation risk: app and sidecar both create SERVER spans—configure telemetry to avoid duplicate trace trees. Full stack guidance: Observability → OpenTelemetry.

Traffic metrics — RED without Micrometer

For every meshed service, Prometheus scrapes Istio metrics:

  • Rateistio_requests_total (requests per second by source, destination, response_code)
  • Errors — ratio of 5xx (and optionally 4xx) over total requests
  • Durationistio_request_duration_milliseconds histogram buckets for p50/p95/p99

Compare v1 vs v2 during canary by filtering on destination subset labels or workload version. Grafana dashboards per namespace are standard platform deliverables—see Observability → RED. App-level business metrics (orders placed, revenue) still require Micrometer in code—mesh does not know your domain.

PromQL — error rate for order-service
sum(rate(istio_requests_total{
  destination_service_name="order-service",
  response_code=~"5.."
}[5m]))
/
sum(rate(istio_requests_total{
  destination_service_name="order-service"
}[5m]))

Kiali — Istio service graph dashboard

Kiali is the operational UI for Istio: live topology graph, traffic rates on edges, mTLS lock icons, health of VirtualServices, and drill-down into metrics/traces per service.

Service graph — nodes are workloads/services; edges show request rate, error percentage, and latency animation. During canary, visually confirm traffic split matches VirtualService weights— “is v2 actually getting 10%?”

mTLS indicators — lock icons on edges show encrypted traffic; broken lock highlights PERMISSIVE/plaintext paths still in use before STRICT cutover.

Config validation — flags broken VirtualService/DestinationRule references (subset not found, host typo)—catches GitOps mistakes before on-call pages.

Integrated views — jump to Grafana dashboards or Jaeger traces from a service node—reduces tab switching during incidents. Install Kiali as part of Istio addon profile or standalone; restrict access via OAuth2 proxy—graph exposes internal architecture.

🎯 Interview Tip

Contrast “Java-only Micrometer tracing” vs “mesh covers Go legacy too.” Mention Kiali for ops, Prometheus for alerting, OTel in app for business span attributes—three layers, one trace ID.

When a mesh is not worth it

Meshes add CPU/memory per pod, control plane ops, and debugging indirection. Prove you need one—do not adopt Istio because a conference slide said so.

  • Fewer than ~10 services — Spring Cloud Gateway + Resilience4j + OTel agent may suffice
  • No Kubernetes — VM estates: library stack or Consul Connect; full sidecars awkward
  • No platform team — misconfigured VirtualService causes URX 503 mysteries; hire ops first
  • Strict latency budget — measure sidecar hop cost; HFT paths may exclude mesh
  • Serverless-heavy — short-lived functions poor fit for classic sidecar (ambient mesh evolving)
⚖️ Trade-off

Modular monolith on K8s with NetworkPolicy + one gateway is valid. Microservices do not mandate Istio—match infrastructure to team size and risk.