Architecture
Big picture
Aeraki has no data plane. It is a control plane that sits on top of Istio and Envoy and plays two roles. The first role generates Istio EnvoyFilter resources: it watches Istiod over MCP over xDS, reacts to changes in ServiceEntry, VirtualService, Gateway, and Aeraki's own custom resource definitions (CRDs), produces the matching Envoy configuration, and writes it back to the Istio API server. The second role is a Route Discovery Service (RDS) server for the MetaProtocol Proxy data plane: it pushes routes computed from MetaRouter resources over gRPC. Envoy's own RDS is HTTP-only, so this provides dynamic route delivery for arbitrary L7 protocols (README.md:71).
Components
The components are wired together in NewServer (internal/bootstrap/server.go:103).
istio.Controller
Watches Istiod over MCP over xDS. It is created at internal/bootstrap/server.go:113 via istio.NewController. Inside, it holds an Aggregated Discovery Service Client (xdsMCP *adsc.ADSC, internal/controller/istio/controller.go:69) and connects to Istiod with adsc.New (internal/controller/istio/controller.go:131). It exposes a config Store that the other components read.
envoyfilter.Controller
The component that turns config changes into EnvoyFilter resources. It is created at internal/bootstrap/server.go:120 via envoyfilter.NewController. The server registers a handler that forwards every config event to it (internal/bootstrap/server.go:122-124):
configController.RegisterEventHandler(func(_, _ *istioconfig.Config, event model.Event) {
envoyFilterController.ConfigUpdated(event)
})xds.CacheMgr and xds.Server
The MetaProtocol RDS path. xds.NewCacheMgr (internal/bootstrap/server.go:126) builds the route cache; xds.NewServer (internal/bootstrap/server.go:132) serves it over gRPC. The cache manager holds a go-control-plane snapshot cache and the Istio config store (internal/xds/cache_mgr.go:53).
Protocol generators
A map from protocol to a Generator implementation, one per non-HTTP protocol. initGenerators registers Thrift, Kafka, Zookeeper, and MetaProtocol (cmd/aeraki/main.go:145-152). Dubbo and Redis are added later because they need the controller-manager client (internal/bootstrap/server.go:144-145):
args.Protocols[protocol.Dubbo] = dubbo.NewGenerator(scalableCtrlMgr.GetConfig())
args.Protocols[protocol.Redis] = redis.New(cfg, configController.Store)How a request flows
Trace one config change from Istiod to a written EnvoyFilter.
- Istiod sends a config change over MCP.
istio.Controllerinvokes the registered handler, which callsenvoyFilterController.ConfigUpdated(event)(internal/bootstrap/server.go:122-124). ConfigUpdatedonly sends the event to an internal channel (internal/envoyfilter/controller.go:513-515).mainLoopselects on that channel and debounces withdebounce.New(debounceAfter, debounceMax, callback, stop)before firing (internal/envoyfilter/controller.go:116). The debounce window is 1 second minimum, 10 seconds maximum (internal/envoyfilter/controller.go:47-51). The callback callspushEnvoyFilters2APIServerand, on failure, re-queues up to 3 times (internal/envoyfilter/controller.go:100-115).pushEnvoyFilters2APIServer(internal/envoyfilter/controller.go:128) generates the desired filters, lists the existing ones by labelmanager=aeraki(internal/envoyfilter/controller.go:135-137), then reconciles: delete removed (internal/envoyfilter/controller.go:140-149), update changed usingproto.Equalto detect drift (internal/envoyfilter/controller.go:156), create new (internal/envoyfilter/controller.go:171-178).generateEnvoyFilters(internal/envoyfilter/controller.go:200) lists allServiceEntryresources (internal/envoyfilter/controller.go:202) and, for each port, determines the L7 protocol from the port name withprotocol.GetLayer7ProtocolFromPortName(port.Name)(internal/envoyfilter/controller.go:222).- If a generator exists for that protocol (
internal/envoyfilter/controller.go:223), it builds anEnvoyFilterContext(internal/envoyfilter/controller.go:226) and callsgenerator.Generate(ctx)(internal/envoyfilter/controller.go:233).
Key design decisions
The unique virtual IP (VIP) assignment runs in a single controller because the VIP is used as a match condition when generating the EnvoyFilter. Istio allocates a VIP for a ServiceEntry, but in a sidecar scope, so the IP is not consistent across the mesh. Aeraki needs it consistent, so a singleton controller assigns it (internal/bootstrap/server.go:296-302).
Scaling is asymmetric. Writing EnvoyFilter resources runs only on the leader: in master mode the server runs the controller under leader election (internal/bootstrap/server.go:341-351). The RDS server, route cache, and config watcher run on every replica regardless of master or slave role (internal/bootstrap/server.go:355-368). Writes are funneled to one instance; read-side route delivery scales horizontally.
Extension points
The single extension interface is Generator (internal/envoyfilter/generator.go:22), with one method Generate(context *model.EnvoyFilterContext) ([]*model.EnvoyFilterWrapper, error) (internal/envoyfilter/generator.go:23). New protocols register a Generator in the protocol map. Beyond that, new MetaProtocol protocols need only a codec in the meta-protocol-proxy data plane plus a MetaRouter CRD, not a new control-plane component.