In Depth · Service Mesh Architecture

Service Mesh Architecture

The ICA and CCA blueprints both test a mesh's surface — the CRDs you write, the CLI verbs you run, the policy you configure. Neither exam asks you to explain why the thing underneath looks the way it does, or why three CNCF projects that all claim to solve "service mesh" ship such different-looking answers. This page is that missing layer. It compares three real architectures — Istio's classic Envoy sidecar and Linkerd's deliberately minimal micro-proxy on one side, and two genuinely different sidecarless designs, Istio's own Ambient Mode and Cilium's native eBPF mesh, on the other — against the one structural idea every mesh shares underneath: a control plane that decides, and a data plane that acts. It closes with the question none of the exams ask at all: whether a mesh is worth the operational cost you are about to take on.

☺ Explain it like I'm 10

Picture an apartment building where every single tenant hires their own private security guard, who stands right outside that one door, checks every visitor's ID, and keeps a little diary of who came and went. That works, but now the building employs one guard per apartment — expensive, and every new tenant means training a new guard. Now picture a different building: instead of one guard per door, a smaller team works the lobby and hallways, checking badges as people walk past, watching everyone on every floor without needing to stand outside any one specific door. Fewer guards, less cost per tenant — but if a hallway guard has a bad day, it affects everyone on that floor, not just one apartment. Both buildings end up secure. They just spend their guard budget in completely different places.

🦉Your host for this topic: Professor Owl — he draws the reference architecture before anyone argues about a single field in a VirtualService, and no page on this shelf needs that more than one comparing three meshes at once.

Every mesh is two systems wearing one name

☺ Like you're 10: One part of the mesh decides the rules. A completely different part actually touches every message. They're not the same program, even when people talk about "the mesh" as if it were one thing.

"Service mesh" describes a shape, not a single piece of software, and every real implementation — Istio, Linkerd, Cilium's own mesh — splits into the same two systems. The control plane (istiod for Istio, cilium-operator plus the per-node cilium-agent for Cilium, linkerd-destination and linkerd-identity for Linkerd) watches the Kubernetes API, computes what every workload's routing rules, TLS identity and authorization policy should be, and continuously pushes that computed state down. The data plane is whatever actually sits on the path of a real packet or request and enforces what it was told — an Envoy sidecar, a per-node ztunnel, an eBPF program attached to a kernel hook, a linkerd2-proxy. The control plane never touches a single byte of application traffic; the data plane never decides policy on its own. That separation is what makes "the control plane is down" and "the mesh is down" two completely different sentences.

Control Plane istiod · cilium-operator · linkerd-destination computes desired state config pushed down — the slow path Pod A App Proxy Pod B App Proxy Pod C App Proxy real traffic — the fast path — never touches the control plane
◆ Key idea

This split is why a mesh's control plane can go down without an outage. Every proxy or eBPF program already holds its last-known configuration and keeps enforcing it — new routes and new identities stop arriving, but existing traffic keeps flowing on stale-but-valid rules. A mesh fails static, not instantly open or instantly closed. Confirming that behaviour on your own cluster, deliberately, is worth more than reading about it here.

The sidecar model: one proxy per pod

☺ Like you're 10: One guard per apartment door. Simple to reason about, and every new apartment needs its own guard, no exceptions.

Istio's original architecture, and the only architecture Linkerd has ever shipped, injects a proxy container into every pod that joins the mesh. A Kubernetes mutating admission webhook intercepts pod creation and adds the sidecar automatically — which means injection only happens at creation time. Label a namespace for injection and nothing happens to the pods already running in it; they need a rollout restart before they're actually in the mesh, the single most common "why isn't this pod showing a sidecar" support ticket on any mesh team. Once injected, every request the app container sends leaves through its own local proxy first, and every request it receives arrives through that same proxy — which is what lets the proxy enforce mTLS, retries, timeouts and authorization without the application ever calling a library or knowing the mesh exists.

# A pod with an injected sidecar shows two containers, not one
$ kubectl get pods -n prod
NAME                        READY   STATUS    RESTARTS   AGE
checkout-7d9f6c8b4d-x2k9p   2/2     Running   0          3h

# Istio's proxy defaults are real resource consumers — one per pod, not per node
$ kubectl get pod checkout-7d9f6c8b4d-x2k9p -n prod -o jsonpath='{.spec.containers[1].resources}'
{"limits":{"cpu":"2","memory":"1Gi"},"requests":{"cpu":"100m","memory":"128Mi"}}

