- The
deployment_changesMySQL table (pkg/db/schema.sql) — a change notification log. - The
WatchDeploymentChangesRPC (svc/ctrl/services/cluster/rpc_watch_deployment_changes.go) — a streaming endpoint that delivers changes to agents. - The krane watcher (
svc/krane/internal/watcher) — consumes the stream and dispatches events to controllers.
Why this exists
The control plane and krane agents are separate processes in separate clusters. When a deploy workflow writes new state to MySQL, agents need to discover that change and apply it to Kubernetes. The deployment sync system bridges this gap with a pull-based streaming model similar to Kubernetes LIST+WATCH. Previously, a Restate virtual object generated monotonic version numbers that were stamped onto state table rows. This coupled version generation to Restate and required a cross-system round-trip on every write. Thedeployment_changes table replaces this with a pure MySQL solution: writes and notifications happen in a single transaction, and the system can be tested and operated without Restate.
How it works
The deployment_changes table
Every mutation to deployment state (topology inserts, sentinel creation, cilium policy updates, desired status changes) writes a row to deployment_changes in the same MySQL transaction as the state change itself. The row contains no state — just a pointer:
| Column | Purpose |
|---|---|
pk | Auto-increment primary key. Acts as the streaming cursor. |
resource_type | Enum: deployment_topology, sentinel, or cilium_network_policy. |
resource_id | The ID of the changed resource in its state table. |
region_id | The region this change applies to. |
created_at | Timestamp for TTL-based cleanup. |
(region_id, resource_type, pk) makes polling efficient.
The unified stream
Krane agents open a singleWatchDeploymentChanges stream per region. The stream operates in two modes:
Full sync (version_last_seen = 0). On first connection or periodic resync, the server:
- Reads
MAX(pk)fromdeployment_changesto establish the cursor. - Paginates through all rows in
deployment_topology,sentinels, andcilium_network_policiesfor the region. - Streams every resource as a
DeploymentChangeEventwith the version set to the max cursor.
deployment_changes entries are.
Incremental (version_last_seen > 0). The server polls deployment_changes for rows with pk > version_last_seen, does a point lookup for each row to load current state from the relevant table, wraps it in a DeploymentChangeEvent, and streams it. Polling happens every second when idle.
Event dispatch in krane
The krane watcher receivesDeploymentChangeEvent messages, each containing a oneof with a deployment, sentinel, or cilium policy state. It dispatches to the appropriate controller:
DeploymentState→deployment.Controller.ApplyDeploymentorDeleteDeploymentSentinelState→sentinel.Controller.ApplySentinelorDeleteSentinelCiliumNetworkPolicyState→cilium.Controller.ApplyCiliumNetworkPolicyorDeleteCiliumNetworkPolicy
Periodic full resync
The watcher resets its cursor to 0 every 5 minutes, triggering a full sync. This acts as a consistency safety net: if a change was missed (e.g., adeployment_changes row was cleaned up before the agent processed it), the periodic full sync will reconcile the drift.
Writing changes
Every code path that mutates deployment state must insert adeployment_changes row in the same transaction. The current write sites are:
deploy_handler.go—createTopologies(bulk insert + deployment_changes per region) andensureSentinels(sentinel insert + deployment_changes).cilium_policy.go—ensureCiliumNetworkPolicy(insert or update + deployment_changes).deployment_state.go—ChangeDesiredState(topology status update + deployment_changes per region).
deployment_changes row or the change will be invisible to krane until the next periodic full sync.
Cleanup
Olddeployment_changes rows can be cleaned up with TTL-based deletion since full syncs read directly from state tables. The cleanup query deletes rows older than a threshold in batches of 10,000 to avoid long-running transactions.

