Hoppa till innehåll
DjupdykningarKostnadsoptimeringKubernetes

Djupdykning: Varför dina Kubernetes-kostnader är 3x vad de borde vara

En forensisk analys av strategier för molnkostnadsoptimering som faktiskt fungerar - med verkliga siffror från vår klusterrevision

Simon Axelsson

Simon Axelsson

IT-konsult & teknisk rådgivare

2026-03-2514 min läsning15 220203

Total årlig besparing

$127K

Före optimering$18,400/mo
Efter optimering$7,800/mo
Minskning57.6%

Under Q3 2025 nådde vår månatliga Kubernetes-faktura $18 400. För en medelstor organisation som körde vad som borde ha varit en blygsam arbetsbelastning - ett par dussin mikrotjänster, viss batchbearbetning och en handfull databaser - var det absurt. Den här artikeln dokumenterar den forensiska revisionen vi genomförde, optimeringarna vi tillämpade, och de $127K i annualiserade besparingar vi uppnådde över fyra månader.

Problemet

Varningstecknen hade funnits där i månader. Vår molnfaktura hade vuxit 15-20% per kvartal, men ingen spårade Kubernetes-kostnaderna specifikt - de var begravda i den övergripande GCP-fakturan tillsammans med BigQuery, Cloud Functions och lagringskostnader. När vi äntligen isolerade GKE-posten var siffran häpnadsväckande: $18 400 per månad för vad som uppgick till 47 poddar som körde på 12 noder.

Rotorsaken var inte ett enstaka misstag utan en ackumulering av små beslut över två år. Varje ny tjänst deployades med generösa resursförfrågningar eftersom ingen ville hantera OOMKills. Nodpooler dimensionerades för toppbelastning och skalades aldrig ner. Namnrymder spred sig utan rensning. Utvecklings- och staging-miljöer kördes dygnet runt på samma dyra nodtyper som produktion. Ingen övervakade faktisk resursanvändning kontra begärda resurser.

Ironin är att Kubernetes var tänkt att spara pengar genom bättre resursutnyttjande. Istället skapade komplexiteten i Kubernetes resurshantering en kultur av överprovisionering. Utvecklare begärde 2 CPU-kärnor och 4 GB RAM för tjänster som faktiskt använde 0,1 kärnor och 200 MB. Multiplicera det med 47 poddar och du får ett kluster som körs på 8% genomsnittligt utnyttjande medan det faktureras för 100%.

Resursutnyttjande före revision

CPU (cluster avg)8/96 cores (8% utilized)
requested
actual
Memory (cluster avg)42/384 GB (11% utilized)
requested
actual
Persistent Volumes340/2000 GB (17% utilized)
requested
actual

Betalar för 96 CPU-kärnor men använder 8. Det är inte ovanligt - branschundersökningar visar genomsnittligt K8s-utnyttjande på 15-25%.

Problemet var inte tekniskt. Det var organisatoriskt. Ingen ägde molnkostnadsoptimeringen. Plattformsteamet hanterade drifttid, utvecklingsteamet levererade funktioner och ekonomiteamet betalade fakturor. Kostnaden föll i glappet mellan alla tre.

Revisionen

Vi lade två veckor på att genomföra en forensisk revision av varje körande arbetsbelastning. Metoden var enkel: för varje pod, mäta faktisk resursförbrukning över ett 14-dagarsfönster, jämföra med begärda resurser och beräkna slöseriekvoten. Vi använde en kombination av Prometheus-mätvärden, kubectl top-ögonblicksbilder och GKE usage metering.

Bashaudit/resource-waste-report.sh
1#!/bin/bash
2# Generate resource waste report across all namespaces
3
4echo "=== Kubernetes Resource Waste Audit ==="
5echo "Cluster: $(kubectl config current-context)"
6echo "Date: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
7echo ""
8
9for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
10 echo "--- Namespace: $ns ---"
11
12 kubectl top pods -n "$ns" --no-headers 2>/dev/null | while read pod cpu mem; do
13 # Get requested resources
14 req_cpu=$(kubectl get pod "$pod" -n "$ns" -o jsonpath='{.spec.containers[0].resources.requests.cpu}')
15 req_mem=$(kubectl get pod "$pod" -n "$ns" -o jsonpath='{.spec.containers[0].resources.requests.memory}')
16
17 echo "Pod: $pod"
18 echo " CPU: actual=$cpu requested=$req_cpu"
19 echo " Memory: actual=$mem requested=$req_mem"
20 echo " Waste: ~$(calculate_waste "$cpu" "$req_cpu")% CPU, ~$(calculate_waste "$mem" "$req_mem")% Memory"
21 done
22done

