<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Andrey Lesnikov</title>
    <description>The latest articles on DEV Community by Andrey Lesnikov (@andrey_lesnikov_03d1b72f9).</description>
    <link>https://dev.to/andrey_lesnikov_03d1b72f9</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4187294%2F8391d73a-4dc9-4312-aea9-64ad5984dfab.png</url>
      <title>DEV Community: Andrey Lesnikov</title>
      <link>https://dev.to/andrey_lesnikov_03d1b72f9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/andrey_lesnikov_03d1b72f9"/>
    <language>en</language>
    <item>
      <title>Rehearse the Change Before You Apply It: Fail-Closed Blast Radius for Kubernetes and Argo CD</title>
      <dc:creator>Andrey Lesnikov</dc:creator>
      <pubDate>Sat, 17 Oct 2026 15:56:44 +0000</pubDate>
      <link>https://dev.to/andrey_lesnikov_03d1b72f9/rehearse-the-change-before-you-apply-it-fail-closed-blast-radius-for-kubernetes-and-argo-cd-3l6l</link>
      <guid>https://dev.to/andrey_lesnikov_03d1b72f9/rehearse-the-change-before-you-apply-it-fail-closed-blast-radius-for-kubernetes-and-argo-cd-3l6l</guid>
      <description>&lt;p&gt;A Helm upgrade scales two deployments in a &lt;code&gt;payments&lt;/code&gt; namespace: one goes from 6 to 18 replicas, the other from 4 to 12. Review is easy. CI is green. The diff is tiny. Argo CD syncs it, and a few minutes later the new pods are stuck. The events say &lt;code&gt;FailedCreatePodSandBox: failed to assign an IP address&lt;/code&gt;. The pod IP pool was nearly full, and nobody in the review thought about it, because nobody in the review looks at IP capacity when they read a replica count.&lt;/p&gt;

&lt;p&gt;That scenario is not hypothetical decoration. It is the main end-to-end fixture in my open-source project, &lt;a href="https://github.com/justrunme/architecture-rehearsal" rel="noopener noreferrer"&gt;Architecture Rehearsal&lt;/a&gt;, because it is the cleanest example of a failure class I care about: changes whose blast radius only shows up once production executes them. The review had opinions. Production had the actual dependency graph.&lt;/p&gt;

&lt;p&gt;I'm a DevOps platform engineer, and this post explains what the tool does, how it is built, what it has actually been tested against, and where it falls short.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: rehearse first, and treat "I don't know" as a result
&lt;/h2&gt;

&lt;p&gt;The core idea is simple. Before a change is applied, build a model of the relevant part of the system, apply the change to the model, and run a set of deterministic failure scenarios against it. Then let a policy decide whether the change is allowed to proceed.&lt;/p&gt;

&lt;p&gt;The part I spent the most time on is a rule from the project's first ADR:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Missing data never becomes false &lt;code&gt;approve&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most gates I have used are binary: pass or fail. The problem is what they do when they lack information. If the PDB inventory was never collected, a binary checker usually finds no PDB violations and passes. That is a false approve, and it is worse than having no gate, because now there is a green check to point at.&lt;/p&gt;

&lt;p&gt;So the gate has four outcomes, each with its own exit code:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Exit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;approve&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No matched high-risk patterns; required coverage present&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;warn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Medium-risk findings&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;block&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;High/critical findings with evidence&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unknown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Required data missing, not safe to approve&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;unknown&lt;/code&gt; is fail-closed by default. CI has to treat exit 4 as a failing gate, or someone has to explicitly write a policy that says otherwise. The intended consequence is that operators fix their collectors and RBAC instead of ignoring the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The pipeline, in the repo's own words, is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Collect â†’ Model â†’ Propose â†’ Rehearse â†’ Gate â†’ Observe â†’ Verify â†’ Calibrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A causal graph from a cluster baseline
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;rehearsal snapshot k8s&lt;/code&gt; turns a cluster dump (a &lt;code&gt;kubectl&lt;/code&gt; List export, or live collection) into a baseline snapshot: workloads, services, PVCs, NetworkPolicies, PDBs and the edges between them. A change is compiled from rendered manifests (&lt;code&gt;rehearsal change manifests&lt;/code&gt;), from Terraform plans, or from an Argo CD Application's desired state. The change is scoped: by default it cannot remove nodes, so a rendered chart for one namespace doesn't accidentally look like "delete everything else in GitOps".&lt;/p&gt;

