Files
scrivas/findings/code_dashboard.html
adelvalleandAlvaro Del Valle c987ed07c5 EC2 source-code discovery and reliability triage (#2)
Second discovery pass over the Scrivas EC2 estate (`716468089330`, `us-east-2`, 7 instances). Prompted by two client signals: they do not hold the source code for their own platform (built under contract by the incumbent vendor), and they are reporting production reliability issues.

All access was **read-only** — no writes, restarts or config changes on any instance.

## Source code is recoverable

**All 10 application repositories exist as complete git checkouts on Scrivas-owned instances**, with full commit history rather than deployed artifacts.

| Tier | Location | Repos |
|---|---|---|
| App | `Scrivas_dev_env:/home/admin/` | `scrivas_backend` (1380 commits), `scrivas_gate` (183), `scrivas_search` (96), `patient` (59) |
| ML | `ML_dev:/srv/` | `post_processor` (137), `sai_suggestions` (93), `ml_monitoring` (62), `patient-summary-service` (49), `patient_document_parser` (41), `soniox_transcriber` (25) |

Every remote points at `git@git.devteam.space:scrivas/*` — the contractor's self-hosted GitLab, which Scrivas does **not** control. The on-instance checkouts are the client's only independent leverage over their own source.

**Gap:** both `/var/www` frontends are build output with no `.git`. Frontend source is not recoverable from EC2.

**Time-sensitive:** `scrivas_backend` received a commit on the assessment date. Development is active on infrastructure the contractor controls. Mirroring the repos to Scrivas-controlled storage is the recommended immediate action and is **not** included in this PR — it needs a scope decision first (it involves the contractor's GitLab credentials held on the dev box).

## Reliability triage

Production runs **23 containers on a single 15 GiB host** — including 3 Postgres instances, Kafka and OpenSearch — at **73% memory at rest**, with **no per-container memory limits** and **no swap** on any of the 7 instances. With no limits the OOM killer selects by resident size, so it would typically kill a database rather than the worker that caused the pressure. That matches the "random, unreproducible" symptom profile.

Kafka, OpenSearch and search-api additionally carry restart policy `no`, so a host reboot yields a partially-recovered stack that looks healthy from outside.

**Recorded as a structural exposure, not an observed root cause.** No OOM event appears in retained logs, `State.OOMKilled` is false on all 23 containers, and `RestartCount` is **0** on every one — the Celery workers showing "Up 6 hours" were redeployed, not crash-restarted. Confirming the hypothesis needs CloudWatch history these boxes do not retain, which is itself a finding and the basis for recommendations 3–5 in the report.

One encouraging contrast: the **ML tier is well-built** — ECR images tagged by commit SHA, blue/green slots, passing health checks. The application tier is compose-from-git-checkout with uncommitted `.env.save` and `docker-compose.yml.bkp` files in the prod working tree. The better pattern already exists in-house.

## Contents

| File | |
|---|---|
| `scripts/ec2_code_discovery.py` | EC2 inventory — describe + user data |
| `scripts/ec2_code_inspect.py` | read-only SSM probe set; commands reviewable in `PROBES` |
| `findings/ec2_code_discovery_report.md` | narrative writeup |
| `findings/code_dashboard.html` | client-facing dashboard, matching the existing design system |
| `findings/ec2_code_inspect*.json` | raw probe output |
| `index.html` | links the new dashboard and evidence |

## Review notes

- Probe output was **scanned for credentials before commit** ��� no AWS keys, passwords, tokens or private key blocks. The probe set reads manifests and VCS metadata, never file contents.
- Git metadata was read **as the owning user** (`sudo -u`) rather than by writing a `safe.directory` entry into root's gitconfig, to preserve the read-only guarantee.
- Several prod probes initially returned empty and were re-run with stderr visible; the empties were artifacts (`last` is not installed on prod), not clean health. Worth knowing when reading the JSON.
- The dashboard keeps the "what the evidence does not show" section in the **client-facing** version. If CloudWatch later points elsewhere, overclaiming a root cause would cost more credibility than the softer framing gains.

https://claude.ai/code/session_01YMxVaHXJsqpqKwncNQ9b1e
Co-authored-by: Alvaro Del Valle <alvaro.delvalle2@gmail.com>
Reviewed-on: #2
2026-09-02 14:36:28 -06:00

319 lines
20 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scrivas — Source Code &amp; Reliability</title>
<style>
:root {
--bg:#eef0f3; --panel:#ffffff; --panel-2:#f7f8fa; --ink:#171d27;
--ink-soft:#4a5563; --ink-faint:#79828f; --line:#dde1e7; --line-soft:#e8ebef;
--accent:#0d6e78; --accent-ink:#0a565e;
--good:#2f8657; --warn:#b5832a; --crit:#bd463b;
--crit-bg:#fbeceb; --good-bg:#eaf4ee; --warn-bg:#f8f1e2;
--mono: ui-monospace,"SF Mono","SFMono-Regular",Menlo,Consolas,monospace;
--sans: ui-sans-serif,system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
--shadow:0 1px 2px rgba(20,26,38,.04),0 6px 20px -12px rgba(20,26,38,.18);
}
@media (prefers-color-scheme:dark){:root{
--bg:#0d1117;--panel:#161c26;--panel-2:#1b222e;--ink:#e8ecf1;--ink-soft:#aab3c0;
--ink-faint:#7c8695;--line:#28303c;--line-soft:#212934;--accent:#3bb4bf;--accent-ink:#8fdfe6;
--good:#54b681;--warn:#d6a94a;--crit:#e26b60;--crit-bg:#2a1917;--good-bg:#132419;--warn-bg:#251d10;
--shadow:0 1px 2px rgba(0,0,0,.3),0 8px 24px -14px rgba(0,0,0,.6);}}
:root[data-theme="dark"]{
--bg:#0d1117;--panel:#161c26;--panel-2:#1b222e;--ink:#e8ecf1;--ink-soft:#aab3c0;
--ink-faint:#7c8695;--line:#28303c;--line-soft:#212934;--accent:#3bb4bf;--accent-ink:#8fdfe6;
--good:#54b681;--warn:#d6a94a;--crit:#e26b60;--crit-bg:#2a1917;--good-bg:#132419;--warn-bg:#251d10;
--shadow:0 1px 2px rgba(0,0,0,.3),0 8px 24px -14px rgba(0,0,0,.6);}
:root[data-theme="light"]{
--bg:#eef0f3;--panel:#ffffff;--panel-2:#f7f8fa;--ink:#171d27;--ink-soft:#4a5563;
--ink-faint:#79828f;--line:#dde1e7;--line-soft:#e8ebef;--accent:#0d6e78;--accent-ink:#0a565e;
--good:#2f8657;--warn:#b5832a;--crit:#bd463b;--crit-bg:#fbeceb;--good-bg:#eaf4ee;--warn-bg:#f8f1e2;
--shadow:0 1px 2px rgba(20,26,38,.04),0 6px 20px -12px rgba(20,26,38,.18);}
*{box-sizing:border-box;}
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);line-height:1.5;-webkit-font-smoothing:antialiased;}
.wrap{max-width:1120px;margin:0 auto;padding:32px 24px 64px;}
header{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:16px;padding-bottom:20px;border-bottom:2px solid var(--ink);}
.eyebrow{font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--accent);font-weight:700;}
h1{font-size:clamp(24px,4vw,34px);margin:6px 0 4px;letter-spacing:-.02em;text-wrap:balance;font-weight:750;}
.sub{color:var(--ink-soft);font-size:14px;}
.idcard{font-family:var(--mono);font-size:12px;color:var(--ink-soft);text-align:right;line-height:1.7;}
.idcard b{color:var(--ink);}
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:22px 0 12px;}
@media (max-width:760px){.kpis{grid-template-columns:repeat(2,1fr);}}
.kpi{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px;box-shadow:var(--shadow);position:relative;overflow:hidden;}
.kpi::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--accent);}
.kpi.crit::before{background:var(--crit);}.kpi.warn::before{background:var(--warn);}.kpi.good::before{background:var(--good);}
.kpi .n{font-size:28px;font-weight:750;letter-spacing:-.02em;font-variant-numeric:tabular-nums;}
.kpi.crit .n{color:var(--crit);}.kpi.warn .n{color:var(--warn);}.kpi.good .n{color:var(--good);}
.kpi .lbl{font-size:12px;color:var(--ink-faint);margin-top:2px;}
.kpi .meta{font-size:11px;color:var(--ink-soft);margin-top:8px;}
section{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:22px 24px;margin-bottom:18px;box-shadow:var(--shadow);}
.sec-head{display:flex;align-items:baseline;gap:10px;margin-bottom:16px;flex-wrap:wrap;}
.sec-head .tag{font-family:var(--mono);font-size:12px;color:var(--accent);font-weight:700;}
.sec-head h2{font-size:17px;margin:0;letter-spacing:-.01em;}
.sec-head .note{margin-left:auto;font-size:12px;color:var(--ink-faint);}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:18px;}
@media (max-width:820px){.grid2{grid-template-columns:1fr;}}
table{width:100%;border-collapse:collapse;font-size:13px;}
th{text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:var(--ink-faint);
font-weight:700;padding:0 10px 8px 0;border-bottom:1px solid var(--line);}
td{padding:9px 10px 9px 0;border-bottom:1px solid var(--line-soft);vertical-align:top;color:var(--ink-soft);}
td b{color:var(--ink);}
tbody tr:last-child td{border-bottom:none;}
.num{font-family:var(--mono);font-variant-numeric:tabular-nums;text-align:right;color:var(--ink);}
code{font-family:var(--mono);font-size:12px;color:var(--ink-soft);}
.scroll{overflow-x:auto;}
.pill{display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:700;padding:3px 9px;border-radius:999px;white-space:nowrap;}
.pill.crit{background:var(--crit-bg);color:var(--crit);}
.pill.good{background:var(--good-bg);color:var(--good);}
.pill.warn{background:var(--warn-bg);color:var(--warn);}
.callout{border-radius:10px;padding:14px 16px;font-size:13px;color:var(--ink-soft);margin:0 0 16px;
background:var(--warn-bg);border:1px solid color-mix(in srgb,var(--warn) 30%,transparent);}
.callout.crit{background:var(--crit-bg);border-color:color-mix(in srgb,var(--crit) 30%,transparent);}
.callout.good{background:var(--good-bg);border-color:color-mix(in srgb,var(--good) 30%,transparent);}
.callout b{color:var(--ink);}
.callout p{margin:8px 0 0;}
.memtrack{height:26px;background:var(--panel-2);border:1px solid var(--line);border-radius:7px;overflow:hidden;display:flex;margin:6px 0 4px;}
.memtrack .used{background:var(--crit);height:100%;}
.memtrack .cache{background:color-mix(in srgb,var(--warn) 55%,transparent);height:100%;}
.memlegend{display:flex;gap:16px;font-size:11.5px;color:var(--ink-faint);flex-wrap:wrap;}
.memlegend i{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:5px;vertical-align:baseline;}
ol.steps{margin:0;padding-left:20px;font-size:13px;color:var(--ink-soft);}
ol.steps li{margin-bottom:9px;}
ol.steps b{color:var(--ink);}
footer{display:flex;flex-wrap:wrap;gap:18px;margin-top:24px;font-size:12px;color:var(--ink-faint);font-family:var(--mono);}
</style>
</head>
<body>
<div class="wrap">
<header>
<div>
<div class="eyebrow">Dasnuve · Cloud Discovery</div>
<h1>Source Code Recovery &amp; Reliability Triage</h1>
<div class="sub">What is running on the Scrivas EC2 estate, where the source lives, and why production is fragile.</div>
</div>
<div class="idcard">
Account <b>716468089330</b><br>
Region <b>us-east-2</b> · 7 instances<br>
2026-08-28 · <b>CONFIDENTIAL</b>
</div>
</header>
<div class="kpis">
<div class="kpi good">
<div class="n">10</div>
<div class="lbl">Repos recoverable on EC2</div>
<div class="meta">Full git history, not just build output</div>
</div>
<div class="kpi warn">
<div class="n">2</div>
<div class="lbl">Frontends <u>not</u> recoverable</div>
<div class="meta">Deployed build only — no <code>.git</code></div>
</div>
<div class="kpi crit">
<div class="n">23</div>
<div class="lbl">Prod containers, one host</div>
<div class="meta">Incl. 3 × Postgres, Kafka, OpenSearch</div>
</div>
<div class="kpi crit">
<div class="n">0</div>
<div class="lbl">Memory limits set</div>
<div class="meta">And zero swap, on all 7 instances</div>
</div>
</div>
<section>
<div class="sec-head">
<span class="tag">01</span><h2>The source code is recoverable — and that is time-sensitive</h2>
<span class="note">Scrivas_dev_env · ML_dev</span>
</div>
<div class="callout good">
<b>All 10 application repositories exist as complete git checkouts on Scrivas-owned instances</b>, with full
commit history — not merely deployed artifacts. Scrivas can recover its codebase from its own AWS account today.
<p>Every repository points at a single external remote: <code>git@git.devteam.space:scrivas/&lt;repo&gt;.git</code>
the incumbent contractor's self-hosted GitLab, which Scrivas does <b>not</b> control. The on-instance checkouts are
therefore the client's only independent leverage over their own source.</p>
</div>
<div class="grid2">
<div>
<div class="sec-head"><span class="tag">APP</span><h2>Scrivas_dev_env <code>/home/admin/</code></h2></div>
<div class="scroll"><table>
<thead><tr><th>Repository</th><th class="num">Commits</th><th>Last commit</th></tr></thead>
<tbody>
<tr><td><b>scrivas_backend</b></td><td class="num">1380</td><td>2026-08-28 · Vasilii</td></tr>
<tr><td><b>scrivas_gate</b></td><td class="num">183</td><td>2026-08-14 · Vasilii</td></tr>
<tr><td><b>scrivas_search</b></td><td class="num">96</td><td>2026-06-24 · Azamat</td></tr>
<tr><td><b>patient</b></td><td class="num">59</td><td>2026-08-13 · vturtugeshev</td></tr>
</tbody>
</table></div>
</div>
<div>
<div class="sec-head"><span class="tag">ML</span><h2>ML_dev <code>/srv/</code></h2></div>
<div class="scroll"><table>
<thead><tr><th>Repository</th><th class="num">Commits</th><th>Last commit</th></tr></thead>
<tbody>
<tr><td><b>post_processor</b></td><td class="num">137</td><td>2026-06-30</td></tr>
<tr><td><b>sai_suggestions</b></td><td class="num">93</td><td>2026-06-16</td></tr>
<tr><td><b>ml_monitoring</b></td><td class="num">62</td><td>2026-07-20</td></tr>
<tr><td><b>patient-summary-service</b></td><td class="num">49</td><td>2026-06-12</td></tr>
<tr><td><b>patient_document_parser</b></td><td class="num">41</td><td>2026-06-16</td></tr>
<tr><td><b>soniox_transcriber</b></td><td class="num">25</td><td>2026-06-16</td></tr>
</tbody>
</table></div>
</div>
</div>
<div class="callout" style="margin:18px 0 0;">
<b>The gap:</b> <code>/var/www/scrivas_frontend</code> and <code>/var/www/Scrivas_admin_frontend</code> are
<b>deployed build output with no <code>.git</code></b>. Frontend source cannot be recovered from EC2 and must be
obtained from the contractor's GitLab.
<p><b>Why this is urgent:</b> <code>scrivas_backend</code> received a commit <b>on the day of this assessment</b>.
Development is active on infrastructure the contractor controls. Mirror all 10 repos
(<code>git clone --mirror</code>) to Scrivas-controlled storage <i>before</i> any contract conversation changes
access posture.</p>
</div>
</section>
<section>
<div class="sec-head">
<span class="tag">02</span><h2>Two tiers, two engineering standards</h2>
<span class="note">the better pattern already exists in-house</span>
</div>
<div class="grid2">
<div>
<div class="sec-head"><span class="tag">APP</span><h2>Application tier <span class="pill crit">fragile</span></h2></div>
<p class="sub" style="font-size:13px;">23 containers on a <b>single host</b><code>encounter_api</code>,
<code>gate_api</code>, <code>patient_api</code>, 8 Celery workers, <code>celery_beat</code>,
<b>3 × Postgres 17.5</b>, Redis, Kafka, Debezium CDC, OpenSearch, autoheal.</p>
<p class="sub" style="font-size:13px;">Deployed by <code>docker-compose</code> directly from git checkouts.
No orchestration, no image registry, no immutable artifact.</p>
</div>
<div>
<div class="sec-head"><span class="tag">ML</span><h2>ML tier <span class="pill good">mature</span></h2></div>
<p class="sub" style="font-size:13px;">Deployed from <b>ECR</b>
(<code>…dkr.ecr.us-east-2…/scrivas/*</code>) with <b>commit-SHA image tags</b> and
<b>blue/green slots</b> (<code>dev-green-*</code>, <code>stage-blue-*</code>).</p>
<p class="sub" style="font-size:13px;">Health checks defined and passing across every service.
This is a materially stronger delivery pipeline — and a template the application tier can adopt.</p>
</div>
</div>
</section>
<section>
<div class="sec-head">
<span class="tag">03</span><h2>Reliability findings</h2>
<span class="note">ranked by likely contribution to reported instability</span>
</div>
<div class="scroll"><table>
<thead><tr><th>#</th><th>Finding</th><th>Evidence</th><th>Risk</th></tr></thead>
<tbody>
<tr><td class="num">1</td><td><b>No memory limit on any prod container</b></td><td><code>HostConfig.Memory=0</code> on all 23</td><td><span class="pill crit">High</span></td></tr>
<tr><td class="num">2</td><td><b>No swap on any instance</b></td><td><code>swapon --show</code> empty, all 7</td><td><span class="pill crit">High</span></td></tr>
<tr><td class="num">3</td><td><b>Prod at 73% memory at rest</b></td><td>11 GiB used of 15 GiB</td><td><span class="pill crit">High</span></td></tr>
<tr><td class="num">4</td><td><b>Whole stack on one host, incl. 3 databases</b></td><td><code>docker ps</code> — 23 containers</td><td><span class="pill crit">High</span></td></tr>
<tr><td class="num">5</td><td><b>3 prod containers set to never restart</b></td><td>Kafka, OpenSearch, search-api — policy <code>no</code></td><td><span class="pill crit">High</span></td></tr>
<tr><td class="num">6</td><td>Prod OS differs from dev/stage</td><td>Ubuntu <b>26.04</b> vs <b>24.04</b> elsewhere</td><td><span class="pill warn">Medium</span></td></tr>
<tr><td class="num">7</td><td><code>autoheal</code> deployed in production</td><td><code>willfarrell/autoheal:1.2.0</code></td><td><span class="pill warn">Medium</span></td></tr>
<tr><td class="num">8</td><td>Uncommitted config drift in prod checkouts</td><td><code>docker-compose.yml.bkp</code>, <code>.env.save</code></td><td><span class="pill warn">Medium</span></td></tr>
<tr><td class="num">9</td><td>47 interactive login sessions on prod</td><td><code>uptime</code> (32 on Ml_prod)</td><td><span class="pill warn">Medium</span></td></tr>
<tr><td class="num">10</td><td><code>sai-suggestions-redis.service</code> failed</td><td>ML_stage <code>systemctl --state=failed</code></td><td><span class="pill good">Low</span></td></tr>
</tbody>
</table></div>
</section>
<section>
<div class="sec-head">
<span class="tag">04</span><h2>Primary hypothesis: unbounded memory on a shared host</h2>
</div>
<p class="sub" style="font-size:13px;margin-top:0;">Production memory, at rest — before any workload spike:</p>
<div class="memtrack" role="img" aria-label="Production memory: 11 GiB used, 3.5 GiB cache, of 15 GiB total">
<div class="used" style="width:73%"></div><div class="cache" style="width:23%"></div>
</div>
<div class="memlegend">
<span><i style="background:var(--crit)"></i>11 GiB used (73%)</span>
<span><i style="background:color-mix(in srgb,var(--warn) 55%,transparent)"></i>3.5 GiB cache</span>
<span><i style="background:var(--panel-2);border:1px solid var(--line)"></i>441 MiB free</span>
<span><b style="color:var(--crit)">0 B swap</b></span>
</div>
<div class="callout crit" style="margin-top:18px;">
Findings 14 compound into a single failure mode. Production runs 23 containers — including three Postgres
instances, Kafka and OpenSearch — on one 15 GiB host already <b>73% consumed at rest</b>, with
<b>no per-container memory limits</b> and <b>no swap</b>. A single Celery worker handling an oversized document
or audio chunk can exhaust host memory. With no limits, the kernel OOM killer selects by resident size — so it
will typically kill a <b>database</b>, not the worker that caused the problem.
<p>That yields precisely the profile a client reports as "random reliability issues": unpredictable, uncorrelated
with the triggering workload, and difficult to reproduce.</p>
<p><b>Finding 5 compounds recovery.</b> Kafka, OpenSearch and search-api carry restart policy <code>no</code> — if
they die or the host reboots, they <b>stay down</b> until someone intervenes. Everything else is
<code>unless-stopped</code>, so a reboot yields a partially-recovered stack that looks healthy from the outside.</p>
<p><b>Finding 7 corroborates.</b> Someone deliberately deployed <code>autoheal</code>, a daemon whose only job is
restarting containers that fail health checks. That treats a symptom — and indicates health-check failures were
frequent enough to be worth automating around.</p>
</div>
<div class="callout">
<b>What the evidence does not show — stated plainly, because it bounds the conclusion.</b>
<p>· <b>No OOM kill was captured.</b> <code>dmesg</code> and <code>journalctl</code> on prod show no OOM events in
the retained window; <code>State.OOMKilled</code> is <code>false</code> on all 23 containers.</p>
<p>· <b>No crash-looping.</b> <code>RestartCount</code> is <b>0</b> on every prod container. The Celery workers
showing "Up 6 hours" were <b>redeployed</b> at 13:49 that day, not restarted by failure.</p>
<p>· <code>last</code> is not installed on prod, so reboot history could not be read there.</p>
<p>Findings 15 are therefore a demonstrated <b>structural exposure</b>, not an observed root cause. Confirming
the hypothesis requires history these instances do not currently retain — which is itself a finding.</p>
</div>
</section>
<section>
<div class="sec-head"><span class="tag">05</span><h2>Recommended next steps</h2></div>
<div class="grid2">
<div>
<div class="sec-head"><span class="tag">NOW</span><h2>Immediate</h2></div>
<ol class="steps">
<li><b>Mirror all 10 repos</b> to Scrivas-controlled storage. Highest value, lowest effort, time-sensitive.</li>
<li><b>Set <code>unless-stopped</code></b> on Kafka, OpenSearch and search-api — removes the silent partial-recovery failure.</li>
</ol>
<div class="sec-head" style="margin-top:18px;"><span class="tag">PROVE</span><h2>Confirm the hypothesis</h2></div>
<ol class="steps" start="3">
<li><b>Enable per-container memory metrics</b> in CloudWatch — <code>EC2-CloudWatchAgent-Role</code> is already attached.</li>
<li><b>Add <code>mem_limit</code> per service</b>, sized from observed peaks; add swap as an OOM buffer.</li>
<li><b>Query historic <code>mem_used_percent</code></b> to date the onset of instability.</li>
</ol>
</div>
<div>
<div class="sec-head"><span class="tag">FIX</span><h2>Structural</h2></div>
<ol class="steps" start="6">
<li><b>Move the three Postgres instances</b> off the shared application host.</li>
<li><b>Align prod OS/AMI with stage</b> — 26.04 vs 24.04 makes staging an unreliable rehearsal for production.</li>
<li><b>Extend the ML tier's pattern</b> — ECR, commit-SHA tags, blue/green — to the application tier. The better pattern already exists in-house.</li>
<li><b>Reconcile config drift</b> (<code>.env.save</code>, <code>docker-compose.yml.bkp</code>) into version control.</li>
<li><b>Audit the 47 interactive prod sessions.</b></li>
</ol>
</div>
</div>
</section>
<footer>
<span>Read-only · SSM probes via <code>dasnuve-scrivas-louis-impersonation</code></span>
<span>Reproduce: <code>scripts/ec2_code_inspect.py</code></span>
<span>Raw: <code>findings/ec2_code_inspect.json</code></span>
<span>Narrative: <code>findings/ec2_code_discovery_report.md</code></span>
</footer>
</div>
</body>
</html>