# Existing pods are never retrofitted — injection only fires at pod creation
$ kubectl label namespace prod istio-injection=enabled
$ kubectl rollout restart deployment -n prod   # required, every time

Istio and Linkerd made opposite bets on what that sidecar should be. Istio's sidecar is Envoy — a general-purpose, extremely feature-rich proxy originally built at Lyft, driven by the xDS configuration protocol, capable of nearly anything you can express in a VirtualService or DestinationRule. That breadth is also its resource footprint: Envoy is a substantial binary with a correspondingly real CPU and memory budget per pod, doubled across every pod on the cluster. Linkerd's linkerd2-proxy is the opposite bet on purpose — written in Rust specifically to be small, and deliberately narrow in what it does: mutual TLS, retries and timeouts, golden-signal metrics, and basic traffic splitting, with none of Istio's expansive header-manipulation and fault-injection surface. Linkerd's own positioning is explicit about this trade: fewer features, in exchange for a proxy light enough that most teams stop thinking about its overhead at all. Neither choice is wrong — they're optimizing for different things, and "which sidecar" is really "how much mesh do you actually need."

Sidecarless, take one: Istio's own Ambient Mode

☺ Like you're 10: Instead of a guard per door, one small team covers a whole hallway — and a second, fancier team only shows up on the floors that actually asked for extra scrutiny.

Istio didn't wait for a competitor to solve the sidecar's overhead problem — it shipped its own sidecarless mode, Ambient, splitting the data plane into two layers instead of one. ztunnel runs once per node, handles every pod on that node, and does exactly the part of the job that scales cleanly without per-pod state: mTLS, workload identity, and basic L4 authorization. No injection webhook, no sidecar container, no rollout restart to join — a namespace enrols with a label (istio.io/dataplane-mode: ambient) and every existing pod is covered immediately. Layer two is the waypoint proxy, a full Envoy instance deployed only for the namespaces or service accounts that actually need Layer-7 features — header-based routing, path-based authorization, fault injection. Most workloads never get one; they run on ztunnel alone and never pay Envoy's cost at all. It's the same idea as an office giving every floor basic security, and only wiring a fancier badge reader onto the doors that actually need it.

◆ Key idea

Ambient is not a new mesh with new CRDs — it's the same VirtualService, DestinationRule, PeerAuthentication and AuthorizationPolicy resources the ICA already tests, running on a different data plane underneath. The published ICA curriculum's "Installing Istio in Sidecar or Ambient Mode" competency exists precisely because the two are meant to be interchangeable choices on the same API, not two different products to separately learn.

⚠ Still the newer of the two

Ambient reached general availability well after Istio's sidecar model had years of production hardening behind it. The architecture is sound and increasingly deployed, but the operational folklore — the "here's the weird failure mode nobody warns you about" knowledge that accumulates around any mature system — is thinner for Ambient than for sidecar Istio. Weigh that maturity gap honestly against the resource savings before betting a production migration on it; it narrows every release, but it isn't zero yet.

Sidecarless, take two: Cilium's native mesh

☺ Like you're 10: This building was never guarded by people at doors in the first place — the hallways themselves were built to watch everyone walking through them. Adding "mesh" features barely costs anything extra, because most of the watching was already happening.

Cilium's sidecarless story is a different kind of different. Ambient still adds a dedicated per-node proxy process to do its L4 job. Cilium doesn't need to, because it's already your CNI — every packet on the cluster is already passing through eBPF programs attached to kernel hooks before a single mesh feature gets switched on. Identity-based authorization, mutual encryption via WireGuard or IPsec, and load balancing are consequences of capabilities Cilium's datapath already has for ordinary networking duty, not new machinery bolted on for "mesh." Only Layer-7 rules — an HTTP method, a path, a header match — need something to actually parse the protocol, which eBPF alone can't do; for those, Cilium spins up one Envoy instance per node, invoked only for the specific flows a policy's rules.http block asks it to inspect. The rest of Cilium's mesh behaviour never leaves the kernel at all. eBPF & the Cilium Datapath is the page that goes underneath this one and explains exactly how that kernel-level enforcement actually works.

# Enabling Cilium's L7 visibility/enforcement doesn't add a sidecar —
# it tells the existing per-node Envoy to start parsing THIS traffic.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: checkout-l7
  namespace: prod
spec:
  endpointSelector:
    matchLabels: { app: checkout }
  ingress:
    - fromEndpoints:
        - matchLabels: { app: frontend }
      toPorts:
        - ports: [{ port: "8080", protocol: TCP }]
          rules:
            http:                       # <-- ONLY this rule triggers node-Envoy parsing
              - method: "GET"
                path: "/v1/cart.*"