&lt;p&gt;The scenarios are plain Go code, each with a name and a risk model. The current set includes &lt;code&gt;cni-ip-capacity&lt;/code&gt;, &lt;code&gt;pdb-disruption&lt;/code&gt;, &lt;code&gt;rwo-node-loss&lt;/code&gt;, &lt;code&gt;volume-az&lt;/code&gt;, &lt;code&gt;service-routing&lt;/code&gt; and &lt;code&gt;prom-zero-match&lt;/code&gt;. For the Argo CD path there are dedicated detectors for scheduling capacity (CPU/memory requests vs allocatable), PVC zone conflicts, NetworkPolicies that cut a dependency, PDB-vs-rollout conflicts, and blast radius. Each one returns PASS, WARN, BLOCK or UNKNOWN. When a scenario doesn't have the facts it needs, it emits an &lt;code&gt;unknown&lt;/code&gt; finding instead of staying silent.&lt;/p&gt;

&lt;p&gt;There is no LLM in the risk path. The capability matrix lists that explicitly as not supported. The graph and the rules decide.&lt;/p&gt;

&lt;h3&gt;
  
  
  The policy gate
&lt;/h3&gt;

&lt;p&gt;Scenarios produce facts. A small policy engine turns them into a decision. This is the built-in default, written as the YAML policy document the engine accepts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rehearsal.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RehearsalPolicy&lt;/span&gt;
&lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;risk&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;["critical","high"]'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"block"'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rollback&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"unavailable"'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;requiredMissing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0'&lt;/span&gt;
&lt;span class="na"&gt;warn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;risk&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"medium"'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"warn"'&lt;/span&gt;
&lt;span class="na"&gt;unknownAsBlock&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter more than the syntax. First, if you omit &lt;code&gt;unknownAsBlock&lt;/code&gt;, it defaults to &lt;code&gt;true&lt;/code&gt;; setting it to &lt;code&gt;false&lt;/code&gt; is allowed but logged as a warning. Second, an expression the engine can't parse doesn't get skipped. It is rejected when the policy loads, and if one somehow reaches evaluation it produces &lt;code&gt;block&lt;/code&gt;. Both of these were bugs I fixed in the latest round of hardening. Before that fix, an omitted &lt;code&gt;unknownAsBlock&lt;/code&gt; meant &lt;code&gt;false&lt;/code&gt;, and an unrecognised expression simply never matched, so a typo could quietly weaken a policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  RehearsalRun: the gate as cluster state
&lt;/h3&gt;

&lt;p&gt;For teams that want this inside the cluster, there is an operator built on controller-runtime and a &lt;code&gt;RehearsalRun&lt;/code&gt; CRD (&lt;code&gt;rehearsal.io/v1beta1&lt;/code&gt;). The example in the repo looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rehearsal.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RehearsalRun&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments-rollout&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;baselineRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;baselines/prod.json&lt;/span&gt;
  &lt;span class="na"&gt;changeRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;changes/payments-v2.json&lt;/span&gt;
  &lt;span class="c1"&gt;# observedRef: observed/prod-after.json&lt;/span&gt;
  &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The status carries &lt;code&gt;phase&lt;/code&gt;, &lt;code&gt;decision&lt;/code&gt;, &lt;code&gt;risk&lt;/code&gt;, &lt;code&gt;controlPlaneRunId&lt;/code&gt;, &lt;code&gt;jobId&lt;/code&gt;, &lt;code&gt;evidenceDigest&lt;/code&gt;, a &lt;code&gt;specDigest&lt;/code&gt; to detect drift, and standard conditions (&lt;code&gt;Accepted&lt;/code&gt;, &lt;code&gt;Running&lt;/code&gt;, &lt;code&gt;Ready&lt;/code&gt;, &lt;code&gt;Failed&lt;/code&gt;). Each spec generation gets its own run ID (&lt;code&gt;{namespace}-{name}-{uid8}-g{generation}&lt;/code&gt;), so editing a run produces a new rehearsal instead of overwriting the old record.&lt;/p&gt;

