Control plane and data plane — config over gRPC

The data plane is the fleet of sidecars handling real requests; the control plane is a separate process (Istiod) that watches the cluster and streams listener/route/cluster/endpoint config to every sidecar over a long-lived xDS gRPC stream — so config changes propagate without restarts and the control plane is never on the request path.

Previously

Certs came from a central CA, route tables came from a central operator — there has to be one process responsible for fanning all of that out to every sidecar live, and that role splits the mesh into two planes.

Scene 11

Control plane and data plane — config over gRPC

  1. Watch
  2. Try it
  3. Predict
  4. Capture
CONTROL PLANEControl PlaneIstiod · xDS server● healthyDATA PLANE · 8 sidecarsxDS gRPC streamsc-1envoyroute90/10sc-2envoyroute90/10sc-3envoyroute90/10sc-4envoyroute90/10sc-5envoyroute90/10sc-6envoyroute90/10sc-7envoyroute90/10sc-8envoyroute90/10request traffic · never pausesOPERATOR · YAMLapiVersion: networking.isti…kind: VirtualServicemetadata: name: paymentsspec: http: - route: - destination: host: v1 weight=90idlesteady state — xDS streams are open, no config edits, requests flow through the data plane
↑ control plane — config issuer, NOT on the request path
long-lived streaming gRPC ↓
↓ data plane — sidecars handling traffic
data-plane traffic never pauses
What to watch for

One control plane up top streams config to every sidecar over a long-lived xDS gRPC stream. The sidecars are the data plane — they hold cached config and move bytes for every request. Watch the request traffic below the sidecar row.

Implementation

Highlighted lines are the ones running in the diagram right now.

Sidecar.subscribeXds
long-lived gRPC stream — server pushes, sidecar acks
stream = grpc.stream(
CONTROL_PLANE,
"/envoy.service.discovery.v3"
".AggregatedDiscoveryService/StreamAggregatedResources",
)
stream.send(DiscoveryRequest(
resource_names=["mesh-listener", "checkout-routes", ...],
))
loop:
resp = stream.recv() # server pushes on change
apply(resp.resources) # update cached LDS/RDS/CDS/EDS
stream.send(ack(resp.nonce)) # ack the version_info
ControlPlane.onYamlChange
operator edits a VirtualService — Istiod fans out new routes
def on_yaml_change(vs: VirtualService):
new_routes = compile_routes(vs)
version = bump_version()
for sidecar in registry.affected_sidecars(vs):
sidecar.xds_stream.send(DiscoveryResponse(
resources=[new_routes],
type_url=RDS_TYPE_URL,
version_info=version,
nonce=fresh_nonce(),
))
DataPlane.whenControlPlaneDies
why traffic survives the loss of Istiod
# Sidecars KEEP serving — they hold the last applied
# LDS/RDS/CDS/EDS in memory and consult THAT cache
# on every request, never the control plane.
for req in inbound_traffic:
route = cached_rds.match(req) # in-memory, authoritative
forward(req, route.cluster)
# New operator edits stall — no Istiod to compile them.
# Cert rotation continues from the CA pool until SVIDs
# expire (~24h Istio default).
# Therefore: control plane is on the CONFIG path,
# NOT the REQUEST path.

Where this sits in Build a Service Mesh (Envoy / Istio style)

Scene 10 of 13. Sidecars (data plane) handle traffic; the control plane (Istiod) streams listener/route/cluster/cert config via xDS. Kill the control plane and traffic keeps flowing.

Up next. Once every request crosses two sidecars whose config is pushed by one control plane, those two sidecars are the perfect place to emit a record of the request — and a way to stitch many such records into one user story.

All 13 scenes in Build a Service Mesh (Envoy / Istio style) · Every curriculum

Built with Arqly
Every scene in Build a Service Mesh (Envoy / Istio style) builds on the one before it.All 13 Build a Service Mesh (Envoy / Istio style) scenes