Consize — Feature Guide
Where every feature is, how it works, and exactly where the code lives.
Overview
Consize is an infrastructure rightsizing platform that finds the waste in your Kubernetes workloads and cloud databases, removes it safely, and proves the savings. The product is a loop, not a report: analyze real usage → recommend guarded applies → verify the change against the system's own signals → auto-rollback on regression → audit every step. One dashboard and one savings number cover both surfaces — compute (pod requests/limits) and data (database instance classes). The savings figure is the demo; the safety engine is the product. Two defining principles run through everything: nothing applies silently, and no apply happens without an audit trail (ADR-008).
1. Compute surface (collector + Prometheus)
What it does. Ingests Kubernetes workload metadata and 14 days of Prometheus
usage for every managed deployment, stored as idempotent 15-minute buckets in
usage_buckets (P50=P95=P99=Max per window — single-sample windows, ADR-011).
How it works. A one-shot collector binary runs as a CronJob every 15 minutes
(*/15 * * * *). It runs Prometheus query_range queries for CPU and memory
(container_cpu_usage_seconds_total rate, container_memory_working_set_bytes)
(internal/collector/collector.go), and resolves pod→deployment ownership with
three bulk list calls — never per-pod lookups (ADR-015). CONSIZE_NAMESPACES
scopes reads per-namespace so a least-privilege Role works (ADR-025). Windows
are upserted by (workload, metric, window_start), so re-runs are safe and
backfill is a re-run (ADR-007).
Where the code lives.
- engine/cmd/collector/main.go — binary entry point
- engine/internal/collector/collector.go — orchestration, k8s + Prometheus intake
- engine/internal/collector/k8s.go — metadata + owner resolution
- engine/internal/collector/prometheus.go — query_range client
- engine/deploy/collector-cronjob.yaml — the CronJob manifest
Where you see it. The consize-collector CronJob; its output shows in
GET /api/v1/workloads and every chart.
2. Database surface
What it does. Brings cloud databases (RDS today, Cloud SQL next) into the
same model: DB instances are Workloads with Source="db", their metrics ride
the same usage_buckets under db_cpu_percent, db_iops, db_connections,
db_mem_percent, db_errors (ADR-030).
How it works. A collector-side Source seam (ListInstances, Series)
keeps cloud adapters thin (ADR-030 §8, ADR-033). The fixture (a deterministic
demo source: rds/payments-prod, db.t3.large → db.t3.medium, $50/mo) exists
for tests and demos via CONSIZE_DBMETRICS=fixture, but all live paths run a
real adapter — the demo seed was deleted from the live store (ADR-035). The
AWS CloudWatch adapter implements the seam against RDS (DescribeDBInstances
paged, GetMetricStatistics chunked to the 1,440-point cap), reusing the
pricing package's SigV4 signer — no AWS SDK dependency (ADR-034). It maps
CPUUtilization, FreeableMemory→mem %, Read+WriteIOPS, and DatabaseConnections;
db_errors has no CloudWatch equivalent, so the verifier treats it as
no-evidence, never FAIL. The GCP Cloud Monitoring / Cloud SQL adapter (ADR-035)
implements the same seam against sql/v1beta4 + Monitoring v3/timeSeries,
auth via a hand-rolled RS256 JWT from the service-account key (metadata-server
fallback in-cluster). It maps database/cpu/utilization and
database/memory/utilization (×100, clamped) plus database/network/connections;
IOPS and errors have no GCP equivalent → no-evidence, never FAIL. Class catalogs
are provider-scoped (RDS classes never cross-recommend GCP tiers); the
maintenance-window day convention is Monday-first per the Admin API (test-locked).
Where the code lives.
- engine/internal/dbmetrics/dbmetrics.go — the Source seam + fixture
- engine/internal/dbmetrics/cloudwatch/cloudwatch.go — live RDS adapter
- engine/internal/dbmetrics/cloudmonitoring/cloudmonitoring.go — live Cloud SQL adapter (JWT auth, day mapping, metric mapping)
- engine/internal/analysis/db.go — GCPDBCatalog (provider-scoped, price-ordered)
- engine/deploy/collector-cronjob.yaml — CONSIZE_DBMETRICS wiring (live cluster: gcp + SA key from consize-gcp Secret)
- engine/internal/store/migrations/0003_db_surface.sql — DB fields
Where you see it. DB workloads in GET /api/v1/workloads, class
recommendations (Resource="class") in /recommendations. On the live cluster
(devops-portfolio): gcp/consize-demo (db-custom-1-3840, us-central1,
window sun 03:00–04:00) ingested from Cloud Monitoring with a real
db-g1-small $29/mo recommendation; consize-collector runs
CONSIZE_DBMETRICS=gcp.
3. Analysis engine
What it does. Turns buckets into recommendations: per-workload request/limit targets for compute, and instance-class targets with headroom guarantees for databases.
How it works. Pure functions, golden-tested. Compute: daily p95 series, then
window percentiles — request = p95 × 1.2, limit = max(2×request, p99),
downsize-only (ADR-002, ADR-003). Workloads are skipped, with a recorded reason,
when: labeled excluded, in a protected namespace, flagged data-loss-risk,
under the data-minimum gate (CONSIZE_MIN_DATA_DAYS, default 5 — ADR-024), or
already optimal; half-known windows are dropped in the merge, never built on
half-truths (ADR-016). Databases: a class catalog with utilization caps
(CPU < 60%, IOPS < 60%, mem < 75%, connections < 70% — ADR-030 §9),
cheapest-fit selection, and explicit bottleneck attribution ("kept …
bottleneck X") when nothing fits.
Where the code lives.
- engine/internal/analysis/analysis.go — compute policy + skip conditions
- engine/internal/analysis/db.go — class catalog, caps, candidate search
- engine/cmd/analyze/main.go — the CronJob binary
- engine/cmd/demo/main.go — the 60-second fixture demo report
- engine/internal/fixtures/ — the 10 deterministic fixture workloads
Where you see it. The consize-analyze CronJob (*/15 * * * *); output in
GET /api/v1/recommendations?status=pending.
4. Pricing
What it does. Turns sizes into dollars, with degradation as a design principle: pricing never fails analysis.
How it works. Three layers (ADR-014): Static (shipped GKE-style default
rates), AWS (SigV4 fetch of the EC2 on-demand index, median $/vCPU-hr and
$/GiB-hr × 730 h, 24 h TTL cache), and Resilient (falls back to static on any
primary error, with a visible warning). CONSIZE_PRICING=static|aws selects
the mode. GCP pricing is deferred — same Service interface when it lands
(M1 plan). The same package exports the AWSSigner the CloudWatch adapter
reuses.
Where the code lives.
- engine/internal/pricing/pricing.go — Service interface, Static, Cached, Resilient
- engine/internal/pricing/aws.go — AWS Price List client
- engine/internal/config/config.go — env plumbing
Where you see it. The active price table rides in GET /api/v1/savings —
a fallen-back table is visible, not silent.
5. The safety engine: guarded apply → verify → auto-rollback → audit
What it does. The reason Consize is a platform and not a report. Every change is dry-run-able, guardrailed, verified, and reversible.
How it works. The k8s apply engine (internal/apply) enforces six
guardrails before any patch: store health (the audit trail must be up —
ADR-008), pending-only, exclusions win, mode policy (auto needs the
consize.savings.dev/auto-apply namespace label; everything else needs an
actor — ADR-004), a ≤30% step limit, and concurrency (one in-flight apply per
namespace, global cap). Patches go through the K8sPatcher: proportional
per-container distribution with exact-sum rounding and QoS-class preservation,
resourceVersion-guarded updates with conflict retries — one write surface
shared by apply and rollback (ADR-021). Larger reductions step down 30% at a
time; each remainder materializes as a follow-up pending recommendation that
cannot apply until the previous step verifies — the apply → verify → apply
rhythm by construction (ADR-020).
Verification (internal/verifier) is a one-shot CronJob binary (every minute,
ADR-018/ADR-048): it compares a step-scaled pre-apply baseline against the
matching post window on
workload-scoped kubelet-native signals — throttling, OOM kills, restarts,
evictions — with opt-in app-level error/p99 expressions (ADR-019). Verdicts
are three-valued: passed | failed | inconclusive. Rollback fires only on
FAIL (ADR-022); inconclusive is terminal, never silent, and never rolls back —
a metrics-path failure must not veto a good change (ADR-027). Rollback restores
pre-apply values absolutely, drifting live state included, not the inverted
diff (ADR-026). FAIL also alerts — structured logs always, Slack webhook when
CONSIZE_SLACK_WEBHOOK is set (internal/alert/alert.go).
The audit trail is INSERT-only: apply_events records planned → applied →
reverted as new rows, never edits, and in-flight state is derived (an
applied event with no verification_runs row), so a crash leaves a retryable
trail, not a lie (ADR-023).
Databases get the same philosophy with two extra guardrails
(internal/dbapply, ADR-031): a maintenance window (weekly UTC, enforced
on every real apply, fail-closed when unconfigured) and one class step per
apply (adjacent catalog class only; multi-step moves queue follow-ups).
Approval is the default: mode=auto requires
consize.savings.dev/auto-db=enabled. DB verification (verifier/db.go)
judges store buckets against the absolute analysis caps on the applied
class — the threshold is the cap, not baseline × multiplier, because a
healthy downsize legitimately raises utilization (ADR-032). The provider is a
stub (StubChanger); real writes fail with an explicit "manual class change
required" until a live provider lands.
Where the code lives.
- engine/internal/apply/apply.go, engine/internal/apply/k8s.go — guardrails, patcher, rollback
- engine/internal/dbapply/dbapply.go — DB guardrails, maintenance window, stub changer
- engine/internal/verifier/verifier.go, engine/internal/verifier/db.go — SLI comparison, DB judgment
- engine/internal/alert/alert.go — notifications
- engine/internal/store/migrations/0002_apply_audit.sql — apply_events + verification_runs
- engine/cmd/verify/main.go — the verifier CronJob binary
- engine/deploy/verify-cronjob.yaml, engine/deploy/rbac.yaml — schedule + write identity
Where you see it. POST /api/v1/recommendations/{id}/apply (dry_run /
approved / auto), GET /api/v1/applies, GET /api/v1/verification-runs, and
the consize-verify CronJob (every minute). 503 without a write
identity, structured 422 with reasons when guardrails block — never silent.
6. The API
What it does. The REST contract behind the dashboard, same origin as the UI (ADR-029).
| Endpoint | Contract |
|---|---|
GET /healthz |
liveness, no dependencies |
GET /readyz |
store (and cluster/DB engine, if configured) reachable — gates applies (ADR-008) |
GET /api/v1/workloads |
all workloads, k8s and DB |
GET /api/v1/workloads/{id} |
one workload, DB fields included |
GET /api/v1/workloads/{id}/series?metric=&days= |
chart contract: five metric names (cpu_percent, mem_percent, iops, connections, errors), surface-aware units; no-data is 200 with empty points (ADR-034 §3) |
GET /api/v1/recommendations?status=&workload_id=&limit=&offset= |
paginated (default 100, cap 500, pagination.total), ranked by savings, with risk + risk_reasons |
GET /api/v1/savings |
projected + realized + by_owner + active price table |
GET /api/v1/system/status |
Consize's own pipeline health: store status, latest telemetry bucket, freshness age, stale threshold, workload/pending counts, in-flight applies, due verifications |
POST /api/v1/recommendations/{id}/apply |
{"mode","actor"}; routes by resource — class → DB engine, cpu/memory → k8s engine (ADR-031 §6) |
GET /api/v1/applies?workload_id=&result= |
the INSERT-only apply trail, newest first |
GET /api/v1/verification-runs?apply_event_id= |
verdicts + SLI evidence |
Where the code lives.
- engine/internal/api/server.go — router + all handlers; engine/internal/api/savings.go, series.go, risk.go, status.go
- engine/cmd/api/main.go
Where you see it. Port 8080 (CONSIZE_LISTEN_PORT); the dashboard at GET /.
Self-observability addendum. The dashboard consumes /system/status to show
whether Consize's own collector/analyzer/verifier loop is fresh. The shipped
stale-data threshold is configurable with CONSIZE_DATA_STALE_AFTER and
defaults to 2 h — enough tolerance for missed 15-minute collector ticks, but
short enough to catch a broken pipeline before users mistake stale data for a
valid recommendation.
7. The UI
What it does. One dashboard for both surfaces: savings tiles, ranked recommendations with risk pills, per-workload 14-day percentile charts, the apply audit timeline — with the safety loop front and center.
How it works. The product UI is a Next.js app in the top-level ui/
directory (App Router, TypeScript, Tailwind, Recharts; ADR-036) — benchmarked
to usage.ai-class dark FinOps consoles: near-black canvas, sidebar navigation,
KPI cards with dollar deltas, status pills, uppercase micro-labels. It is a
typed read-mostly client: lib/api.ts talks to relative /api/v1, which
next.config.ts rewrites to API_UPSTREAM (default http://127.0.0.1:18099)
so one build runs against the local poke, the cluster, or a cloud backend —
same-origin, no CORS. No apply buttons by design; RBAC is enforced server-side,
not by hidden buttons. The embedded vanilla SPA in the API binary (served at
GET /, no build step) remains as the single-binary fallback — untouched, not
developed further (ADR-036).
Where the code lives.
- ui/app/ — routes (dashboard, workloads, workload detail, recommendations, audit, apply)
- ui/components/ — Sidebar.tsx, ApplyModal.tsx, ApplyTimeline.tsx, UsageChart.tsx, primitives
- ui/lib/api.ts, ui/lib/types.ts, ui/lib/format.ts — typed client + API contract types
- ui/next.config.ts — /api/v1 rewrite to API_UPSTREAM
- engine/ui/ui.go + engine/ui/app.js — the embedded fallback SPA
Where you see it. next start on the poke (against 127.0.0.1:18099) or
the cluster; the embedded fallback at GET / on a running API.
The polish pass (E1.6–E1.8, ADRs 038–040)
Three UX slices shipped 2026-08-26 as one pass (user's reorder: branding → navigation → light/dark):
- Branding (ADR-038). One
Brandcomponent is the only logo renderer (sidebar, login, loading frames): the gauge brand tile + the conSize wordmark with a capital S in the brand green. Favicon is a gauge mark (ui/app/icon.svg), page title "conSize — infrastructure rightsizing". - Navigation (ADR-039). Grouped sections (Overview / Optimize / Operations) with active-route marking; a ⌘K command palette (routes + live workload jump by name, lazily indexed on first open); on screens below lg the sidebar is an off-canvas drawer toggled from a sticky top bar (hamburger + compact brand + theme toggle), closed by backdrop, link click, or route change.
- Light/dark mode (ADR-040).
next-themesstampsdata-themeon<html>(default dark, persisted in localStorage); one:root[data-theme="light"]token block re-themes the whole console — panels, charts, pills, and the brand tile — with the accents re-tinted for contrast on white. Toggle surfaces: Dark/Light segmented control in the sidebar footer, icon buttons on the mobile top bar and the login page.
Where the code lives (polish).
- ui/components/Brand.tsx, CommandPalette.tsx, ThemeProvider.tsx, ThemeToggle.tsx
- ui/app/icon.svg (favicon), ui/app/globals.css (.brand-tile, .nav-section,
.palette*, .kbd, light token block)
- ui/components/Sidebar.tsx (sections + drawer), ui/components/Shell.tsx
(drawer state, mobile top bar, palette mount), ui/app/login/page.tsx (brand + toggle)
Where you see it. Dark is the default; the sidebar's Dark/Light control or the top-bar icon flips the whole console; ⌘K jumps anywhere from any page.
The visual standard (ADR-041, 2026-08-26) — Zorveus-standard restyle
The polish-pass look was reviewed against the reference the user set (zorveus.com — "I want that standard") and rebuilt the same day. The structural slices above (ADRs 038–040) are unchanged; their rendering now follows the measured Zorveus standard (screenshots + computed styles, not guesswork):
- Geist replaces Inter (Vercel's grotesque,
--font-geisttoken). - Pure-neutral palette — dark: pure-black canvas
#000000,#0a0a0asurfaces,#141414cards with a new--raisegradient top token so light mode re-themes card faces too; light:#f7f7f7canvas, white panels (superseding the ADR-040 slate palette). White-alpha borders everywhere. - Vercel layered shadows, 14 px card radius, ghost/solid button pairing, circular avatars, pill-badge kickers, 34 px KPI values, tighter tracking — with green as the single brand accent in both themes.
Everything flows from the :root tokens, so the restyle touched presentation
tokens plus four small edits (Geist wiring, login hero, two hover classes, KPI
gap) — no component logic or data changes. Full mapping and evidence in
docs/decisions.md ADR-041; verified by the polish smoke 33/33 and auth smoke
10/10 with the new pixel values, plus pixel-level screenshot checks against
the token values (/tmp/ui-smoke/consize-new-{login,dashboard,dashboard-light}.png).
Same-day amendment (ADR-041 Amendment 1, per the user's clarification "make it
like that standard", not copy it): card radius is now 18 px through the
--radius token (Zorveus's measured card radius, within the announced 14–18
range; modal/palette/safety radii follow the token), the primary CTA stays the
brand green, and the UI source was swept — 53 unnecessary comments removed
(narrative blocks, ADR citations), keeping only the ~50 that carry information
the code can't show (enum values, wire formats, race guards, server contracts).
Second amendment (same day, user directive — the copy "makes the whole thing
look AI-generated"): the five marketing/explainer blocks are gone from the
product UI and reserved for a future landing page (plan.md backlog): the
sidebar safety-engine card (incl. the ⌘K hint), the dashboard's 5-step safety
strip and its subtitle, the login "Infrastructure rightsizing" hero pill, and
the "Sessions are server-verified · roles viewer / operator / admin" footnote.
The functional first-run wizard message stays. Dead CSS followed (.safety*
strip block, sidebar-only .kbd); PageHead.sub is now optional and the
dashboard renders title-only. Re-verified: build clean, polish smoke 33/33,
auth smoke 10/10, screenshots re-captured + pixel-checked, and a live-DOM
absence check found none of the five strings on login or dashboard.
Third amendment (same day — the premium-dashboard refinement brief, ADR-042):
flat ultra-dark surfaces replace the layered cards (#0a0a0a panels, hairline
white/5 borders, shadows off — superseding ADR-041's #141414 + Vercel
shadow), the neon green is muted to emerald #10b981 and reserved for
the active sidebar item + positive financial metrics only, so the primary
CTA is now solid white and decorative green is swept (login glow, avatar
circle, hovers, palette selection, spinner). Brand lockup is a single-line
extrabold conSize with a minimal emerald diamond mark (no tile, no
RIGHTSIZING sub-text; favicon matches). Ink is pure white, faint is zinc-500;
micro-type is 10 px uppercase tracking-widest; neutral pills are rounded-full
white/10 with white text (semantic pass/fail tints kept); tables are roomier
(th 12/16/10, td 14/16) with right-aligned numerics — white for data, emerald
for savings (.money). Verified: build clean, polish smoke 35/35 (new
checks: single-line lockup, white primary, emerald active-nav + savings KPI,
diamond favicon), auth smoke 10/10, screenshots pixel-checked (pure-black
canvas, flat #0a0a0a cards, white glow, light #ffffff panels).
8. Savings semantics
What it does. One savings number that can be proven — projected and realized are never conflated (ADR-034 §4).
How it works. Projected = sum of SavingsMonthly over pending
recommendations. Realized = sum over recommendations whose latest apply event
is still applied and whose latest apply has a passed verification verdict.
A later reverted event, failed verification, or inconclusive verification
excludes it. by_owner breaks both down by owner label (unassigned when
absent). On the live cluster today: projected $52.59/mo, realized $0.36/mo
(the one verified apply from the M2 E2E).
Where the code lives. engine/internal/api/savings.go; realized-eligibility
combines the latest apply event with verification verdicts.
9. Risk flags on recommendations
What it does. A low | medium | high risk pill with risk_reasons on every
recommendation, so the plan ranks by savings and safety.
How it works. Computed at the API from existing data — no schema change (ADR-034 §5): low data days, saturation near the headroom caps, step distance
1 class, maintenance window not open, follow-up pending,
data-loss-riskflags.
Where the code lives. engine/internal/api/risk.go; surfaced through
GET /api/v1/recommendations.
10. RBAC and security posture
What it does. Least-privilege end to end: Consize can read only what it analyzes and update only what it applies, and it dogfoods its own advice (ADR-010).
How it works. Two ServiceAccounts in engine/deploy/rbac.yaml:
consize-reader — read-only cluster discovery for deployments/statefulsets/
daemonsets/replica sets/jobs/cronjobs/pods/namespaces when CONSIZE_NAMESPACES
is empty; consize-writer — get/list/watch/update on deployments only,
bound per namespace via RoleBindings only where Direct apply is allowed. Apply
endpoints answer 503 without a write identity. mode=auto additionally
requires the namespace label consize.savings.dev/auto-apply=enabled;
approved Direct apply requires an authenticated operator plus the RoleBinding.
IaC PR mode does not need Kubernetes write RBAC; it uses the GitHub integration
to create a reviewable source change. Exclusions always win.
Full posture in docs/security.md (13/13 RBAC matrix proven live in the E2E).
On top of that sits user authentication and server-side authorization
(ADR-037): local users with roles viewer (read-only), operator
(approve + apply), admin (everything), bcrypt passwords, revocable
Postgres sessions (7-day TTL, hashed tokens), and a login surface
(POST /api/v1/auth/login, /auth/logout, GET /auth/me). Writes are
role-gated server-side — a viewer's apply call answers 403 — and the apply
actor in the audit trail is the server-verified session email
(api:<email>), never a client-supplied string. CONSIZE_BOOTSTRAP_ADMIN
creates the first admin while the users table is empty; the poke runs
auth-enforced, the live cluster runs CONSIZE_AUTH_REQUIRED=false until
onboarding lands (E1.5). The provider seam (internal/auth.Authenticator)
keeps the door open for OIDC SSO per security.md §2.
Ad-hoc deployments get the interactive equivalent of the bootstrap env var
(ADR-037 §6 amendment, 2026-08-26): first-run admin creation —
POST /api/v1/auth/setup creates the first admin while users is empty
(409 forever after, minimum 8-character password), and /auth/me's 401
body carries needs_setup:true so the /login page renders a "Create
admin & sign in" wizard instead of the login form. There is deliberately
no default credential (no admin/admin) and no open registration: once one
admin exists, the wizard is gone forever and the only way in is a session.
Verified by TestFirstAdminSetup and a 10/10 CDP smoke against a fresh
empty-users stack; the poke's own setup answers 409 (its bootstrap admin
already exists — honest).
Where the code lives. engine/deploy/rbac.yaml, docs/security.md,
tests/e2e-live/namespace.yaml, engine/internal/auth/,
engine/internal/store/ (users/sessions, migration 0004_auth.sql),
ui/app/login/ (login + first-run wizard), ui/components/auth.tsx,
ui/components/Shell.tsx.
11. Live-cluster E2E and the living demo
What it does. Proof the loop works on a real cluster — and a demo that stays deployed.
How it works. tests/e2e-live/ is a scripted runbook (run.sh preflight →
deploy → ingest → track1 → verify1 → track2 → verify2 → rbac → status →
summary) against GKE devops-portfolio: Track 1 applies a real
recommendation to boutique/frontend and verifies PASS; Track 2 applies to a
synthetic canary, injects a regression, and proves FAIL → auto-rollback →
byte-identical restore. The run surfaced and fixed real bugs: a rollback-drift
defect (ADR-026), the no-baseline conservatism rule (ADR-027), and the durable
SLI storage requirement (ADR-028). Consize stays deployed on that cluster as a
living demo — DB fixture surface included (CONSIZE_DBMETRICS=fixture) — so
the whole loop is exercisable end to end at any time.
Where the code lives.
- tests/e2e-live/run.sh, tests/e2e-live/teardown.sh, tests/e2e-live/namespace.yaml, tests/e2e-live/canary.yaml, tests/e2e-live/out/
- Runbook and evidence: docs/e2e.md
Architecture
┌────────────────────────────────────────────────┐
│ Postgres store │
│ engine/internal/store/ (memory fallback, │
│ migrations 0001–0003, INSERT-only audit) │
└────────────────────────────────────────────────┘
▲ │ │ │ │
│ usage_buckets, workloads │ │ │ │
│ ▼ ▼ ▼ │
┌───────────────┐ ┌───────────────┐ ┌────────────────────┐
│ Collector │ │ Analysis │ │ API + embedded UI │
│ cmd/collector │ │ cmd/analyze │ │ cmd/api │
│ internal/ │──────────────▶│ internal/ │ │ internal/api │
│ collector │ │ analysis, │ │ engine/ui (GET /) │
│ dbmetrics │ │ pricing │ └──────┬─────────────┘
│ (k8s + DB │ │ (static|aws) │ │ REST (JSON)
│ sources) │ └───────────────┘ │
└───────┬───────┘ ▼
│ Prometheus / k8s API / RDS / CloudWatch ┌─────────────┐
│ │ Dashboard │
▼ └─────────────┘
┌───────────────────────────────┐ ┌──────────────────────────┐
│ Apply engine (guarded) │ │ Verifier (CronJob) │
│ internal/apply (k8s) │ │ internal/verifier │
│ internal/dbapply (DB) │◀───────│ internal/alert │
│ dry-run → guardrails → patch │ FAIL→ │ baseline vs post SLIs │
│ → follow-up steps │ rollback│ → verdict → rollback │
└───────────────────────────────┘ └──────────────────────────┘
Deployed as: consize-collector (CronJob 15m), consize-analyze (CronJob
15m), consize-api (Deployment, serves API + dashboard), consize-verify
(CronJob every minute) — manifests in engine/deploy/, write identity in
engine/deploy/rbac.yaml (ADR-010: Consize runs in the cluster it manages).
12. Teams and on-call ownership
What it does. Gives every managed workload an explicit human ownership boundary. A Team has a name, named owner, and on-call contact; workloads show their assigned team in both the inventory and detail views. The Teams view shows the workloads each team owns and lets administrators update escalation contacts.
How it works. teams is a small durable directory and workloads.team_id
is nullable. Ownership is deliberately outside collector input: a collector
refresh updates observed resource state while preserving the admin-selected
team. All signed-in users can read team data; only admins can create/edit teams
or assign/unassign a workload. A failed verification embeds the team and
on-call contact in its existing notification, so the shared webhook is
actionable today without guessing a provider-specific routing format.
Where the code lives. engine/internal/store/ (migration 0005_teams.sql
and both Store implementations), engine/internal/api/server.go (/teams and
workload-team endpoints), engine/internal/verifier/verifier.go (alert
ownership labels), and ui/app/teams/ + ui/components/views/TeamsView.tsx.
13. Incident routing and on-call ownership (in progress)
Each installation will route actionable failures to the organization's incident-management system and its Slack collaboration channel. The incident system—not a manually maintained team directory—owns schedules, escalation, acknowledgement, assignment, and resolution. Consize keeps a durable local projection so the dashboard can show the current on-call owner, incident state, provider link, and the change or rollback still required. Alerts use a stable deduplication key, are safe to retry, and include the installation, workload, namespace, failed verification signal, proposed change, rollback state, and dashboard deep link. Slack delivery uses a configured channel and Slack user/group ID for a real mention; free-form display names are not treated as routable identities. Provider credentials are referenced from Kubernetes Secrets. Incident ownership is accountability and does not grant apply permission; existing server-side RBAC remains authoritative.
First slice — Grafana-style notification routing (2026-08-28). The
verifier emits structured alert events for verification failures and rollback
failures. Each event has labels (alertname, severity, namespace,
workload, resource, surface, optional team/oncall) plus annotations
for the change, rollback state, failed signal, and dashboard link. The alert
router loads CONSIZE_ALERT_ROUTING, which mirrors Grafana's model:
notification policies match labels and route to named contact points. Slack
webhook delivery is the first integration and sends Block Kit messages with a
stable dedup key and optional Slack user-group/user mention. If no routing JSON
is configured, the legacy CONSIZE_SLACK_WEBHOOK becomes the default Slack
contact point.
Product configuration UI. /alerting is now a first-class Operations page
for configuring contact points and notification policies. Admins can save the
routing config and send a test notification; viewers can inspect it. The API
stores only the routing metadata in app_settings and rejects raw
webhook_url values, forcing Slack webhooks to be supplied as Kubernetes
Secret-backed environment variables such as CONSIZE_SLACK_WEBHOOK.
Example routing JSON:
{
"default_contact_point": "ops-slack",
"contact_points": [
{
"name": "ops-slack",
"integrations": [
{
"type": "slack",
"webhook_env": "CONSIZE_SLACK_WEBHOOK",
"channel": "#platform-oncall",
"mention": "<!subteam^S123456>"
}
]
}
],
"notification_policies": [
{
"name": "critical-verification",
"match": {
"severity": "critical",
"alertname": "ConsizeVerificationFailed"
},
"contact_point": "ops-slack"
}
]
}
Ownership UI amendment (2026-08-27)
The ownership route no longer exposes a Create team form or manual contact editing. For the installation-per-team model, ownership is deployment configuration and is displayed read-only until provider-backed onboarding and on-call routing land. The route is labeled Ownership in navigation and shows installation scope, owner, on-call context, and managed workloads.
Follow-up correction (2026-08-27): after review, the interim ownership
route was removed from navigation and /teams now redirects to the dashboard.
Workload detail no longer exposes manual ownership assignment. The backend
compatibility surface remains dormant until installation onboarding and the
remaining provider-backed incident projection/webhook work are implemented.
14. Weekly savings reports
What it does. Gives stakeholders a periodic savings pulse without turning Consize back into a passive reporting tool. Admins can enable a weekly Slack digest and choose the default report range. Anyone with read access can generate an on-demand report for the past 7, 14, or 30 days and download it as a PDF.
How it works. internal/report builds the report from the existing source
of truth: pending recommendations, apply events, verification runs, and the
latest telemetry bucket. Realized savings in the report count recommendations
whose apply was verified during the selected period. Pending opportunity comes
from current pending recommendations. Rollbacks and failed/inconclusive
verifications are counted from the audit trail. Delivery uses the configured
Alerting contact point/policy path, so Slack secrets stay in Kubernetes and
only reporting metadata is stored in app_settings.
Where the code lives. engine/internal/report/, engine/internal/api/reports.go,
engine/cmd/report/, engine/deploy/report-cronjob.yaml, and the Next.js
/reports console page. API surface:
GET /api/v1/reports/configPUT /api/v1/reports/configGET /api/v1/reports/savings?range=7d|14d|30d&format=json|pdfPOST /api/v1/reports/send
The first live rollout used images api:consize-weekly-report-20260828 and
report:consize-weekly-report-20260828; live smoke verified JSON generation,
PDF generation, Slack send-now, and the disabled CronJob no-op path.
15. Cloud-waste opportunities and IaC PR workflow
What it does. Extends Consize beyond workload rightsizing into common cloud-cost leaks: unattached storage volumes, idle load balancers, unused NAT gateways, and stopped instances whose attached resources are still billed. Findings are shown as cost opportunities with evidence, risk, estimated monthly savings, and a reviewable next action.
How it works. internal/costscan defines a provider Source seam. The
live GKE deployment runs CONSIZE_COSTSCAN=gcp with the same consize-gcp
service-account key used by the Cloud SQL collector. Manual scans from the API
use that same environment-selected source; unset/none means disabled, never
implicit fixtures. The GCP source queries the Compute Engine API for detached
Persistent Disks and stopped VMs whose Persistent Disks still accrue cost. The
fixture source remains for explicit local demos and tests only. Scan results
are upserted into cost_opportunities, keyed by
provider/account/region/type/id, so repeated scans refresh evidence without
duplicating rows. Operators can either prepare an audited IaC PR plan/diff from
an opportunity, keeping cloud changes reviewable and avoiding configuration
drift, or use Direct cleanup where the provider deleter has a safety re-check.
The first Direct cleanup implementation supports GCP unattached Persistent
Disks only: Consize re-reads the disk, refuses if it is attached or no longer
READY, then requests deletion and marks the opportunity resolved. Direct
cleanup writes an insert-only cost_actions trail (requested → applied,
dry_run, or failed) so resolved cloud-waste items remain visible after they
leave the active opportunities list. Stopped VM, load balancer, and NAT cleanup
remain PR-first until provider-specific safety checks are implemented.
The IaC PR workflow is intentionally not exclusive to cloud waste. Normal rightsizing recommendations also support a PR delivery path: operators can either run a direct Consize apply for non-IaC workloads and convenience flows, or open a PR for teams whose repository is the source of truth. The MVP stores a planned branch/title/body/diff without requiring GitHub credentials. GitHub configuration is installation-wide, not recommendation- specific: admins configure a GitHub organization/account, token environment reference, and the repositories Consize may read/write. A monorepo is represented as one authorized repository with a root path; enterprise teams can add multiple repositories. Specific source file and resource selection happens in the PR workflow today and should later be inferred from annotations, repo scan, Terraform state, ArgoCD Application metadata, Helm release metadata, or ownership metadata — not from the GitHub connection page.
When GitHub credentials are present, Consize can create a branch, commit the source change, and open a draft GitHub PR. Supported recommendation PR writers:
- Terraform files (
.tf,.tf.json) patch the selected Terraform resource address. - Kubernetes YAML files (
.yaml,.yml) patch the matching workload manifest by kind/name/namespace. - Kustomize/GitOps apps should target repo-owned patch files or overlays first.
If no overlay exists yet, they can target a vendored base manifest that is
committed to the team's repo. For example, a Google Online Boutique install
can vendor the upstream release, keep common overrides in
kubernetes/boutique/overlays/dev/, and still let Consize open PRs againstkubernetes/boutique/base/kubernetes-manifests.yamlfor workloads that do not have an overlay patch yet. - Helm values are recognized as an IaC source type, but opening a Helm-values PR requires an explicit values key mapping before Consize can safely edit it; charts differ too much to infer this blindly.
The source path must point at a concrete file, not a directory. For monorepos,
configure the repository root path as the folder such as infra/terraform or
clusters/prod, then use a file path such as workloads.tf,
apps/api/deployment.yaml, or values.yaml in the PR workflow.
Where the code lives. engine/internal/costscan/, engine/cmd/costscan/,
store migrations 0007_cost_opportunities.sql,
0008_iac_plans_for_recommendations.sql, and 0009_cost_actions.sql, API routes in
engine/internal/api/cost.go, deploy manifest
engine/deploy/costscan-cronjob.yaml, and the Next.js /cost console page.
API surface:
GET /api/v1/cost-opportunitiesPOST /api/v1/cost-opportunities/scanPOST /api/v1/cost-opportunities/{id}/applyPOST /api/v1/cost-opportunities/{id}/iac-prPOST /api/v1/recommendations/{id}/iac-prGET /api/v1/integrations/githubPUT /api/v1/integrations/github
Current MVP scope: persisted findings, manual scan, daily GCP CronJob, and IaC
PR generation for cloud waste and rightsizing recommendations. When the
configured GitHub token is available, Consize creates a branch, updates the
mapped source file, and opens a draft GitHub PR if the selected file can be
patched safely. The GitHub integration stores metadata only; tokens stay outside
Postgres as Kubernetes Secret-backed environment variables such as
CONSIZE_GITHUB_TOKEN. Future hardening: traffic-backed idle load balancer and
Cloud NAT detection, repository ownership discovery, ArgoCD/Helm/Kustomize
resolvers, GitLab PR creation, and snooze/exempt policy before any automated
cleanup.