Revisionen avslöjade fem kategorier av slöseri, var och en krävde olika optimeringsstrategier. Överdimensionerade pod-begäranden stod för 42% av totalt slöseri. Alltid-på-utvecklings/staging-miljöer bidrog med 23%. Oanvända persistent volumes och föräldralösa resurser stod för ytterligare 15%. Suboptimala nodtyper (att använda beräkningsoptimerade instanser för minnesintensiva arbetsbelastningar) representerade 12%. Resterande 8% var diverse: övergivna jobb, bortglömda CronJobs och testdeployments som ingen städade upp.

Slöserikategorier

Överdimensionerade pod-begäranden42%
Alltid-på icke-produktionsmiljöer23%
Föräldralösa resurser & PV:er15%
Felaktiga nodtyper12%
Bortglömda jobb & testdeployments8%

Med denna data prioriterade vi optimeringar efter påverkan och insats. Rätt dimensionering av pod-begäranden var hög påverkan och låg insats. Konvertering av icke-produktionsmiljöer till spot-instanser var hög påverkan och medelhög insats. Nodtypsoptimering krävde mer planering men hade tydlig avkastning. Vi skapade en fyra månaders implementationsplan och spårade besparingar veckovis.

Rätt dimensionering av arbetsbelastningar

Rätt dimensionering var den enskilt mest effektfulla optimeringen, ansvarig för 48% av totala besparingar. Tillvägagångssättet var metodiskt: för varje deployment analyserade vi P99-resursanvändning över 14 dagar, lade till en 30% buffert för säkerhet och uppdaterade resursförfrågningar och gränser därefter.

Vi deployade också Vertical Pod Autoscaler (VPA) i rekommendationsläge över hela klustret. VPA analyserar faktisk resursförbrukning och föreslår optimala begäranden. Vi aktiverade inte VPA i auto-uppdateringsläge eftersom vi ville ha mänsklig granskning av varje ändring - vissa tjänster har spikmönster som P99-mätvärden inte fångar väl.

YAMLmanifests/api-gateway-optimized.yaml
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: api-gateway
5 namespace: production
6spec:
7 replicas: 3
8 template:
9 spec:
10 containers:
11 - name: api-gateway
12 image: gcr.io/enterprise/api-gateway:v2.14
13 resources:
14 # BEFORE: requests: cpu=2000m, memory=4Gi
15 # AFTER: based on 14-day P99 + 30% buffer
16 requests:
17 cpu: 250m # was 2000m (actual P99: 180m)
18 memory: 512Mi # was 4Gi (actual P99: 380Mi)
19 limits:
20 cpu: 500m # 2x request for burst capacity
21 memory: 1Gi # 2x request for safety
22 # Added: Readiness probe to prevent routing during startup
23 readinessProbe:
24 httpGet:
25 path: /healthz
26 port: 8080
27 initialDelaySeconds: 5
28 periodSeconds: 10
29 # Added: Pod topology spread for availability
30 topologySpreadConstraints:
31 - maxSkew: 1
32 topologyKey: topology.kubernetes.io/zone
33 whenUnsatisfiable: DoNotSchedule

Den svåraste delen av rätt dimensionering var att hantera tjänster med varierande belastningsmönster. Våra batchbearbetningspoddar spikar till 8x sin baslinje under det nattliga datainförselsfönstret. För dessa använde vi en kombination av rätt dimensionerade basbegäranden med Horizontal Pod Autoscaler (HPA)-skalning baserad på anpassade Prometheus-mätvärden snarare än enkel CPU-procent. HPA-konfigurationen använder vårt faktiska ködjupsmätvärde, som korrelerar mycket bättre med faktiska resursbehov än CPU-utnyttjande.

Topp 5 tjänster: Före vs efter rätt dimensionering

API Gateway
$3 200$840-74%
Data Ingestion Workers
$2 800$1 100-61%
CRM Backend
$2 400$920-62%
Analytics Service
$1 900$650-66%
Search Indexer
$1 600$580-64%

Vi rullade ut ändringarna för rätt dimensionering stegvis över tre veckor, med start på de lägsta riskernas tjänster och övervakade latensregressioner, felfrekvenökningar och OOMKill-händelser. Bara två tjänster behövde justering efter den initiala utrullningen - båda på grund av minneshungriga garbage collection-mönster i Java-tjänster som vårt 14-dagars observationsfönster inte hade fångat. Vi ökade deras minnesgränser med 50% och de stabiliserades omedelbart.

