Service Mesh in Kubernetes
What is a Service Mesh?
A service mesh is a dedicated infrastructure layer that handles all service-to-service communication inside a Kubernetes cluster — without changing application code.
WITHOUT Service Mesh:

WITH Service Mesh:

Core Features
| Feature | What it gives you |
|---|---|
| mTLS | Encrypted + authenticated service-to-service traffic |
| Traffic Management | Canary, blue/green, weighted routing, mirroring |
| Observability | Automatic metrics, traces, logs per service |
| Retries & Timeouts | Resilience without code changes |
| Circuit Breaking | Stop cascading failures |
| Rate Limiting | Protect services from overload |
| Access Policy | Allow/deny between specific services |
Service Mesh Landscape
| Mesh | Best for | Complexity |
|---|---|---|
| Istio | Full-featured, enterprise | 🔴 High |
| Linkerd | Simple, lightweight, fast | 🟢 Low |
| Consul Connect | Multi-cluster, multi-cloud | 🟠 Medium |
| Cilium | eBPF-based, no sidecar | 🟠 Medium |
| Open Service Mesh | AKS native (deprecated) | 🟢 Low |
Option 1 — Istio (Most Feature-Rich)
Architecture
┌─────────────────────────────────────────────────────┐│ CONTROL PLANE ││ istiod ││ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ ││ │ Pilot │ │ Citadel │ │ Galley │ ││ │(traffic) │ │ (certs) │ │ (config valid.) │ ││ └──────────┘ └──────────┘ └──────────────────┘ │└─────────────────────────────────────────────────────┘ ↓ xDS API┌─────────────────────────────────────────────────────┐│ DATA PLANE ││ Pod A Pod B ││ ┌─────────┬───────────┐ ┌──────────┬──────┐ ││ │ App │ Envoy │──mTLS─▶│ Envoy │ App │ ││ │Container│ Sidecar │ │ Sidecar │ │ ││ └─────────┴───────────┘ └──────────┴──────┘ │└─────────────────────────────────────────────────────┘
Install Istio
# Download istioctlcurl -L https://istio.io/downloadIstio | sh -export PATH=$PWD/istio-1.20.0/bin:$PATH# Install with default profileistioctl install --set profile=default -y# Verifyistioctl verify-installkubectl get pods -n istio-system# Enable sidecar injection for a namespacekubectl label namespace production istio-injection=enabled# Check injection is workingkubectl get namespace production --show-labels
Sidecar Injection
# Namespace-level (all pods in namespace)apiVersion: v1kind: Namespacemetadata: name: production labels: istio-injection: enabled # ← auto-inject Envoy sidecar---# Pod-level override (opt out specific pod)apiVersion: apps/v1kind: Deploymentmetadata: name: legacy-appspec: template: metadata: annotations: sidecar.istio.io/inject: "false" # ← skip this pod
Traffic Management
VirtualService — routing rules
apiVersion: networking.istio.io/v1alpha3kind: VirtualServicemetadata: name: api-routing namespace: productionspec: hosts: - api-service http: # Canary — 10% to v2, 90% to v1 - route: - destination: host: api-service subset: v1 weight: 90 - destination: host: api-service subset: v2 weight: 10 # Timeout & retry timeout: 10s retries: attempts: 3 perTryTimeout: 3s retryOn: "5xx,reset,connect-failure"
DestinationRule — load balancing & circuit breaking
apiVersion: networking.istio.io/v1alpha3kind: DestinationRulemetadata: name: api-destination namespace: productionspec: host: api-service trafficPolicy: # Load balancing loadBalancer: simple: LEAST_CONN # Connection pool (circuit breaking) connectionPool: tcp: maxConnections: 100 http: http1MaxPendingRequests: 50 http2MaxRequests: 100 # Outlier detection (eject unhealthy pods) outlierDetection: consecutive5xxErrors: 5 interval: 10s baseEjectionTime: 30s maxEjectionPercent: 50 # Subsets for canary subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2
Header-based routing
apiVersion: networking.istio.io/v1alpha3kind: VirtualServicemetadata: name: api-header-routingspec: hosts: - api-service http: # Route beta users to v2 - match: - headers: x-user-group: exact: beta route: - destination: host: api-service subset: v2 # Everyone else → v1 - route: - destination: host: api-service subset: v1
Fault Injection (Chaos Testing)
apiVersion: networking.istio.io/v1alpha3kind: VirtualServicemetadata: name: fault-injection-testspec: hosts: - api-service http: - fault: # Inject 500ms delay for 10% of requests delay: percentage: value: 10 fixedDelay: 500ms # Inject 503 error for 5% of requests abort: percentage: value: 5 httpStatus: 503 route: - destination: host: api-service
Security (mTLS + Authorization)
Enforce strict mTLS
# Namespace-wide strict mTLSapiVersion: security.istio.io/v1beta1kind: PeerAuthenticationmetadata: name: default namespace: productionspec: mtls: mode: STRICT # All traffic must be mTLS---# Mesh-wide strict mTLS (all namespaces)apiVersion: security.istio.io/v1beta1kind: PeerAuthenticationmetadata: name: default namespace: istio-system # ← applies mesh-widespec: mtls: mode: STRICT
AuthorizationPolicy — zero trust between services
# Only allow frontend to call api-serviceapiVersion: security.istio.io/v1beta1kind: AuthorizationPolicymetadata: name: api-service-policy namespace: productionspec: selector: matchLabels: app: api-service action: ALLOW rules: - from: - source: principals: - "cluster.local/ns/production/sa/frontend" # service account to: - operation: methods: ["GET", "POST"] paths: ["/api/*"]---# Deny all by default (then allow explicitly)apiVersion: security.istio.io/v1beta1kind: AuthorizationPolicymetadata: name: deny-all namespace: productionspec: {} # Empty spec = deny everything
JWT Authentication
apiVersion: security.istio.io/v1beta1kind: RequestAuthenticationmetadata: name: jwt-auth namespace: productionspec: selector: matchLabels: app: api-service jwtRules: - issuer: "https://login.microsoftonline.com/tenant-id/v2.0" jwksUri: "https://login.microsoftonline.com/tenant-id/discovery/v2.0/keys" audiences: - "api://your-app-id"
Observability
Install Kiali + Jaeger + Prometheus + Grafana
# Install addonskubectl apply -f istio-1.20.0/samples/addons/prometheus.yamlkubectl apply -f istio-1.20.0/samples/addons/grafana.yamlkubectl apply -f istio-1.20.0/samples/addons/jaeger.yamlkubectl apply -f istio-1.20.0/samples/addons/kiali.yaml# Open dashboardsistioctl dashboard kiali # Service graph + healthistioctl dashboard grafana # Metrics dashboardsistioctl dashboard jaeger # Distributed tracing
Key Metrics (auto-generated by Envoy)
istio_requests_total # Request countistio_request_duration_milliseconds # Latency histogramistio_request_bytes # Request sizeistio_response_bytes # Response sizeistio_tcp_connections_opened_total # TCP connections
Option 2 — Linkerd (Lightweight & Simple)
Install Linkerd
# Install CLIcurl --proto '=https' --tlsv1.2 -sSfL \ https://run.linkerd.io/install | shexport PATH=$PATH:$HOME/.linkerd2/bin# Pre-install checklinkerd check --pre# Install CRDs then control planelinkerd install --crds | kubectl apply -f -linkerd install | kubectl apply -f -# Verifylinkerd check# Install observability (Viz)linkerd viz install | kubectl apply -f -linkerd viz dashboard & # Open dashboard
Inject Linkerd Sidecar
# Namespace annotationapiVersion: v1kind: Namespacemetadata: name: production annotations: linkerd.io/inject: enabled # ← auto-inject---# Or annotate deployment directlyapiVersion: apps/v1kind: Deploymentmetadata: name: api annotations: linkerd.io/inject: enabled
Traffic Splitting (Canary)
apiVersion: split.smi-spec.io/v1alpha1kind: TrafficSplitmetadata: name: api-canary namespace: productionspec: service: api-service backends: - service: api-service-v1 weight: 90 - service: api-service-v2 weight: 10
Retries & Timeouts
apiVersion: policy.linkerd.io/v1beta1kind: HTTPRoutemetadata: name: api-retry namespace: productionspec: parentRefs: - name: api-service kind: Service rules: - backendRefs: - name: api-service port: 80 timeouts: request: 10s retry: limit: 3 timeout: 3s conditions: - gateway-error - http-response-500
Option 3 — Cilium Service Mesh (No Sidecar)
Traditional sidecar mesh: Cilium (eBPF):Pod → Sidecar → Network Pod → eBPF kernel hooks → Network(extra hop, extra resource) (in-kernel, no sidecar overhead)
# Install Cilium with service mesh featureshelm repo add cilium https://helm.cilium.io/helm install cilium cilium/cilium \ --namespace kube-system \ --set kubeProxyReplacement=true \ --set gatewayAPI.enabled=true \ --set envoy.enabled=true# Verifycilium statuscilium connectivity test
# mTLS via CiliumNetworkPolicyapiVersion: cilium.io/v2kind: CiliumNetworkPolicymetadata: name: api-mtls-policy namespace: productionspec: endpointSelector: matchLabels: app: api-service ingress: - fromEndpoints: - matchLabels: app: frontend toPorts: - ports: - port: "8080" protocol: TCP rules: http: - method: "GET" path: "/api/.*"
Istio vs Linkerd vs Cilium
| Istio | Linkerd | Cilium | |
|---|---|---|---|
| Sidecar | Envoy proxy | linkerd-proxy (Rust) | No sidecar (eBPF) |
| Complexity | High | Low | Medium |
| Resource overhead | High | Low | Very low |
| mTLS | ✅ | ✅ | ✅ |
| Traffic splitting | ✅ Full | ✅ Basic | ✅ |
| Circuit breaking | ✅ | ⚠️ Limited | ✅ |
| Fault injection | ✅ | ❌ | ❌ |
| Observability | ✅ Rich | ✅ Good | ✅ Good |
| Multi-cluster | ✅ | ✅ | ✅ |
| AKS native support | ✅ | ✅ | ✅ (BYOCNI) |
| Best for | Enterprise, full control | Simplicity, low overhead | Performance, no sidecar |
Multi-Cluster Service Mesh
Cluster A (East US) Cluster B (West US)┌─────────────────────┐ ┌─────────────────────┐│ istiod │◄────────▶│ istiod ││ ┌───────────────┐ │ shared │ ┌───────────────┐ ││ │ frontend pod │ │ control │ │ api-service │ ││ │ + sidecar │──┼──plane───┼─▶│ + sidecar │ ││ └───────────────┘ │ │ └───────────────┘ ││ East Gateway │◄─mTLS───▶│ West Gateway │└─────────────────────┘ └─────────────────────┘
# Istio multi-cluster setup (primary-remote model)# On primary clusteristioctl install --set profile=default \ --set values.pilot.env.EXTERNAL_ISTIOD=true# Create remote secret (allow primary to manage remote)istioctl create-remote-secret \ --name=remote-cluster \ --context=remote-cluster-context | \ kubectl apply -f - --context=primary-cluster-context
Observability Stack
Envoy Sidecars (metrics + traces) ↓Prometheus (scrape metrics) ↓ ↓ Grafana Jaeger / Zipkin(dashboards) (distributed tracing) ↓ Kiali (service graph + health) ↓ Alertmanager → PagerDuty / Slack
Golden Signal Dashboards (auto from mesh)
Latency → P50 / P95 / P99 per serviceTraffic → Requests per second per serviceErrors → 4xx / 5xx rate per serviceSaturation → Connection pool usage, CPU
Best Practices
| Practice | Why |
|---|---|
| Start with Linkerd if new to meshes | Lower complexity, easier to debug |
| Enable mTLS mesh-wide from day 1 | Harder to enforce later |
Use PERMISSIVE mode first, then STRICT | Gradual rollout without breaking traffic |
| Set resource requests on sidecars | Prevent sidecar starvation |
Use AuthorizationPolicy deny-all baseline | Zero-trust foundation |
| Monitor sidecar resource usage | Envoy adds ~50-100MB RAM per pod |
| Use Kiali for topology visibility | Catch unexpected service dependencies |
| Test with fault injection before prod | Validate resilience patterns work |
| Avoid mesh for batch/job workloads | Sidecar overhead not worth it |
| Use Cilium on performance-critical clusters | eBPF = kernel-level, no overhead |
Troubleshooting
# Check sidecar injectionkubectl get pod api-pod -n production -o jsonpath='{.spec.containers[*].name}'# Should show: api envoy (two containers)# Check mTLS statusistioctl x describe pod api-pod-xxx -n production# Check Envoy configistioctl proxy-config cluster api-pod-xxx.productionistioctl proxy-config listener api-pod-xxx.productionistioctl proxy-config route api-pod-xxx.production# Check authorization policiesistioctl x authz check api-pod-xxx.production# View Envoy access logskubectl logs api-pod-xxx -n production -c istio-proxy# Debug traffic issuesistioctl analyze -n production # ← catch misconfigs# Linkerd debugginglinkerd viz stat deployments -n productionlinkerd viz routes deployment/api -n productionlinkerd viz edges deployment -n production
A service mesh is the foundation of zero-trust networking inside Kubernetes — mTLS by default, explicit authorization policies, and full observability without touching application code. Start with Linkerd for simplicity or Istio if you need the full feature set.