cluster

Infrastructure files for Nordgedanken and Midnightthoughts.
git clone git://archive.git.mtrnord.blog/MTRNord/cluster.git
Log | Files | Refs | README

commit 4dc29e012aaff938cd504fe6852860584160fb91
parent df55da1c9a257f1d3b591a19b598d3177f53cb11
Author: MTRNord <MTRNord@users.noreply.github.com>
Date:   Thu, 26 Feb 2026 11:39:46 +0100

use proper csv

Signed-off-by: MTRNord <MTRNord@users.noreply.github.com>

Diffstat:
Mapps/talos_cluster/opencost/hetzner-pricing.yaml | 156++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Ascripts/hetzner_pricing.csv | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascripts/hetzner_pricing_generator.py | 230+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 506 insertions(+), 18 deletions(-)

diff --git a/apps/talos_cluster/opencost/hetzner-pricing.yaml b/apps/talos_cluster/opencost/hetzner-pricing.yaml @@ -5,21 +5,141 @@ metadata: namespace: opencost data: hetzner.csv: | - Region,Instance Type,vCPUs,RAM (GiB),Storage (GB),GPU,Cost per Hour,PricingUnit - fsn1,cax11,2,4,40,0,0.00742,Hour - hel1,cax11,2,4,40,0,0.00742,Hour - nbg1,cax11,2,4,40,0,0.00742,Hour - hel1,cax21,4,8,80,0,0.01321,Hour - fsn1,cax21,4,8,80,0,0.01321,Hour - nbg1,cax21,4,8,80,0,0.01321,Hour - fsn1,cax31,8,16,160,0,0.02643,Hour - hel1,cax31,8,16,160,0,0.02643,Hour - nbg1,cax31,8,16,160,0,0.02643,Hour - hel1,cax41,16,32,320,0,0.05000,Hour - fsn1,cax41,16,32,320,0,0.05000,Hour - nbg1,cax41,16,32,320,0,0.05000,Hour - hel1,cpx11,2,2,40,0,0.00614,Hour - hel1,cpx21,3,4,80,0,0.01007,Hour - hel1,cpx31,4,8,160,0,0.01793,Hour - hel1,cpx41,8,16,240,0,0.03236,Hour - hel1,cpx51,16,32,360,0,0.06000,Hour + # Hetzner Cloud Pricing for OpenCost CSV Provider + # Generated: 2026-02-26T11:39:02.783509 + # Source: Hetzner Cloud API (https://api.hetzner.cloud/v1/pricing) + # Prices in EUR per hour (net, excluding VAT) + # + EndTimestamp,InstanceID,Region,AssetClass,InstanceIDField,InstanceType,MarketPriceHourly,Version + ,cpx11,ash,node,metadata.labels.node.kubernetes.io/instance-type,cpx11,0.007200,2026.02 + ,cpx11,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx11,0.006300,2026.02 + ,cpx11,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx11,0.006300,2026.02 + ,cpx11,hil,node,metadata.labels.node.kubernetes.io/instance-type,cpx11,0.007200,2026.02 + ,cpx11,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx11,0.006300,2026.02 + ,cpx11,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx11,0.011900,2026.02 + ,cpx21,ash,node,metadata.labels.node.kubernetes.io/instance-type,cpx21,0.014500,2026.02 + ,cpx21,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx21,0.011200,2026.02 + ,cpx21,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx21,0.011200,2026.02 + ,cpx21,hil,node,metadata.labels.node.kubernetes.io/instance-type,cpx21,0.014500,2026.02 + ,cpx21,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx21,0.011200,2026.02 + ,cpx21,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx21,0.022600,2026.02 + ,cpx31,ash,node,metadata.labels.node.kubernetes.io/instance-type,cpx31,0.025700,2026.02 + ,cpx31,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx31,0.021100,2026.02 + ,cpx31,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx31,0.021100,2026.02 + ,cpx31,hil,node,metadata.labels.node.kubernetes.io/instance-type,cpx31,0.025700,2026.02 + ,cpx31,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx31,0.021100,2026.02 + ,cpx31,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx31,0.039700,2026.02 + ,cpx41,ash,node,metadata.labels.node.kubernetes.io/instance-type,cpx41,0.048100,2026.02 + ,cpx41,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx41,0.040900,2026.02 + ,cpx41,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx41,0.040900,2026.02 + ,cpx41,hil,node,metadata.labels.node.kubernetes.io/instance-type,cpx41,0.048100,2026.02 + ,cpx41,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx41,0.040900,2026.02 + ,cpx41,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx41,0.068300,2026.02 + ,cpx51,ash,node,metadata.labels.node.kubernetes.io/instance-type,cpx51,0.096200,2026.02 + ,cpx51,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx51,0.087200,2026.02 + ,cpx51,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx51,0.087200,2026.02 + ,cpx51,hil,node,metadata.labels.node.kubernetes.io/instance-type,cpx51,0.096200,2026.02 + ,cpx51,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx51,0.087200,2026.02 + ,cpx51,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx51,0.123200,2026.02 + ,cax11,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cax11,0.005300,2026.02 + ,cax11,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cax11,0.005300,2026.02 + ,cax11,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cax11,0.005300,2026.02 + ,cax21,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cax21,0.009600,2026.02 + ,cax21,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cax21,0.009600,2026.02 + ,cax21,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cax21,0.009600,2026.02 + ,cax31,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cax31,0.019200,2026.02 + ,cax31,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cax31,0.019200,2026.02 + ,cax31,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cax31,0.019200,2026.02 + ,cax41,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cax41,0.038400,2026.02 + ,cax41,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cax41,0.038400,2026.02 + ,cax41,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cax41,0.038400,2026.02 + ,ccx13,ash,node,metadata.labels.node.kubernetes.io/instance-type,ccx13,0.020900,2026.02 + ,ccx13,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,ccx13,0.019200,2026.02 + ,ccx13,hel1,node,metadata.labels.node.kubernetes.io/instance-type,ccx13,0.019200,2026.02 + ,ccx13,hil,node,metadata.labels.node.kubernetes.io/instance-type,ccx13,0.020900,2026.02 + ,ccx13,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,ccx13,0.019200,2026.02 + ,ccx13,sin,node,metadata.labels.node.kubernetes.io/instance-type,ccx13,0.033700,2026.02 + ,ccx23,ash,node,metadata.labels.node.kubernetes.io/instance-type,ccx23,0.041700,2026.02 + ,ccx23,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,ccx23,0.038400,2026.02 + ,ccx23,hel1,node,metadata.labels.node.kubernetes.io/instance-type,ccx23,0.038400,2026.02 + ,ccx23,hil,node,metadata.labels.node.kubernetes.io/instance-type,ccx23,0.041700,2026.02 + ,ccx23,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,ccx23,0.038400,2026.02 + ,ccx23,sin,node,metadata.labels.node.kubernetes.io/instance-type,ccx23,0.063100,2026.02 + ,ccx33,ash,node,metadata.labels.node.kubernetes.io/instance-type,ccx33,0.080200,2026.02 + ,ccx33,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,ccx33,0.076900,2026.02 + ,ccx33,hel1,node,metadata.labels.node.kubernetes.io/instance-type,ccx33,0.076900,2026.02 + ,ccx33,hil,node,metadata.labels.node.kubernetes.io/instance-type,ccx33,0.080200,2026.02 + ,ccx33,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,ccx33,0.076900,2026.02 + ,ccx33,sin,node,metadata.labels.node.kubernetes.io/instance-type,ccx33,0.119200,2026.02 + ,ccx43,ash,node,metadata.labels.node.kubernetes.io/instance-type,ccx43,0.160300,2026.02 + ,ccx43,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,ccx43,0.153800,2026.02 + ,ccx43,hel1,node,metadata.labels.node.kubernetes.io/instance-type,ccx43,0.153800,2026.02 + ,ccx43,hil,node,metadata.labels.node.kubernetes.io/instance-type,ccx43,0.160300,2026.02 + ,ccx43,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,ccx43,0.153800,2026.02 + ,ccx43,sin,node,metadata.labels.node.kubernetes.io/instance-type,ccx43,0.219600,2026.02 + ,ccx53,ash,node,metadata.labels.node.kubernetes.io/instance-type,ccx53,0.320500,2026.02 + ,ccx53,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,ccx53,0.307700,2026.02 + ,ccx53,hel1,node,metadata.labels.node.kubernetes.io/instance-type,ccx53,0.307700,2026.02 + ,ccx53,hil,node,metadata.labels.node.kubernetes.io/instance-type,ccx53,0.320500,2026.02 + ,ccx53,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,ccx53,0.307700,2026.02 + ,ccx53,sin,node,metadata.labels.node.kubernetes.io/instance-type,ccx53,0.471200,2026.02 + ,ccx63,ash,node,metadata.labels.node.kubernetes.io/instance-type,ccx63,0.480800,2026.02 + ,ccx63,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,ccx63,0.461500,2026.02 + ,ccx63,hel1,node,metadata.labels.node.kubernetes.io/instance-type,ccx63,0.461500,2026.02 + ,ccx63,hil,node,metadata.labels.node.kubernetes.io/instance-type,ccx63,0.480800,2026.02 + ,ccx63,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,ccx63,0.461500,2026.02 + ,ccx63,sin,node,metadata.labels.node.kubernetes.io/instance-type,ccx63,0.772400,2026.02 + ,cpx12,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx12,0.009600,2026.02 + ,cpx22,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx22,0.009600,2026.02 + ,cpx22,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx22,0.009600,2026.02 + ,cpx22,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx22,0.009600,2026.02 + ,cpx22,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx22,0.019200,2026.02 + ,cpx32,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx32,0.016800,2026.02 + ,cpx32,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx32,0.016800,2026.02 + ,cpx32,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx32,0.016800,2026.02 + ,cpx32,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx32,0.040000,2026.02 + ,cpx42,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx42,0.031200,2026.02 + ,cpx42,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx42,0.031200,2026.02 + ,cpx42,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx42,0.031200,2026.02 + ,cpx42,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx42,0.068900,2026.02 + ,cpx52,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx52,0.044900,2026.02 + ,cpx52,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx52,0.044900,2026.02 + ,cpx52,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx52,0.044900,2026.02 + ,cpx52,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx52,0.096100,2026.02 + ,cpx62,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx62,0.061700,2026.02 + ,cpx62,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx62,0.061700,2026.02 + ,cpx62,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx62,0.061700,2026.02 + ,cpx62,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx62,0.123400,2026.02 + ,cx23,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cx23,0.004800,2026.02 + ,cx23,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cx23,0.004800,2026.02 + ,cx23,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cx23,0.004800,2026.02 + ,cx33,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cx33,0.008000,2026.02 + ,cx33,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cx33,0.008000,2026.02 + ,cx33,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cx33,0.008000,2026.02 + ,cx43,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cx43,0.014400,2026.02 + ,cx43,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cx43,0.014400,2026.02 + ,cx43,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cx43,0.014400,2026.02 + ,cx53,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cx53,0.027200,2026.02 + ,cx53,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cx53,0.027200,2026.02 + ,cx53,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cx53,0.027200,2026.02 + ,lb11,fsn1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb11,0.008800,2026.02 + ,lb11,nbg1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb11,0.008800,2026.02 + ,lb11,hel1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb11,0.008800,2026.02 + ,lb11,ash,node,metadata.labels.load-balancer.hetzner.cloud/type,lb11,0.008800,2026.02 + ,lb11,hil,node,metadata.labels.load-balancer.hetzner.cloud/type,lb11,0.008800,2026.02 + ,lb11,sin,node,metadata.labels.load-balancer.hetzner.cloud/type,lb11,0.008800,2026.02 + ,lb21,fsn1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb21,0.025300,2026.02 + ,lb21,nbg1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb21,0.025300,2026.02 + ,lb21,hel1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb21,0.025300,2026.02 + ,lb21,ash,node,metadata.labels.load-balancer.hetzner.cloud/type,lb21,0.025300,2026.02 + ,lb21,hil,node,metadata.labels.load-balancer.hetzner.cloud/type,lb21,0.025300,2026.02 + ,lb21,sin,node,metadata.labels.load-balancer.hetzner.cloud/type,lb21,0.025300,2026.02 + ,lb31,fsn1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb31,0.049500,2026.02 + ,lb31,nbg1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb31,0.049500,2026.02 + ,lb31,hel1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb31,0.049500,2026.02 + ,lb31,ash,node,metadata.labels.load-balancer.hetzner.cloud/type,lb31,0.049500,2026.02 + ,lb31,hil,node,metadata.labels.load-balancer.hetzner.cloud/type,lb31,0.049500,2026.02 + ,lb31,sin,node,metadata.labels.load-balancer.hetzner.cloud/type,lb31,0.049500,2026.02 + ,ssd,fsn1,pv,spec.storageClassName,hcloud-volumes,0.00006027,2026.02 + ,ssd,nbg1,pv,spec.storageClassName,hcloud-volumes,0.00006027,2026.02 + ,ssd,hel1,pv,spec.storageClassName,hcloud-volumes,0.00006027,2026.02 diff --git a/scripts/hetzner_pricing.csv b/scripts/hetzner_pricing.csv @@ -0,0 +1,138 @@ +# Hetzner Cloud Pricing for OpenCost CSV Provider +# Generated: 2026-02-26T11:39:02.783509 +# Source: Hetzner Cloud API (https://api.hetzner.cloud/v1/pricing) +# Prices in EUR per hour (net, excluding VAT) +# +EndTimestamp,InstanceID,Region,AssetClass,InstanceIDField,InstanceType,MarketPriceHourly,Version +,cpx11,ash,node,metadata.labels.node.kubernetes.io/instance-type,cpx11,0.007200,2026.02 +,cpx11,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx11,0.006300,2026.02 +,cpx11,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx11,0.006300,2026.02 +,cpx11,hil,node,metadata.labels.node.kubernetes.io/instance-type,cpx11,0.007200,2026.02 +,cpx11,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx11,0.006300,2026.02 +,cpx11,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx11,0.011900,2026.02 +,cpx21,ash,node,metadata.labels.node.kubernetes.io/instance-type,cpx21,0.014500,2026.02 +,cpx21,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx21,0.011200,2026.02 +,cpx21,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx21,0.011200,2026.02 +,cpx21,hil,node,metadata.labels.node.kubernetes.io/instance-type,cpx21,0.014500,2026.02 +,cpx21,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx21,0.011200,2026.02 +,cpx21,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx21,0.022600,2026.02 +,cpx31,ash,node,metadata.labels.node.kubernetes.io/instance-type,cpx31,0.025700,2026.02 +,cpx31,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx31,0.021100,2026.02 +,cpx31,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx31,0.021100,2026.02 +,cpx31,hil,node,metadata.labels.node.kubernetes.io/instance-type,cpx31,0.025700,2026.02 +,cpx31,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx31,0.021100,2026.02 +,cpx31,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx31,0.039700,2026.02 +,cpx41,ash,node,metadata.labels.node.kubernetes.io/instance-type,cpx41,0.048100,2026.02 +,cpx41,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx41,0.040900,2026.02 +,cpx41,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx41,0.040900,2026.02 +,cpx41,hil,node,metadata.labels.node.kubernetes.io/instance-type,cpx41,0.048100,2026.02 +,cpx41,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx41,0.040900,2026.02 +,cpx41,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx41,0.068300,2026.02 +,cpx51,ash,node,metadata.labels.node.kubernetes.io/instance-type,cpx51,0.096200,2026.02 +,cpx51,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx51,0.087200,2026.02 +,cpx51,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx51,0.087200,2026.02 +,cpx51,hil,node,metadata.labels.node.kubernetes.io/instance-type,cpx51,0.096200,2026.02 +,cpx51,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx51,0.087200,2026.02 +,cpx51,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx51,0.123200,2026.02 +,cax11,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cax11,0.005300,2026.02 +,cax11,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cax11,0.005300,2026.02 +,cax11,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cax11,0.005300,2026.02 +,cax21,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cax21,0.009600,2026.02 +,cax21,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cax21,0.009600,2026.02 +,cax21,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cax21,0.009600,2026.02 +,cax31,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cax31,0.019200,2026.02 +,cax31,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cax31,0.019200,2026.02 +,cax31,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cax31,0.019200,2026.02 +,cax41,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cax41,0.038400,2026.02 +,cax41,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cax41,0.038400,2026.02 +,cax41,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cax41,0.038400,2026.02 +,ccx13,ash,node,metadata.labels.node.kubernetes.io/instance-type,ccx13,0.020900,2026.02 +,ccx13,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,ccx13,0.019200,2026.02 +,ccx13,hel1,node,metadata.labels.node.kubernetes.io/instance-type,ccx13,0.019200,2026.02 +,ccx13,hil,node,metadata.labels.node.kubernetes.io/instance-type,ccx13,0.020900,2026.02 +,ccx13,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,ccx13,0.019200,2026.02 +,ccx13,sin,node,metadata.labels.node.kubernetes.io/instance-type,ccx13,0.033700,2026.02 +,ccx23,ash,node,metadata.labels.node.kubernetes.io/instance-type,ccx23,0.041700,2026.02 +,ccx23,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,ccx23,0.038400,2026.02 +,ccx23,hel1,node,metadata.labels.node.kubernetes.io/instance-type,ccx23,0.038400,2026.02 +,ccx23,hil,node,metadata.labels.node.kubernetes.io/instance-type,ccx23,0.041700,2026.02 +,ccx23,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,ccx23,0.038400,2026.02 +,ccx23,sin,node,metadata.labels.node.kubernetes.io/instance-type,ccx23,0.063100,2026.02 +,ccx33,ash,node,metadata.labels.node.kubernetes.io/instance-type,ccx33,0.080200,2026.02 +,ccx33,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,ccx33,0.076900,2026.02 +,ccx33,hel1,node,metadata.labels.node.kubernetes.io/instance-type,ccx33,0.076900,2026.02 +,ccx33,hil,node,metadata.labels.node.kubernetes.io/instance-type,ccx33,0.080200,2026.02 +,ccx33,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,ccx33,0.076900,2026.02 +,ccx33,sin,node,metadata.labels.node.kubernetes.io/instance-type,ccx33,0.119200,2026.02 +,ccx43,ash,node,metadata.labels.node.kubernetes.io/instance-type,ccx43,0.160300,2026.02 +,ccx43,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,ccx43,0.153800,2026.02 +,ccx43,hel1,node,metadata.labels.node.kubernetes.io/instance-type,ccx43,0.153800,2026.02 +,ccx43,hil,node,metadata.labels.node.kubernetes.io/instance-type,ccx43,0.160300,2026.02 +,ccx43,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,ccx43,0.153800,2026.02 +,ccx43,sin,node,metadata.labels.node.kubernetes.io/instance-type,ccx43,0.219600,2026.02 +,ccx53,ash,node,metadata.labels.node.kubernetes.io/instance-type,ccx53,0.320500,2026.02 +,ccx53,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,ccx53,0.307700,2026.02 +,ccx53,hel1,node,metadata.labels.node.kubernetes.io/instance-type,ccx53,0.307700,2026.02 +,ccx53,hil,node,metadata.labels.node.kubernetes.io/instance-type,ccx53,0.320500,2026.02 +,ccx53,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,ccx53,0.307700,2026.02 +,ccx53,sin,node,metadata.labels.node.kubernetes.io/instance-type,ccx53,0.471200,2026.02 +,ccx63,ash,node,metadata.labels.node.kubernetes.io/instance-type,ccx63,0.480800,2026.02 +,ccx63,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,ccx63,0.461500,2026.02 +,ccx63,hel1,node,metadata.labels.node.kubernetes.io/instance-type,ccx63,0.461500,2026.02 +,ccx63,hil,node,metadata.labels.node.kubernetes.io/instance-type,ccx63,0.480800,2026.02 +,ccx63,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,ccx63,0.461500,2026.02 +,ccx63,sin,node,metadata.labels.node.kubernetes.io/instance-type,ccx63,0.772400,2026.02 +,cpx12,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx12,0.009600,2026.02 +,cpx22,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx22,0.009600,2026.02 +,cpx22,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx22,0.009600,2026.02 +,cpx22,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx22,0.009600,2026.02 +,cpx22,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx22,0.019200,2026.02 +,cpx32,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx32,0.016800,2026.02 +,cpx32,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx32,0.016800,2026.02 +,cpx32,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx32,0.016800,2026.02 +,cpx32,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx32,0.040000,2026.02 +,cpx42,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx42,0.031200,2026.02 +,cpx42,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx42,0.031200,2026.02 +,cpx42,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx42,0.031200,2026.02 +,cpx42,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx42,0.068900,2026.02 +,cpx52,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx52,0.044900,2026.02 +,cpx52,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx52,0.044900,2026.02 +,cpx52,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx52,0.044900,2026.02 +,cpx52,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx52,0.096100,2026.02 +,cpx62,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cpx62,0.061700,2026.02 +,cpx62,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cpx62,0.061700,2026.02 +,cpx62,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cpx62,0.061700,2026.02 +,cpx62,sin,node,metadata.labels.node.kubernetes.io/instance-type,cpx62,0.123400,2026.02 +,cx23,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cx23,0.004800,2026.02 +,cx23,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cx23,0.004800,2026.02 +,cx23,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cx23,0.004800,2026.02 +,cx33,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cx33,0.008000,2026.02 +,cx33,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cx33,0.008000,2026.02 +,cx33,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cx33,0.008000,2026.02 +,cx43,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cx43,0.014400,2026.02 +,cx43,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cx43,0.014400,2026.02 +,cx43,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cx43,0.014400,2026.02 +,cx53,fsn1,node,metadata.labels.node.kubernetes.io/instance-type,cx53,0.027200,2026.02 +,cx53,hel1,node,metadata.labels.node.kubernetes.io/instance-type,cx53,0.027200,2026.02 +,cx53,nbg1,node,metadata.labels.node.kubernetes.io/instance-type,cx53,0.027200,2026.02 +,lb11,fsn1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb11,0.008800,2026.02 +,lb11,nbg1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb11,0.008800,2026.02 +,lb11,hel1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb11,0.008800,2026.02 +,lb11,ash,node,metadata.labels.load-balancer.hetzner.cloud/type,lb11,0.008800,2026.02 +,lb11,hil,node,metadata.labels.load-balancer.hetzner.cloud/type,lb11,0.008800,2026.02 +,lb11,sin,node,metadata.labels.load-balancer.hetzner.cloud/type,lb11,0.008800,2026.02 +,lb21,fsn1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb21,0.025300,2026.02 +,lb21,nbg1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb21,0.025300,2026.02 +,lb21,hel1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb21,0.025300,2026.02 +,lb21,ash,node,metadata.labels.load-balancer.hetzner.cloud/type,lb21,0.025300,2026.02 +,lb21,hil,node,metadata.labels.load-balancer.hetzner.cloud/type,lb21,0.025300,2026.02 +,lb21,sin,node,metadata.labels.load-balancer.hetzner.cloud/type,lb21,0.025300,2026.02 +,lb31,fsn1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb31,0.049500,2026.02 +,lb31,nbg1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb31,0.049500,2026.02 +,lb31,hel1,node,metadata.labels.load-balancer.hetzner.cloud/type,lb31,0.049500,2026.02 +,lb31,ash,node,metadata.labels.load-balancer.hetzner.cloud/type,lb31,0.049500,2026.02 +,lb31,hil,node,metadata.labels.load-balancer.hetzner.cloud/type,lb31,0.049500,2026.02 +,lb31,sin,node,metadata.labels.load-balancer.hetzner.cloud/type,lb31,0.049500,2026.02 +,ssd,fsn1,pv,spec.storageClassName,hcloud-volumes,0.00006027,2026.02 +,ssd,nbg1,pv,spec.storageClassName,hcloud-volumes,0.00006027,2026.02 +,ssd,hel1,pv,spec.storageClassName,hcloud-volumes,0.00006027,2026.02 diff --git a/scripts/hetzner_pricing_generator.py b/scripts/hetzner_pricing_generator.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +""" +Hetzner Cloud Pricing CSV Generator for OpenCost + +This script fetches current pricing from the Hetzner Cloud API and generates +an OpenCost-compatible CSV file for cost tracking. + +Requirements: + pip install requests + +Usage: + # Set your Hetzner Cloud API token + export HCLOUD_TOKEN="your-api-token" + + # Run the script + python3 hetzner_pricing_generator.py + + # Output will be written to hetzner_pricing.csv + +API Documentation: + https://docs.hetzner.cloud/reference/cloud#pricing +""" + +import csv +import os +import sys +from datetime import datetime +from typing import Any + +try: + import requests +except ImportError: + print("Error: 'requests' library is required. Install with: pip install requests") + sys.exit(1) + + +HETZNER_API_URL = "https://api.hetzner.cloud/v1" + +# Hetzner regions +REGIONS = ["fsn1", "nbg1", "hel1", "ash", "hil", "sin"] + +# EU regions (pricing in the CSV will use EU rates by default) +EU_REGIONS = ["fsn1", "nbg1", "hel1"] + + +def get_api_token() -> str: + """Get Hetzner Cloud API token from environment.""" + token = os.environ.get("HCLOUD_TOKEN") + if not token: + print("Error: HCLOUD_TOKEN environment variable not set") + print("Get your token from: https://console.hetzner.cloud/projects/*/security/tokens") + sys.exit(1) + return token + + +def fetch_pricing(token: str) -> dict[str, Any]: + """Fetch pricing data from Hetzner Cloud API.""" + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{HETZNER_API_URL}/pricing", headers=headers) + + if response.status_code != 200: + print(f"Error fetching pricing: {response.status_code}") + print(response.text) + sys.exit(1) + + return response.json() + + +def fetch_server_types(token: str) -> dict[str, Any]: + """Fetch server types from Hetzner Cloud API.""" + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{HETZNER_API_URL}/server_types", headers=headers) + + if response.status_code != 200: + print(f"Error fetching server types: {response.status_code}") + print(response.text) + sys.exit(1) + + return response.json() + + +def fetch_load_balancer_types(token: str) -> dict[str, Any]: + """Fetch load balancer types from Hetzner Cloud API.""" + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{HETZNER_API_URL}/load_balancer_types", headers=headers) + + if response.status_code != 200: + print(f"Error fetching load balancer types: {response.status_code}") + print(response.text) + sys.exit(1) + + return response.json() + + +def parse_hourly_price(price_str: str) -> float: + """Parse price string to float.""" + try: + return float(price_str) + except (ValueError, TypeError): + return 0.0 + + +def generate_csv(output_file: str = "hetzner_pricing.csv"): + """Generate OpenCost-compatible CSV from Hetzner API data.""" + token = get_api_token() + + print("Fetching pricing data from Hetzner Cloud API...") + pricing_data = fetch_pricing(token) + server_types_data = fetch_server_types(token) + lb_types_data = fetch_load_balancer_types(token) + + version = datetime.now().strftime("%Y.%m") + rows = [] + + # Header + header = [ + "EndTimestamp", "InstanceID", "Region", "AssetClass", + "InstanceIDField", "InstanceType", "MarketPriceHourly", "Version" + ] + + # Process server types + print("Processing server types...") + for server_type in server_types_data.get("server_types", []): + name = server_type["name"] + + # Get pricing for each location + for price_info in server_type.get("prices", []): + location = price_info.get("location") + hourly_price = parse_hourly_price( + price_info.get("price_hourly", {}).get("net", "0") + ) + + if hourly_price > 0: + rows.append([ + "", # EndTimestamp + name, # InstanceID + location, # Region + "node", # AssetClass + "metadata.labels.node.kubernetes.io/instance-type", # InstanceIDField + name, # InstanceType + f"{hourly_price:.6f}", # MarketPriceHourly + version # Version + ]) + + # Process load balancer types + print("Processing load balancer types...") + for lb_type in lb_types_data.get("load_balancer_types", []): + name = lb_type["name"] + + for price_info in lb_type.get("prices", []): + location = price_info.get("location") + hourly_price = parse_hourly_price( + price_info.get("price_hourly", {}).get("net", "0") + ) + + if hourly_price > 0: + rows.append([ + "", # EndTimestamp + name, # InstanceID + location, # Region + "node", # AssetClass (using node for LBs to track costs) + "metadata.labels.load-balancer.hetzner.cloud/type", # InstanceIDField + name, # InstanceType + f"{hourly_price:.6f}", # MarketPriceHourly + version # Version + ]) + + # Process volume pricing + print("Processing volume pricing...") + pricing = pricing_data.get("pricing", {}) + volume_pricing = pricing.get("volume", {}) + + # Volume pricing is per GB per month, convert to hourly + volume_monthly = parse_hourly_price( + volume_pricing.get("price_per_gb_month", {}).get("net", "0.052") + ) + volume_hourly = volume_monthly / 730 # Average hours per month + + for region in EU_REGIONS: + rows.append([ + "", # EndTimestamp + "ssd", # InstanceID + region, # Region + "pv", # AssetClass + "spec.storageClassName", # InstanceIDField + "hcloud-volumes", # InstanceType + f"{volume_hourly:.8f}", # MarketPriceHourly + version # Version + ]) + + # Write CSV + print(f"Writing {len(rows)} pricing entries to {output_file}...") + with open(output_file, "w", newline="") as f: + # Write header comment + f.write("# Hetzner Cloud Pricing for OpenCost CSV Provider\n") + f.write(f"# Generated: {datetime.now().isoformat()}\n") + f.write("# Source: Hetzner Cloud API (https://api.hetzner.cloud/v1/pricing)\n") + f.write("# Prices in EUR per hour (net, excluding VAT)\n") + f.write("#\n") + + writer = csv.writer(f) + writer.writerow(header) + writer.writerows(rows) + + print(f"Done! Generated {output_file} with {len(rows)} pricing entries.") + print("\nTo use with OpenCost:") + print(" 1. Copy the CSV to your OpenCost deployment") + print(" 2. Set USE_CSV_PROVIDER=true") + print(" 3. Set CSV_PATH to the file location") + + +def main(): + """Main entry point.""" + import argparse + + parser = argparse.ArgumentParser( + description="Generate OpenCost-compatible pricing CSV from Hetzner Cloud API" + ) + parser.add_argument( + "-o", "--output", + default="hetzner_pricing.csv", + help="Output CSV file path (default: hetzner_pricing.csv)" + ) + + args = parser.parse_args() + generate_csv(args.output) + + +if __name__ == "__main__": + main()