Spot-instansstrategi

Spot (preemptible)-instanser på GKE kostar 60-80% mindre än on-demand-instanser. Avvägningen är att Google kan återta dem med 30 sekunders varsel. För tillståndslösa, feltåliga arbetsbelastningar är denna avvägning nästan alltid värd det. För vårt kluster identifierade vi tre kategorier av arbetsbelastningar lämpliga för spot-instanser.

Icke-produktionsmiljöer var den enklaste vinsten. Utvecklings- och staging-kluster körs nu helt på spot-instanser, vilket sparar $4 200/månad med noll påverkan på utvecklarupplevelsen. Om en spot-nod återtas startar poddar om på en ny nod inom 60 sekunder. För utvecklingsarbete är det omärkbart.

Batchbearbetningsarbetsbelastningar - våra datainförsel- och dbt-transformationspoddar - var nästa mål. Dessa är idempotenta av design: om en pod termineras mitt i ett jobb, kör orkestraren om det. Vi konfigurerade batch-nodpoolen som 100% spot, vilket sparade $1 800/månad. Omförsöksfrekvensen ökade från nära noll till cirka 3% av jobben, men den totala bearbetningstiden minskade faktiskt eftersom vi hade råd med fler parallella arbetare till det lägre spot-priset.

För tillståndslösa produktionstjänster använder vi en blandad strategi: 30% on-demand-noder som garanterad baslinje, 70% spot-noder för ytterligare kapacitet. PodDisruptionBudgets säkerställer att spot-återtagning aldrig tar mer än en pod offline per tjänst åt gången. Vi använder också node affinity-regler för att prioritera latenskänsliga tjänster på on-demand-noder.

Terraformterraform/gke-node-pools.tf
1resource "google_container_node_pool" "spot_general" {
2 name = "spot-general"
3 cluster = google_container_cluster.primary.name
4 location = "europe-north1"
5
6 autoscaling {
7 min_node_count = 0
8 max_node_count = 10
9 }
10
11 node_config {
12 spot = true
13 machine_type = "e2-standard-4" # Changed from n2-standard-8
14
15 labels = {
16 "node-type" = "spot"
17 "workload" = "general"
18 }
19
20 taint {
21 key = "cloud.google.com/gke-spot"
22 value = "true"
23 effect = "NO_SCHEDULE"
24 }
25 }
26}
27
28resource "google_container_node_pool" "ondemand_baseline" {
29 name = "ondemand-baseline"
30 cluster = google_container_cluster.primary.name
31 location = "europe-north1"
32
33 node_count = 3 # Fixed baseline, always available
34
35 node_config {
36 spot = false
37 machine_type = "e2-standard-4"
38
39 labels = {
40 "node-type" = "ondemand"
41 "workload" = "critical"
42 }
43 }
44}

Totala besparingar från spot-instansstrategin: $6 400/månad. Den viktigaste lärdomen: tänk inte på spot som allt-eller-inget. Den blandade strategin med PodDisruptionBudgets ger dig de flesta besparingarna med försumbar tillgänglighetspåverkan. Vi har kört denna konfiguration i fyra månader med noll kundsynliga incidenter hänförbara till spot-återtagning.

Smart schemaläggning

Our dev and staging clusters were running 24/7. Developers work roughly 08:00-18:00 CET. That means these environments were running - and billing - for 14 hours per day with zero users. We implemented automated scaling schedules that scale non-prod environments to zero outside business hours and on weekends.

YAMLschedules/scale-down-nonprod.yaml
1# CronJob: Scale non-prod to zero at 19:00 CET
2apiVersion: batch/v1
3kind: CronJob
4metadata:
5 name: scale-down-nonprod
6 namespace: kube-system
7spec:
8 schedule: "0 19 * * 1-5" # Mon-Fri at 19:00
9 jobTemplate:
10 spec:
11 template:
12 spec:
13 containers:
14 - name: scaler
15 image: bitnami/kubectl:1.28
16 command:
17 - /bin/sh
18 - -c
19 - |
20 for ns in dev staging; do
21 for deploy in $(kubectl get deploy -n $ns -o name); do
22 # Save current replicas as annotation before scaling down
23 replicas=$(kubectl get $deploy -n $ns -o jsonpath='{.spec.replicas}')
24 kubectl annotate $deploy -n $ns --overwrite \
25 cost-opt/original-replicas="$replicas"
26 kubectl scale $deploy -n $ns --replicas=0
27 done
28 done
29 echo "Scaled down dev and staging at $(date)"
30 restartPolicy: OnFailure
31
32---
33# CronJob: Scale non-prod back up at 07:30 CET
34apiVersion: batch/v1
35kind: CronJob
36metadata:
37 name: scale-up-nonprod
38spec:
39 schedule: "30 7 * * 1-5" # Mon-Fri at 07:30
40 jobTemplate:
41 spec:
42 template:
43 spec:
44 containers:
45 - name: scaler
46 image: bitnami/kubectl:1.28
47 command:
48 - /bin/sh
49 - -c
50 - |
51 for ns in dev staging; do
52 for deploy in $(kubectl get deploy -n $ns -o name); do
53 replicas=$(kubectl get $deploy -n $ns \
54 -o jsonpath='{.metadata.annotations.cost-opt/original-replicas}')
55 kubectl scale $deploy -n $ns \
56 --replicas=${replicas:-1}
57 done
58 done