&lt;p&gt;One design decision started as a real security bug: the CR can never carry the control plane URL or token. An earlier version had a &lt;code&gt;controlPlaneURL&lt;/code&gt; field. That meant anyone allowed to create a &lt;code&gt;RehearsalRun&lt;/code&gt; could point the operator's bearer token at a server they controlled. The field was removed in v1.5.1, and now the URL and token come only from the operator's Deployment and Secret.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying the prediction against reality
&lt;/h3&gt;

&lt;p&gt;A gate that is never checked against reality slowly turns into folklore. After a deploy (or after a change was applied anyway), &lt;code&gt;rehearsal verify&lt;/code&gt; compares the rehearsal report with an observed snapshot. It checks that the report is bound to the same baseline and change digests, that the change was actually applied, and that each predicted scenario is consistent with what was observed. Operator annotations such as "we saw failure X" count only as soft checks. The independent scenario check has to pass on its own, and pods stuck in &lt;code&gt;Pending&lt;/code&gt; across the cluster don't count as confirmation. The outcome feeds a calibration step that tracks how accurate each scenario has been.&lt;/p&gt;

&lt;h3&gt;
  
  
  A content-addressed evidence chain
&lt;/h3&gt;

&lt;p&gt;Every analysis writes an evidence chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;baseline â†’ change â†’ proposed â†’ report â†’ observed â†’ verification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each link is a SHA-256 digest computed from the live objects. The report digest is always recomputed from the report body, so if someone edits &lt;code&gt;decision&lt;/code&gt; or &lt;code&gt;risk&lt;/code&gt; in a stored report and leaves the old digest field alone, verification fails. The chain can be wrapped in a DSSE-style envelope signed with HMAC or Ed25519. I want to be precise here: this is not full Sigstore, and the threat model says plainly that HMAC does not give you non-repudiation.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Argo CD PreSync hook
&lt;/h3&gt;

&lt;p&gt;The newest piece came from an obvious gap. A CI check only runs if the change goes through CI. A manual sync from the Argo CD UI, or a direct commit to the tracked branch, skips it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rehearsal argo-presync&lt;/code&gt; runs the same impact gate as an Argo CD &lt;strong&gt;PreSync&lt;/strong&gt; Job (&lt;code&gt;argocd.argoproj.io/hook: PreSync&lt;/code&gt;). The Job reads the desired manifests, takes a live snapshot, runs the detectors and prints a single JSON decision line. ALLOW and WARN exit 0, so the sync continues. BLOCK exits 3 and UNKNOWN exits 4, and Argo aborts the sync with the live objects untouched. Missing inputs, API errors and timeouts also fail closed.&lt;/p&gt;

&lt;p&gt;There is an audited escape hatch for incidents: annotate the Application with &lt;code&gt;rehearsal.io/break-glass: "true"&lt;/code&gt; plus a non-empty &lt;code&gt;rehearsal.io/break-glass-reason&lt;/code&gt;. The Job logs the override and includes it in the JSON line. Break-glass without a reason is itself treated as a failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real run
&lt;/h2&gt;