Practically, this makes Cilium the cheapest of the three sidecarless stories to run at L4 — there is, quite literally, no additional proxy process for L4-only traffic — at the cost of being the most tightly coupled to one specific CNI. You can't bolt Cilium's mesh onto a cluster running Calico or the AWS VPC CNI the way you could add Istio Ambient on top of most CNIs; the mesh capability is inseparable from the choice of CNI itself. The CCA blueprint's own "Service Mesh" domain — 16% of that exam — is built entirely around this sidecarless-by-construction model, alongside Cilium's Ingress and Gateway API support.

Where the operational cost actually lands

☺ Like you're 10: Every option here is a trade, not a free upgrade. Fewer guards means each guard covers more ground — great for cost, worse for you the one time a guard has a bad day.

Laid side by side, the four options this page has covered trade the same handful of costs against each other in different proportions: how many proxy processes exist, how much blast radius one failing proxy carries, how many extra hops a request takes, and how much of the mesh's own upgrade you have to coordinate yourself.

Sidecar Istio classic · Linkerd Node App Proxy App Proxy proxy per pod every pod pays the tax Istio Ambient ztunnel + waypoint Node App (Pod A) App (Pod B) ztunnel — per node, L4 only waypoint — opt-in, L7, per ns proxy per node L7 only where asked for Cilium native mesh eBPF + on-demand Envoy Node App (Pod A) App (Pod B) ⚡ eBPF — in-kernel, L3/L4 node Envoy — on demand, L7 no proxy for L3/L4 L7 proxy spun up per node
DimensionSidecar (Istio classic, Linkerd)Istio AmbientCilium native mesh
Proxy processesOne per podOne ztunnel per node, plus one waypoint per opted-in namespaceNone for L3/L4 (in-kernel); one Envoy per node, only when L7 rules are active
Joining the meshAdmission webhook injects at pod creation — existing pods need a restartNamespace label — covers pods already running immediatelyAlready true the moment the pod exists; Cilium is the CNI
Blast radius of one proxy crashOne podEvery pod on that node (L4); one namespace's L7 traffic (waypoint)L4 keeps working regardless (kernel); one node's L7 traffic (node Envoy)
Added hops for a plain L4 callTwo proxies per hop (sender + receiver sidecar)One ztunnel hop each side, no waypoint if not neededEffectively zero — enforced inline in the existing packet path
Control-plane upgrade unitWhole mesh, via revisions/canaryWhole mesh, via revisions/canary (same mechanism as sidecar)Tied to the CNI's own upgrade cadence
🦊 Foxy's-eye view

"I used to think 'sidecarless' meant 'no operational cost' — like the mesh just got free somehow. It didn't. It moved the cost from 'per pod, and I can see it in every pod's resource request' to 'per node, invisible until one node's ztunnel falls over and suddenly every pod on it is affected at once.' I don't think either shape is objectively better any more. I think 'where do I want my blast radius to live' is a real design question, and I used to skip straight past it."

Deciding whether a mesh earns its keep

☺ Like you're 10: Not every building needs a security team at all. Some just need a good lock on the front door — adding guards you don't need just means paying guards to stand around.

None of the exams on this shelf ask whether you should run a mesh at all — they assume you already decided yes. In real platform work that decision comes first, and getting it wrong in either direction is expensive: adopting a mesh nobody needed adds a whole new distributed system's worth of upgrades, failure modes and 2 a.m. pages for capabilities a simpler setup already covered; skipping one that was actually needed leaves every team reinventing mTLS, retries and authorization inside their own application code, inconsistently, forever.

A mesh tends to earn its cost when several of these are true at once: you have enough services and enough teams that consistent mTLS and authorization can no longer be enforced by convention or code review; you need traffic-shifting or canary releases without redeploying application code every time a rollout strategy changes; a compliance or zero-trust requirement needs provable, enforced encryption-in-transit and identity-aware access, not a policy document; or you want golden-signal observability — latency, error rate, request volume — for every service without instrumenting each one by hand. It tends to not earn its cost when the service count is small enough that a handful of engineers already know every dependency by heart, when latency budgets are tight enough that even Cilium's near-zero L4 tax matters, or — the one people skip past — when nobody on the team has the bandwidth to actually own a control-plane upgrade cadence. A mesh that nobody upgrades for a year is not a safety net; it's an unpatched, unmonitored trust boundary sitting quietly in the middle of your traffic.

