Skip to content

fix(coroot): price Hetzner nodes correctly instead of the ~16x GCP-C4 fallback - #1783

Merged
devantler merged 2 commits into
mainfrom
claude/fervent-shaw-fcc3b9
Jun 5, 2026
Merged

devantler merged 2 commits into
mainfrom
claude/fervent-shaw-fcc3b9

Conversation

@devantler

Copy link
Copy Markdown
Contributor

Problem

Coroot's node cost view is wildly inflated for our Hetzner fleet — ~$122/mo per 4 vCPU / 8 GB node and ~$795/mo total, vs. the real Hetzner bill of ~€43 ≈ $50/mo:

Coroot (before) Reality
CX33 node (4 vCPU / 8 GB) ~$122/mo €6.49 ≈ $7.55/mo
Fleet total $794/mo ~$50/mo

Root cause

Coroot ships embedded pricing only for AWS, GCP and Azure. For nodes on any other cloud it falls back to GCP C4, us-central1 on‑demand rates (db/custom_cloud_pricing.go defaultCustomCloudPricing: per_cpu_core 0.03465, per_memory_gb 0.003938 $/h). That reproduces the screenshot exactly: (4 × 0.03465 + 7.55 × 0.003938) × 730 ≈ $122.9/mo. Idle cost and every per‑app cost allocation inherit the same ~16× inflation.

Fix

A prod-only reconcile CronJob sets the project's Custom Cloud Pricing to Hetzner's CX33 economics — the same source of truth as the existing OpenCost costModel (opencost/helm-release.yaml), so the two cost views agree to the cent:

CX33 = €6.49/mo (4 vCPU / 8 GB) · ECB EUR→USD 1.1637 · 50/50 CPU:RAM split
  per_cpu_core  = €6.49 × 1.1637 × 0.5 / 4 vCPU / 730 = $0.0012933 /vCPU-h
  per_memory_gb = €6.49 × 1.1637 × 0.5 / 8 GB  / 730 = $0.0006466 /GB-h

Coroot prices a node as cpu × per_cpu_core + memGB × per_memory_gb, so a CX33 now reads ~$7.55/mo and the fleet ~$50/mo.

Why a CronJob hitting the API (not the CR or a UI click)

  • The coroot-operator's ProjectSpec has no pricing field, and Coroot exposes Custom Cloud Pricing only in the UI — but it's settable via POST /api/project/{id}/custom_cloud_pricing.
  • The value persists: Coroot's config/bootstrap.go merges only api keys / notifications / categories onto existing project settings and never touches CustomCloudPricing, so an operator reconcile or pod restart won't reset it.
  • authAnonymousRole: Admin (set on the CR) means an in-cluster request that bypasses oauth2-proxy is already Admin → no credential needed.
  • A CronJob (not a one-shot Job — same lesson as umami-provision-tenants) makes it a self-healing reconcile loop: sets the price on first install, re-asserts it after a DR restore / PVC wipe that reverts the project to the GCP-C4 default, and corrects UI drift. It's idempotent — a steady-state tick is two GETs and no write.
  • Prod-only: the platform project and real cloud spend exist only on Hetzner; the base CR has no projects, so local/CI have only Coroot's auto default project and nothing to correct (same split as the Slack wiring).

Coroot vs. OpenCost (the linked issues)

