Change, Redeploy, and Roll Back Safely
Make product change routine by promoting immutable revisions and recovering through the owner of each desired state.
Run it in the public monorepo
This course is built around the public Zheta Kubernetes Lab monorepo. The excerpt below is runnable source, not pseudocode.
Source: services/control_plane/domain.py
def retire(self, project: Project, actor: str) -> None:
self._owner(project, actor)
project.status = "retired"
with self.repository.transaction():
self.repository.save(project)
self._audit(project, actor, "project.retired")
self.runtime.delete(project.organization_id, project.project_id)
def delete(self, project: Project, actor: str) -> None:
self._owner(project, actor)
if project.status != "retired":
raise ValueError("retire project before deletion")
Code to reality
- Declared intent
- Enforce the product lifecycle so published runtime state is retired before durable deletion.
- Interpreter
- The injected Forge model applies ownership and state-machine rules independently of HTTP or storage technology.
- Software effect
- Retirement removes the hosted runtime and records status before deletion can tombstone control-plane state.
- Hardware effect
- Runtime containers and storage can be released in order while retained evidence remains physically durable.
- Observable evidence
- A denied early delete, successful retirement, absent runtime, tombstoned project, and retained audit event prove order.
Start with the people and the result they need
The source tables below remain the detailed contract. Begin with these customer paths:
- D13-UC-01
- Person: Application maintainer
- Job: Deploy an approved change to one generated application
- Observable result: New Research Brief, Service Desk, or Field Inspection revision serves bounded traffic and meets its acceptance probes
- D13-UC-02
- Person: Incident commander
- Job: Restore service after a defective revision
- Observable result: Route returns to a known-good compatible artifact and required data repair is explicit
Turn each customer job into a testable story
Now turn each customer job into a story with a result that an engineer can check:
- D13-US-01
- Story: As an application maintainer, I want a new immutable revision exposed to bounded traffic, so that failures are detected before all users are affected
- Observable acceptance: Baseline, candidate digest, traffic percentage, compatibility result, metrics, environment, timestamps, run, and traces are recorded
- D13-US-02
- Story: As an incident commander, I want recovery to choose rollback or forward repair explicitly, so that restoring code does not hide unsafe data state
- Observable acceptance: Decision record names route, artifact, configuration, schema, data side effects, owner, observed recovery, and unaffected control
Add real state and observable proof
Finally trace each story through the system that owns its state and the evidence that proves the outcome:
- D13-FLOW-01
- Trigger: Maintainer selects Deploy approved change
- Responsible systems: Release Orchestrator, compatibility checker, GitOps/runtime deployment controller, Route Controller, telemetry evaluator
- Authoritative state: Release Store for intent; Git for workload declaration; runtime API for live deployment
- Owned record: DeploymentRevision
- Observable evidence: Actor, application, digests, policy, compatibility result, traffic split, expected and observed signals, environment, timestamp, run, and trace IDs
- Failure signal: Candidate not ready, controller drift, threshold breach, or schema incompatibility
- D13-FLOW-02
- Trigger: Canary breaches its error threshold
- Responsible systems: Incident workflow, Route Controller, GitOps/runtime controller, Migration Service, generated-app data owner
- Authoritative state: Incident Store for decision; generated-app database for domain state
- Owned record: MigrationLedger
- Observable evidence: Rollback decision, restored route digest, schema/data status, affected denial or recovery trace, unaffected positive trace, environment, time, and…
- Failure signal: Route restored but errors persist, incompatible down-migration, repair incomplete, or unaffected control fails
The enterprise problem and today’s slice
Enterprise problem: Once customers depend on a Zheta Forge application, a prompt, connector, schema, or code change can break requests or corrupt data, and “redeploy the old commit” may not reverse database effects. Whole-course context: The incoming evidence is a local release with a known content-addressed artifact; today selects a recorded release for redeployment. Today’s slice: We implement owner-only artifact rollback in the local runtime and distinguish it from unimplemented configuration rollback, canary routing, schema compatibility, and data repair. End-of-day evidence: A known release restores its exact stored artifact and publishes release.rolled_back; an unknown release is denied and the project remains readable. Still unsolved: Canary and GitOps recovery, schema/data repair, long-term observability, support operations, and retirement remain deferred.
Customer use cases
A redeployment is unsafe when the platform cannot say which changes are reversible and which require repair. These use cases cover controlled change and recovery from a bad release.
| Use case ID | Actor | Customer job | Success outcome | Denial or recovery evidence |
|---|---|---|---|---|
| D13-UC-01 | Application maintainer | Deploy an approved change to one generated application | New Research Brief, Service Desk, or Field Inspection revision serves bounded traffic and meets its acceptance probes | Promotion stops when the canary—a small traffic slice—breaches error, latency, policy, or compatibility thresholds |
| D13-UC-02 | Incident commander | Restore service after a defective revision | Route returns to a known-good compatible artifact and required data repair is explicit | Rollback refusal names incompatible schema or irreversible side effect; unaffected tenant control remains healthy |
Actor-centred user stories
“Roll back” is ambiguous unless the customer can see whether bytes, configuration, and data all returned to a safe state. These stories require evidence for each dimension.
| Story ID | Use case IDs | User story | Observable acceptance conditions |
|---|---|---|---|
| D13-US-01 | D13-UC-01 | As an application maintainer, I want a new immutable revision exposed to bounded traffic, so that failures are detected before all users are affected | Baseline, candidate digest, traffic percentage, compatibility result, metrics, environment, timestamps, run, and traces are recorded |
| D13-US-02 | D13-UC-02 | As an incident commander, I want recovery to choose rollback or forward repair explicitly, so that restoring code does not hide unsafe data state | Decision record names route, artifact, configuration, schema, data side effects, owner, observed recovery, and unaffected control |
End-to-end product flows
Replacing every pod at once converts one bad revision into a full outage, so deployment must expose a bounded comparison before promotion. Recovery begins from customer impact and follows the owner of the divergent state.
| Flow ID | Use case IDs | Path | Trigger | Numbered steps | Terminal evidence |
|---|---|---|---|---|---|
| D13-FLOW-01 | D13-UC-01 | Happy | Maintainer selects Deploy approved change | 1. Resolve prior and candidate digests. 2. Verify backward-compatible schema and configuration. 3. Deploy candidate beside prior revision. 4. Route bounded traffic. 5. Compare customer and system signals. 6. Promote only within thresholds. | Actor, application, digests, policy, compatibility result, traffic split, expected and observed signals, environment, timestamp, run, and trace IDs |
| D13-FLOW-02 | D13-UC-02 | Recovery | Canary breaches its error threshold | 1. Freeze promotion. 2. Classify artifact, configuration, schema, and data effects. 3. Route traffic to prior compatible release or issue forward repair. 4. Reconcile owned state. 5. Probe affected and unaffected tenants. | Rollback decision, restored route digest, schema/data status, affected denial or recovery trace, unaffected positive trace, environment, time, and run ID |
The maintainer changes one application while only a controlled portion of requests reaches the candidate. That bounded path creates room to compare before committing every customer.
System design derived from the flows
Rollback fails when multiple controllers write the same state or when runtime recovery is expected to undo generated-app data. One mutating owner per resource keeps diagnosis and repair directional.
| Use case ID | Entry point | Responsible services | Authoritative store | Failure evidence |
|---|---|---|---|---|
| D13-UC-01 | Control-plane deployment action | Release Orchestrator, compatibility checker, GitOps/runtime deployment controller, Route Controller, telemetry evaluator | Release Store for intent; Git for workload declaration; runtime API for live deployment | Candidate not ready, controller drift, threshold breach, or schema incompatibility |
| D13-UC-02 | Incident recovery action | Incident workflow, Route Controller, GitOps/runtime controller, Migration Service, generated-app data owner | Incident Store for decision; generated-app database for domain state | Route restored but errors persist, incompatible down-migration, repair incomplete, or unaffected control fails |
The Release Orchestrator owns the customer request and policy decision, runtime controllers own pods and routes, and the generated application owns its schema and data. Terraform still owns underlying infrastructure existence; GitOps owns declared workloads; neither should mutate the other’s records.
Data model and ownership
A prior container image is not a recovery plan when schema or business records have changed. The model preserves compatibility and repair decisions alongside release identity.
Generated-application database: Not created in this runnable slice — rollback changes the stored hosted-runtime payload only; app-owned schema, migration ledger, compatibility checks, and business records remain production requirements.
| Record or entity | Store and owner | Primary key | Foreign key or opaque reference | Tenant key | Material constraint | Lifecycle and deletion | Use case IDs |
|---|---|---|---|---|---|---|---|
| DeploymentRevision | Control-plane Release Store, owned by Release Orchestrator | deployment_revision_id | Opaque artifact_digest and prior_revision_id | organization_id | Revision, policy, config, and target environment are immutable after approval | Create, canary, promote or supersede, retain through rollback window, expire by policy | D13-UC-01, D13-UC-02 |
| MigrationLedger | Generated-application database, owned by Migration Service | migration_id | Opaque deployment_revision_id | app_tenant_id | Migration is ordered, idempotent, and declares backward compatibility and repair owner | Plan, apply, verify, compensate or retain, delete only with tenant retirement | D13-UC-01, D13-UC-02 |
| RecoveryDecision | Incident Store, owned by Incident Commander | recovery_decision_id | Opaque deployment_revision_id and evidence_bundle_id | organization_id | Exactly one decision—rollback, forward repair, or contain—has accountable owner and deadline | Open on breach, update with observations, close after positive and negative proof, retain immutably | D13-UC-02 |
The same immutable revision reaches a named recovery decision, and the evidence covers runtime route plus generated-app compatibility. This prevents a green rollout status from masking a broken customer or data path.
Reconcile through the owning controller
Imperative pod edits may briefly hide symptoms and then be overwritten by GitOps, so production recovery must change the owning declaration. The current product model implements a smaller local rollback: the owner selects an existing release and the runtime redeploys its stored artifact. It does not route canary traffic, revert Git, or prove Argo CD recovery. Read the exact method in services/control_plane/domain.py.
def rollback(self, project: Project, actor: str, release_id: str) -> dict[str, str]:
self._owner(project, actor)
release = next((item for item in project.releases if item["release_id"] == release_id), None)
if release is None:
raise ValueError("release not found")
self.runtime.deploy(project.organization_id, project.project_id, {"mode": "published", **release, "source": self.artifacts.get(project.organization_id, release["artifact_id"])})
self._audit(project, actor, "release.rolled_back")
return release
| Effect field | What happens |
|---|---|
| Declared intent | Redeploy one previously recorded release and reject an unknown release ID |
| Interpreter | Python executes Forge.rollback; the injected artifact store supplies source and the runtime adapter owns deployment state |
| Software effect | The tenant/app runtime row is overwritten with the chosen published payload and release.rolled_back is published |
| Hardware effect | Local control-plane/runtime CPU, network, and SQLite storage are used; no Git, Argo CD, Kubernetes rollout, canary route, or database migration changes |
| Evidence | Returned release ID, runtime payload/artifact ID, audit action, unknown-release denial, and unaffected project read |
Key takeaways
Rollback is a coordinated business recovery decision, not merely an older image tag.
- Bounded traffic limits blast radius and creates comparative evidence.
- Artifact, configuration, schema, and data reversibility are separate questions.
- Repair the desired state through its single owner, then verify the customer path.
Checklist
A change is safe only when promotion and recovery are both rehearsed.
- [ ] Prior and candidate artifacts are immutable and identifiable
- [ ] Compatibility is checked before traffic changes
- [ ] Threshold breach stops promotion automatically
- [ ] Recovery proves affected and unaffected customer paths