◆ The one-sentence heuristic

If the problem you're solving is "our applications shouldn't have to know about encryption, retries, or who's allowed to call them" — across enough services that hand-rolling it everywhere has already become the bigger cost — a mesh is solving the right problem. If the problem is "we want observability" or "we want one team's traffic encrypted," there is very likely a cheaper, narrower tool for that specific job.

🦫 Benny's workshop · 20 min

See the sidecar tax with your own eyes before you argue about it in the abstract. On a throwaway cluster: deploy one workload with Istio sidecar injection off, note its memory with kubectl top pod; enable injection, restart it, and check again — that gap is the number in the resource-cost argument, made concrete for your own workload's actual traffic shape rather than a vendor's benchmark. If you have Cilium available, repeat the same request against a plain L4 policy versus one with an http rule attached, and watch when — and only when — the per-node Envoy actually gets invoked.

🎬 At Mission Control
🦫

Benny the Beaver: So which one do I actually install? I've got three names now and one afternoon.

🦉

Professor Owl: Wrong first question, Benny. Before "which mesh," it's "do we need one at all." Answer that first — the architecture choice only matters once the answer is yes.

👺

Gizmo: Easy — always yes! More proxies, more resume lines, more conference talks. Install all three at once, see what sticks. 🤑

🐢

Timmy: That's not a decision, Gizmo, that's three new failure domains nobody agreed to own. Benny, count your services and your teams first.

🦊

Foxy: Say the answer's yes. We're already running Cilium as the CNI — does that make the choice easy?

🦉

Professor Owl: It makes Cilium's own mesh the cheapest option to trial, since half of it is already running. It doesn't mean skip evaluating whether you actually need Istio's deeper L7 feature set — that's a separate question from "which one's already installed."

🐘

Ellie the Elephant: And whichever you pick, I'm putting golden-signal dashboards on it from day one. A mesh you can't see into is just a second network you're guessing about instead of one.

🐢 Timmy's checkpoint

1. In your own words, what is the difference between a mesh's control plane and its data plane, and why does that split explain why a mesh "fails static" rather than failing instantly open or closed? 2. Why must a pod be restarted after a namespace is labeled for Istio sidecar injection? 3. Name the two layers of Istio's Ambient data plane, and say which one is optional and per-namespace. 4. Why does Cilium's own mesh need no dedicated proxy at all for L3/L4 traffic, when Istio Ambient still needs ztunnel? 5. Give one operational cost that gets smaller when you move from a sidecar model to a sidecarless one, and one that gets larger. 6. Name two conditions that suggest a mesh is worth adopting, and one that suggests skipping it for now.

Check your answers
  1. The control plane (e.g. istiod, cilium-operator) computes desired configuration — routing, identity, policy — and pushes it to the data plane, but never touches actual traffic itself. The data plane (a sidecar, ztunnel, an eBPF program) is what enforces that configuration on every real packet or request. Because the data plane already holds its last-known configuration independently, a control-plane outage stops new config from arriving but doesn't stop existing proxies from continuing to enforce whatever they last received — that's "failing static."
  2. Injection happens through a mutating admission webhook that only fires when a pod is created. Labeling a namespace changes what happens to future pod creations; it does nothing to pods already running, so they must be recreated (via a rollout restart) to actually pick up the sidecar.
  3. ztunnel (per node, mandatory once a namespace is in ambient mode, handles mTLS/identity/basic L4) and the waypoint proxy (per namespace or service account, optional, only deployed where Layer-7 features are actually needed).
  4. Because Cilium is already the cluster's CNI, every packet already passes through its eBPF programs for ordinary networking duty before any mesh feature is enabled — identity-based authorization and encryption are extensions of capability the datapath already has. Istio Ambient's ztunnel has to be added as new, dedicated infrastructure on top of whatever CNI is already running, because Istio itself has no kernel-level datapath of its own.
  5. Smaller: per-pod resource overhead, since a shared per-node (or in-kernel) component replaces one proxy per pod. Larger: blast radius of one proxy or agent failing, since a per-node or in-kernel failure can affect every pod on that node rather than just one pod.
  6. Worth adopting: any two of — enough services/teams that ad hoc mTLS and authorization no longer scale by convention; a need for traffic-shifting/canary without redeploying app code; a compliance/zero-trust requirement for provable encryption and identity-aware access; wanting uniform golden-signal observability without instrumenting every service. Suggests skipping: a small service count a team already knows by heart, a latency budget too tight for any added hop, or no one with the bandwidth to actually own the mesh's ongoing upgrades.