&lt;p&gt;Everything below comes from the repo at commit &lt;code&gt;920ff4f&lt;/code&gt;, built locally (&lt;code&gt;rehearsal 1.6.0&lt;/code&gt;; the PreSync gate shown later is part of v1.7.0). These are the commands from &lt;code&gt;scripts/e2e_pipeline.sh&lt;/code&gt; (the same steps as &lt;code&gt;make e2e&lt;/code&gt;), run against the payments fixture from the opening.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/justrunme/architecture-rehearsal.git
&lt;span class="nb"&gt;cd &lt;/span&gt;architecture-rehearsal
go build &lt;span class="nt"&gt;-o&lt;/span&gt; bin/rehearsal ./cmd/rehearsal
bash scripts/e2e_pipeline.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Snapshot, scoped change, then analysis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;==&amp;gt; 1/5 snapshot k8s (baseline from kubectl List dump)
wrote snapshot out/e2e-pipeline/baseline.json phase=baseline source=kubernetes-manifests (22 nodes)
==&amp;gt; 2/5 change manifests (scoped to payments; removals off)
wrote change out/e2e-pipeline/change.json (removals=false scope_ns=[payments])
  scope OK: 2 patch(es), 0 removals
==&amp;gt; 3/5 analyze (expect exit 3 block)
risk=critical decision=block findings=1 rollback=unknown digest=270e3b4f4ea2e25509461038cafe2e0fe804027a73abce74b77bc3fb6e55e5f9 evidence=out/e2e-pipeline/change-helm-scale-payments-e2e-20261017T133404Z
  analyze blocked as expected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The finding in &lt;code&gt;latest-report.json&lt;/code&gt; explains the block with numbers instead of a vague severity label:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pod IP capacity exhaustion (CNI / VPC)"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Replica delta 20 + surge 6 = 26 IPs needed; available 10 (deficit 16). baseline=14 proposed=34"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"cascade"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"baseline_replicas=14"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"proposed_replicas=34"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"rollout_surge=6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"requested_ips=26 available=10 deficit=16"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"IP exhaustion â†’ FailedCreatePodSandbox â†’ readiness timeout â†’ Helm pending-install"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"recommended_controls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"increase max-pods / prefix delegation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"add nodes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"reduce maxSurge"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same report includes a &lt;code&gt;coverage&lt;/code&gt; block (overall &lt;code&gt;0.45&lt;/code&gt; on this fixture) and lists its gaps, for example &lt;code&gt;no meta.metric_labels â€” prom scenarios limited&lt;/code&gt;. I want the tool to say what it didn't look at.&lt;/p&gt;

&lt;p&gt;The fixture also contains a post-deploy dump for the case where the change was applied anyway. Verification passes, with the independent scenario check succeeding apart from the operator annotation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"name": "scenario:cni-ip-capacity",
"passed": true,
"detail": "meta.cni_ip_available=0 (explicit CNI provider)"
...
"name": "rollout_status:workload/payments/payments-api",
"detail": "rollout_not_converged (consistent with capacity/CNI prediction risk): readyReplicas=6 want=18",
...
  verify OK outcome=verified score=1 digest=72f7dbab61a69655
  decision=block risk=critical failures=['cni-ip-capacity']
e2e pipeline OK: dumpâ†’graphâ†’scoped changeâ†’blockâ†’verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Signing and verifying the chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ export REHEARSAL_HMAC_SECRET="$(openssl rand -hex 32)"
$ ./bin/rehearsal evidence sign-dsse --chain out/e2e-pipeline/latest-chain.json --out out/e2e-pipeline/evidence-dsse.json
wrote out/e2e-pipeline/evidence-dsse.json verified=true
$ ./bin/rehearsal evidence verify-dsse --envelope out/e2e-pipeline/evidence-dsse.json
signature OK keyId=hmac-env changeId=change-helm-scale-payments-e2e baseline=3035a1f23205 change=e73dd8fe9fa6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Argo CD fixtures (&lt;code&gt;bash scripts/e2e_argo_impact.sh&lt;/code&gt;) show the four outcomes. This is the case I care about most, where the baseline never recorded PDB collection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;==&amp;gt; unknown-pdb-inventory (expect 4)
verdict=UNKNOWN decision=unknown findings=1 confidence=low ...
unknown-pdb-inventory exit=4 expect=4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated markdown report says why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| `pdb-rollout` | **UNKNOWN** | low | PDB inventory status unknown (baseline did not record PDB collection) |

## Missing data