Coroot's cost model is per-CPU + per-GB only — it cannot do the per-instance Hetzner-API lookup that OpenCost's native hcloud provider (opencost#3710, opencost#3523) can. A blended CX rate is therefore the correct-by-design approach for Coroot. The whole CX line is a fixed 1 vCPU : 2 GB ratio, so this single per-unit rate is exact for our six CX33 control-plane/worker nodes; Hetzner's mild economy-of-scale (CX23 €2.00/vCPU → CX53 €1.41/vCPU) makes it a few % off for other autoscale sizes — still ~16× closer than the GCP fallback, and identical to what OpenCost already reports.

Validation

  • kubectl kustomize builds clean for both providers/hetzner/infrastructure (CronJob present) and providers/docker/infrastructure (correctly absent).
  • CronJob passes kubectl apply --dry-run=client; the embedded script passes sh -n and its parse / idempotency logic was simulated against realistic /api/user and /custom_cloud_pricing payloads (GCP-fallback → POST, already-correct → SKIP, drift → POST).
  • ksail --config ksail.prod.yaml workload validate: 79/80 groups pass. The single failure (providers/hetzner/infrastructure) is the pre-existing stale coroot.com/v1 CRDs-catalog schema rejecting spec.projects[].notificationIntegrations on the already-committed Coroot CR — unrelated to this change (ksail.prod.yaml is protected and lacks the skipKinds:[Coroot] workaround ksail.yaml carries).

Note

No live-cluster steps were run. The price applies on the next CronJob tick (≤30 min) after merge + reconcile; to apply immediately: kubectl create job -n observability --from=cronjob/coroot-custom-cloud-pricing coroot-pricing-now.

🤖 Generated with Claude Code

… fallback

Coroot ships embedded pricing only for AWS, GCP and Azure. For nodes on any
other cloud it falls back to "GCP C4, us-central1" on-demand rates
(per_cpu_core $0.03465/h, per_memory_gb $0.003938/h), which prices each
4 vCPU / 8 GB Hetzner node at ~$122/mo and the fleet at ~$795/mo — about 16x
the real ~$50/mo bill, and the same inflation flows into idle cost and every
per-app cost allocation.

The coroot-operator's ProjectSpec has no pricing field and Coroot exposes
Custom Cloud Pricing only in the UI, but it is settable via
POST /api/project/{id}/custom_cloud_pricing and persists (config bootstrap
merges only api keys / notifications / categories onto existing settings and
never touches CustomCloudPricing). Add a prod-only reconcile CronJob that
asserts the project's per-CPU / per-GB rate to Hetzner's CX33 economics, using
the exact same source of truth as the OpenCost costModel so the two cost views
agree: $0.0012933/vCPU-h and $0.0006466/GB-h (CX33 EUR6.49/mo, FX 1.1637,
50/50 split, /730). A CX33 then reads ~$7.55/mo and the fleet ~$50/mo.

authAnonymousRole=Admin makes an in-cluster request Admin (no credential), and
the CronJob is a self-healing, idempotent reconcile loop (sets on install,
re-asserts after a DR restore that reverts to the default, corrects UI drift;
a steady-state tick is two GETs and no write). Prod-only because the platform
project and real spend exist only on Hetzner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@botantler-1
botantler-1 Bot enabled auto-merge June 5, 2026 16:20
@devantler
devantler disabled auto-merge June 5, 2026 16:22
@devantler
devantler merged commit 7b715f9 into main Jun 5, 2026
8 of 9 checks passed
@devantler
devantler deleted the claude/fervent-shaw-fcc3b9 branch June 5, 2026 16:22
@github-project-automation github-project-automation Bot moved this from 🫴 Ready to ✅ Done in 🌊 Project Board Jun 5, 2026
@botantler-1

botantler-1 Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.34.2 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@botantler-1 botantler-1 Bot added the released label Jun 5, 2026
devantler added a commit that referenced this pull request Jun 5, 2026
Resolve the resources-list conflict in the hetzner infrastructure kustomization by keeping both additions: the coroot custom-cloud-pricing cronjob (from main, #1783) and volume-snapshot-classes (this branch). Verified: 4 kustomize overlays build, ksail validate clean except the pre-existing Coroot catalog issue, and the Velero CSI patch + Longhorn dependsOn + snapshotter var all still render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

This branch had an error being deployed

1 failed deployment
ci — eaa13d08 Deployed Jun 5, 2026 by devantler via 🧪 System Test #2118
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

1 participant