This saved $2,100/month. The schedule runs on weekdays only, and we added a Slack command (/wake-up-staging) for engineers who occasionally need the staging environment outside business hours. The command triggers a manual scale-up and automatically scales back down after 2 hours unless extended. Total off-hours usage since implementing the schedule: about 12 hours per month, confirming that the 24/7 running was pure waste.

Cost Monitoring

The biggest lesson from this entire exercise: optimization without monitoring is a one-time event. Costs will creep back up as new services get deployed with generous defaults, as node pools auto-scale and never scale back down, and as abandoned resources accumulate. We built a cost monitoring system that prevents regression.

We deploy Kubecost in our cluster to provide real-time cost attribution per namespace, deployment, and label. Every namespace has a monthly budget configured in Kubecost. When a namespace exceeds 80% of its budget, a Slack alert fires to the team that owns it. At 100%, it escalates to the platform team. This has shifted cost awareness from a quarterly finance conversation to a daily engineering concern.

We also run a weekly automated audit that checks for the most common cost regressions: pods with resource requests more than 3x their actual usage, PersistentVolumeClaims that have not been read in 30 days, and node pools with sustained utilization below 20%. The audit generates a report and opens Jira tickets for the top five offenders each week.

Additionally, we added a CI check that estimates the monthly cost of any new deployment before it merges. The check uses Infracost for Terraform changes and a custom script for Kubernetes manifests that multiplies resource requests by our blended node cost. Any PR that would increase monthly costs by more than $200 requires explicit approval from the platform team lead. This single gate has prevented more cost regressions than any monitoring dashboard.

Savings Timeline

The optimization was rolled out över four months. Here is the cumulative savings timeline, showing each major initiative and its impact.

Month 1: October 2025$4 200 saved

Orphan cleanup: deleted 8 unused PVs, 3 abandoned namespaces, 12 completed jobs. Scheduled non-prod environments to scale down outside business hours.

Month 2: November 2025$14 800 saved

Right-sizing Phase 1: updated resource requests for 28 of 47 pods based on VPA recommendations. Moved dev cluster entirely to spot instances.

Month 3: December 2025$25 200 saved

Right-sizing Phase 2: remaining 19 pods. Production spot strategy deployed (30/70 split). Node pool machine types optimized (n2-standard-8 -> e2-standard-4).

Month 4: January 2026$31 800 saved

Monitoring and governance deployed. Kubecost budgets enforced. CI cost estimation gate activated. Weekly automated audit started. Monthly steady-state reached at $7,800.

Annualized (projected)$127,200 saved

From $18,400/mo to $7,800/mo = $10,600/mo savings = $127,200/year

“Kubernetes does not optimize costs automatically. It optimizes resource scheduling. Cost optimization is a human problem that requires human discipline, continuous monitoring, and organizational accountability.”

- Simon Axelsson

If you are running Kubernetes in production and have never conducted a resource audit, I can almost guarantee you are overspending by 40-60%. The optimizations described here are not exotic - right-sizing, spot instances, scheduling, and monitoring are all well-documented practices. The hard part is not the technology. It is making someone responsible, giving them time, and building the organizational habits that prevent regression. Start with the audit. The numbers will make the business case for everything else.

KubernetesCost OptimizationGKECloudDevOpsTerraformSpot InstancesFinOps
Simon Axelsson

Simon Axelsson

IT-konsult & teknisk rådgivare

Simon Axelsson is a senior IT consultant and founder of SIAX Technology AB in Angelholm. He helps Nordic companies with cloud infrastructure, data platforms, and AI automation. He believes the best infrastructure is the infrastructure you do not pay for.