Gate cannot safely PASS while these are missing:

- `pdb-inventory`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four detectors passed. A binary gate would probably have approved this change. This one refuses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What has actually been tested
&lt;/h2&gt;

&lt;p&gt;I keep a claim-by-claim &lt;a href="https://github.com/justrunme/architecture-rehearsal/blob/main/docs/EVIDENCE.md" rel="noopener noreferrer"&gt;EVIDENCE.md&lt;/a&gt; that links each claim to a CI run. In short: the policy gate, the evidence chain and DSSE path are covered by the &lt;code&gt;test&lt;/code&gt; job. The operator installs via Helm on kind in &lt;code&gt;operator-kind-e2e&lt;/code&gt;. The native Argo CD gate runs against real Argo CD Applications on kind in &lt;code&gt;argo-kind-e2e&lt;/code&gt;, where a benign Synced app passes and a risky OutOfSync Deployment is blocked.&lt;/p&gt;

&lt;p&gt;The PreSync hook runs in &lt;code&gt;argo-presync-kind-e2e&lt;/code&gt; with pinned Argo CD v2.14.9. On that fixture set (kind e2e plus an offline matrix), 5 of 5 risky changes were blocked and 0 of 3 safe changes were falsely blocked. The in-cluster hook added roughly 0.9 to 1.0 seconds per sync on a single-node kind cluster. Those are eight cases. That's a regression check. It tells you nothing about field accuracy, and I wouldn't quote it as an accuracy figure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The blast radius is simulated.&lt;/strong&gt; It comes from a graph plus a change, not from fault injection against a live cluster. It complements chaos engineering and doesn't replace it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model is only as good as the inventory.&lt;/strong&gt; Fail-closed protects you from false approves caused by &lt;em&gt;missing&lt;/em&gt; data. It can't protect you from data that is present but wrong. The repo explicitly does not claim zero false approves on incomplete real-world inventories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live collection against arbitrary production clusters&lt;/strong&gt; is still at the "reference / not yet" stage in the evidence table. CI uses fixtures and kind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-cluster.&lt;/strong&gt; Everything tested is single-node kind. Multi-cluster federation of evidence stores is out of scope for now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The scenario catalogue is small.&lt;/strong&gt; Six rehearsal scenarios and five Argo detectors cover failure classes I have seen often, not everything that can go wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The PreSync hook expects desired manifests in a ConfigMap&lt;/strong&gt;, which adds wiring work. It shipped in the v1.7.0 release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No production SLOs yet.&lt;/strong&gt; There are no published time-to-gate or false-approve-rate numbers beyond the fixture metrics above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signing is DSSE-style with HMAC/Ed25519&lt;/strong&gt;, not a full Sigstore/transparency-log setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Next on my list: more evidence from live collection, more scenarios and detectors (especially around IAM and observability coverage, which score 0 on the main fixture), a less manual way to feed desired state into the PreSync hook, and turning calibration data into something a team can read as "this scenario has been right N out of M times on our clusters".&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it, and tell me where it's wrong
&lt;/h2&gt;

&lt;p&gt;The repo is Apache-2.0: &lt;strong&gt;&lt;a href="https://github.com/justrunme/architecture-rehearsal" rel="noopener noreferrer"&gt;github.com/justrunme/architecture-rehearsal&lt;/a&gt;&lt;/strong&gt;. The fastest way in is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make demo &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make e2e
make e2e-argo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a longer write-up on the &lt;a href="https://justrunme.com/cases/architecture-rehearsal/" rel="noopener noreferrer"&gt;case study page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'm especially interested in two kinds of feedback: failure classes from your own incidents that a graph-plus-scenario model should catch, and places where the gate says &lt;code&gt;approve&lt;/code&gt; when you think it should have said &lt;code&gt;unknown&lt;/code&gt;. Issues and PRs are welcome.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Andrey Lesnikov is a Senior DevOps Platform Engineer based in Halle, Germany, and a 3rd place Company Award recipient at the Cloud Native Rockstar Awards 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>gitops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
