Skip to content

deploy: Helm chart for the api and engine roles (#61, #62) - #103

Merged
richardmhope merged 2 commits into
mainfrom
m4-helm
Jul 31, 2026
Merged

deploy: Helm chart for the api and engine roles (#61, #62)#103
richardmhope merged 2 commits into
mainfrom
m4-helm

Conversation

@richardmhope

Copy link
Copy Markdown
Contributor

Stacked on #102 (which is stacked on #101). Review the last commit.

deploy/helm/icebergsst/ — api Deployment/Service/Ingress, engine Deployment + HPA, a
pre-upgrade migration Job, a ConfigMap, two Secrets, two ServiceAccounts, and an
optional NetworkPolicy pair.

Two Secrets, not one

This is the shape of the whole chart. The api's Secret holds the database URL, master key, session
secret and OIDC client secret. The engine's holds a broker URL and its own token — no database
credential, no master key, because there is nothing an engine could correctly do with either.

The engine Deployment does not reference the api's Secret at all, so ADR 0002 is enforced by what
is mounted rather than by everyone remembering. Separate ServiceAccounts mean a cluster RBAC
policy can express the same boundary.

Production should create both out of band and set secrets.existingApiSecret /
existingEngineSecret — values passed to Helm end up in the release's stored manifest, which is
not where a master key belongs. The inline values exist so a first install works; the docs say
plainly that they are not the production path.

Other decisions worth a look

  • Postgres and Redis are not in the chart. A bundled database is a development convenience that
    becomes a production liability: no backups, no failover, and a helm uninstall that takes the
    findings with it.
  • Migrations are a pre-install,pre-upgrade hook running the api image and
    python -m iceberg_api migrate — the same entry point compose and the migration tests use, so
    production runs what has been exercised. hook-delete-policy leaves a failed Job behind for
    its logs.
  • Engine autoscaling on, api autoscaling off. Engines hold leases rather than state and a
    lapsed lease is reclaimed (ADR 0009); the api's useful replica count is availability, not CPU.
    The HPA has a 5-minute scale-down window so the quiet moment after a scheduled sweep does not
    kill engines mid-lease and cause re-done work.
  • Restricted Pod Security Standard everywhere: non-root uid 10001, no privilege escalation,
    read-only root filesystem, all capabilities dropped, RuntimeDefault seccomp, resource requests.
  • NOTES.txt warns about the two settings that are wrong by default for a real cluster —
    trustedProxyHops at 0 behind an ingress, and secrets supplied via values.

Verification

deploy/helm/verify-chart.sh (make helm-verify, plus a CI job) lints, renders with
example-values.yaml, renders again against externally-managed Secrets, then asserts twelve
properties of the rendered manifests that helm lint cannot see — the engine mounting only its
own Secret and carrying no DB config, migrations as a pre-upgrade hook on the api image, nothing
migrating on start, every workload hardened with requests set, the engine having an HPA and no
Service. The cheap textual half runs in the ordinary suite.

Run against helm 4.2.3: lint clean, both renders succeed, all twelve checks pass.

helm lint                    ok
helm template (example)      ok
helm template (ext secrets)  ok
engine: no db config         ok
engine: own Secret only      ok
migrations: pre-upgrade hook ok
migrations: packaged entry   ok
migrations: api image        ok
workloads hardened           ok
engine HPA                   ok
engine has no Service        ok

check-yaml now skips the chart templates: Go templates only become YAML once rendered, which is
exactly what helm-verify checks.

Closes #61
Closes #62

🤖 Generated with Claude Code

Findings, their append-only trail, and the administrative audit log grow forever.
This bounds that — and, mostly, defines what will never be deleted.

**Every window is off by default.** This database is evidence: it records where
an organisation's secrets are and who did what about each one. Deleting some of
it has to be a decision somebody made, not something that starts happening
because they upgraded. With nothing configured the purge runs and removes
nothing.

**Never eligible, whatever the window says:**

* open findings — an unresolved secret is not old news;
* analyst decisions (`false_positive`, `accepted_risk`, *manual* resolutions) —
  a judgement that expires silently means the finding returns next scan with
  nobody remembering it was already considered. Only `auto` resolutions, which
  are an inference from absence, are eligible;
* suppressed findings — deleting one resurrects it as new on the next scan,
  the exact outcome the suppression exists to prevent (ADR 0008);
* an open finding's events, however old.

The finding clock runs from the decision, not the first sighting: a secret found
two years ago and auto-resolved yesterday is one day old here.

Purges audit themselves. Deleting evidence is an administrative action, so a
round that removed anything writes `retention.purged` with the counts *and the
windows that justified them*, so the row still explains itself when the settings
have since changed. A round that deleted nothing writes nothing — an audit log
full of "deleted 0 rows" every minute is one nobody reads. That row is subject to
the audit window like any other, which is honest rather than a special case
hiding from the policy it enforces.

Runs last in the maintenance round, under the existing advisory lock, and
deletes in batches so the first purge on a database that has never had one cannot
hold locks for minutes. `python -m iceberg_api retention-purge` runs one now.

`docs/retention.md` covers the windows, the never-delete rules, and guidance on
choosing them (including that "you cannot get these rows back").

Closes #73

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`deploy/helm/icebergsst/`: api Deployment/Service/Ingress, engine Deployment
with an HPA, a pre-upgrade migration Job, a ConfigMap, two Secrets, two
ServiceAccounts, and an optional NetworkPolicy pair.

**Two Secrets, not one**, which is the whole shape of the chart. The api's holds
the database URL, master key, session secret and OIDC client secret; the
engine's holds a broker URL and its own token. The engine Deployment does not
reference the api's Secret at all, so ADR 0002 is enforced by what is mounted
rather than by everyone remembering — and separate ServiceAccounts let a cluster
RBAC policy say the same thing.

**Migrations are a `pre-install,pre-upgrade` hook** running the api image and
`python -m iceberg_api migrate` — the entry point compose and the migration
tests already use, so what runs in production is what has been exercised.
`hook-delete-policy` keeps a failed Job for its logs.

**Postgres and Redis are deliberately not in the chart.** A bundled database is
a development convenience that becomes a production liability: no backups, no
failover, and a `helm uninstall` that takes the findings with it.

Engine autoscaling is on by default and the api's is off — engines hold leases,
not state, and a lapsed lease is reclaimed (ADR 0009), while the api's useful
replica count is driven by availability. The HPA's scale-down window is 5
minutes so a quiet moment after a scheduled sweep does not kill engines holding
leases that then have to lapse and be re-done.

Every workload runs the restricted Pod Security Standard: non-root uid 10001, no
privilege escalation, read-only root filesystem, all capabilities dropped,
RuntimeDefault seccomp, resource requests set.

`deploy/helm/verify-chart.sh` (`make helm-verify`, and a CI job) lints, renders
with `example-values.yaml`, renders again against externally-managed Secrets,
and then asserts twelve properties of the *rendered manifests* that `helm lint`
cannot see. The cheap textual half runs in the ordinary suite.

check-yaml skips the chart templates: they are Go templates that only become
YAML once rendered, which is what helm-verify checks.

Verified with helm 4.2.3: lint clean, both renders succeed, all twelve checks
pass.

Closes #61
Closes #62

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@richardmhope
richardmhope merged commit 98b9e20 into main Jul 31, 2026
4 checks passed
@richardmhope
richardmhope deleted the m4-helm branch July 31, 2026 08:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Helm: secrets, values & migration Job Helm: api & engine charts

1 participant