Skip to content
getsentryPublic

About

A quick and dirty kubernetes cost estimator

Topics

Resources

Code of conduct

Security policy

Stars

6 stars

Watchers

26 watching

Forks

Repository files navigation

skrooge

PyPI Changelog Tests License

A quick and dirty kubernetes cost estimator

OIG 0uCanRKKeLq7XwJysZQ3 Ebenezer scrooge standing at the helm of a ship, pencil sketch

Why

Scaling a Kubernetes deployment up or down changes how many nodes it needs, so it changes what you pay. Working that out by hand is grunt work every time: look up the node's machine type, figure out whether the change is CPU-bound or memory-bound, count the extra nodes, and price them. skrooge does that arithmetic for you using bundled GCP machine specs and regional pricing.

For example, doubling a deployment of 2 CPU / 3GiB pods from 16 to 32 replicas on c2-standard-30 nodes (30 cores, 120GiB) adds 32 cores and 48GiB of memory. CPU runs out first, so it needs ceil(32 / 30) = 2 more nodes:

$ uvx skrooge estimate --replicas 16 --cpu 2000 --mem 3072 --instance c2-standard-30
This workload is running on c2-standard-30 instances in us-central1 with 30 cores and 120GiB of memory.
This workload will require 32.0 cores and 48.0 GiB of memory. This workload is cores-constrained.
This workload will require 2 instances, costing $2.51/h (or $1829/m (or $21950/y)) at sud rates.

For a scale-down, pass the size of the reduction: the cost skrooge reports is what you'd save.

It's a quick estimate, not a bill. It assumes pods pack perfectly onto whole nodes, and it ignores the node capacity reserved for the system and DaemonSets, cluster autoscaler behaviour, and storage or network costs. Prices come from a snapshot in skrooge/instances.json, taken whenever that file was last regenerated.

Installation

skrooge requires Python 3.11 or newer. The quickest way to use it is to run it with uvx, which fetches and runs the latest release without installing it:

uvx skrooge --help

To install it as a standalone tool instead, use uv:

uv tool install skrooge

Or with pipx or pip:

pipx install skrooge
pip install skrooge

Usage

Estimate the cost of adding 5.81 cores and 9950MiB of memory to each of 1000 replicas running on n2-highcpu-64 nodes:

$ uvx skrooge estimate --replicas 1000 --cpu 5810 --mem 9950 --instance n2-highcpu-64
This workload is running on n2-highcpu-64 instances in us-central1 with 64 cores and 64GiB of memory.
This workload will require 5810.0 cores and 9716.796875 GiB of memory. This workload is memory-constrained.
This workload will require 152 instances, costing $278.98/h (or $203658/m (or $2443898/y)) at sud rates.
Bin-packed in a cluster at 100% efficiency, this workload will cost $179.87/h (or $131300/m (or $1575609/y)): $107257/m for cores and $24043/m for memory.

Use --region, --cost-class, and --format json to change the region, pricing tier, and output format:

uvx skrooge estimate --replicas 1000 --cpu 5810 --mem 9950 --instance n2-highcpu-64 --region europe-west1 --cost-class cud-1y --format json

Not every instance type has every cost class in every region. To list the regions with pricing for an instance type and cost class:

uvx skrooge regions --instance n2-highcpu-64 --cost-class cud-1y

If you installed skrooge, drop the uvx prefix and run skrooge directly (or python -m skrooge).

Whole nodes or bin-packed

skrooge estimates costs two ways:

  • Whole nodes: the nodes a dedicated node pool would need. The workload pays for whole nodes, including the memory it doesn't use on a CPU-bound workload (or the CPU on a memory-bound one). This is an upper bound.
  • Bin-packed: just the CPU and memory the workload requests, at GCP's per-vCPU and per-GiB prices. In a large shared cluster, other workloads fill the gaps, so this is closer to what a workload really costs. With perfect packing it's a lower bound.

No cluster packs perfectly, so --packing-efficiency (-p) sets the percentage of the cluster's CPU and memory that pods request on average. The bin-packed cost is divided by it, so at 80% it's 25% higher:

$ uvx skrooge estimate --replicas 1000 --cpu 5810 --mem 9950 --instance n2-highcpu-64 --packing-efficiency 80
...
Bin-packed in a cluster at 80% efficiency, this workload will cost $224.83/h (or $164126/m (or $1969511/y)): $134072/m for cores and $30054/m for memory.

A cluster usually runs short of one resource while the other sits partly idle, say with 85% of its CPU requested but only 55% of its memory. --cpu-packing-efficiency and --mem-packing-efficiency set each one separately, overriding --packing-efficiency:

$ uvx skrooge estimate --replicas 1000 --cpu 5810 --mem 9950 --instance n2-highcpu-64 --cpu-packing-efficiency 85 --mem-packing-efficiency 55
...
Bin-packed in a cluster at 85% CPU and 55% memory efficiency, this workload will cost $232.74/h (or $169900/m (or $2038804/y)): $126185/m for cores and $43715/m for memory.

Bin-packed estimates use GCP's per-vCPU and per-GiB prices for each machine family. GPU machine types (a2, g2) also pay for their GPUs, and shared-core types (f1-micro, g1-small, e2-micro, e2-small, e2-medium) for a fraction of a vCPU, so those only get whole-node estimates.

Autoscaling

For a deployment that autoscales, pass the fewest and most replicas you expect it to run, with --min-replicas and --max-replicas instead of --replicas. skrooge estimates the cost at each end, and an average over a typical week of scaling, set with --scaling-pattern:

Pattern Replicas Time at max
business-hours (default) Max from 08:00 to 18:00 on weekdays, min the rest of the time 30%
daytime Max from 08:00 to 18:00 every day, min overnight 42%
diurnal A smooth daily curve from min at night to max in the afternoon Averages halfway
hourly-spikes Max for 20 minutes of every hour, like hourly batch jobs, min the rest of the time 33%
A percentage, like 25% Max for that share of the time, min the rest That percentage

The cluster autoscaler adds and removes whole nodes as replicas come and go, so the average whole-node cost is the average of the nodes needed at each point in the week. Between min and max, replicas are rounded up, as the Horizontal Pod Autoscaler does. Use 0 for --min-replicas for a workload that scales to zero.

If none of the patterns fit, pass the share of the time the deployment spends at max replicas, like --scaling-pattern 25%. For a deployment that holds steady at some number of replicas, use --replicas instead.

$ uvx skrooge estimate --min-replicas 2 --max-replicas 30 --cpu 2500 --mem 2024 --instance n2-standard-32
This workload is running on n2-standard-32 instances in us-central1 with 32 cores and 128GiB of memory.
Autoscaling between 2 and 30 replicas, this workload will require 5.0 to 75.0 cores and 3.953125 to 59.296875 GiB of memory. This workload is cores-constrained.
At sud rates, this workload will require 1 instances at 2 replicas, costing $1.24/h (or $907/m (or $10890/y)), and 3 instances at 30 replicas, costing $3.73/h (or $2722/m (or $32669/y)).
With the business-hours scaling pattern (max replicas from 08:00 to 18:00 on weekdays, min the rest of the time), this workload will average 10.33 replicas on 1.6 instances, costing $1.98/h (or $1448/m (or $17372/y)) at sud rates.
Bin-packed in a cluster at 100% efficiency, this workload will cost $102/m at 2 replicas and $1532/m at 30, averaging $0.72/h (or $528/m (or $6329/y)): $477/m for cores and $51/m for memory.

The patterns are rough shapes, not your traffic, and the cluster autoscaler waits about 10 minutes before removing a node that's no longer needed, so short spikes cost a little more on whole nodes than estimated.

With --format json, replicas, required_instance_count, costs and resource_costs are the weekly averages, and autoscaling holds the scaling_pattern (a pattern's name, or a percentage like "25%"), min_replicas and max_replicas, and the estimates at each end in min and max.

JSON output and custom formats

--format json prints the whole estimate as one JSON object, for scripts or to format the output your own way with jq. The most useful fields are:

  • required_instance_count and constrained_resource: how many whole nodes the change needs, and whether cores or memory decides it
  • costs: the whole-node cost, hourly, monthly and yearly, with its cost class and region
  • resource_costs: the bin-packed cost, with hourly, monthly and yearly costs for cores, memory and the total. It's null when the instance type's price can't be split into CPU and memory, as for GPU machine types.
  • delta_cpu and delta_mem: the total change, in milli-cores and MiB

To pick out a few fields:

$ uvx skrooge estimate --replicas 1000 --cpu 5810 --mem 9950 --instance n2-highcpu-64 --format json \
    | jq '{nodes: .required_instance_count, constrained_resource, whole_nodes: .costs.monthly, bin_packed: .resource_costs.total.monthly}'
{
  "nodes": 152,
  "constrained_resource": "memory",
  "whole_nodes": 203658,
  "bin_packed": 131300
}

To print a line in your own format:

$ uvx skrooge estimate --replicas 1000 --cpu 5810 --mem 9950 --instance n2-highcpu-64 --format json \
    | jq -r '"\(.replicas) replicas on \(.instance) in \(.region): \(.required_instance_count) nodes, $\(.costs.monthly)/month at \(.costs.class) rates"'
1000 replicas on n2-highcpu-64 in us-central1: 152 nodes, $203658/month at sud rates

To compare cost classes as CSV, for a spreadsheet, with the monthly whole-node and bin-packed costs:

$ for class in ondemand sud cud-1y cud-3y; do
    uvx skrooge estimate --replicas 1000 --cpu 5810 --mem 9950 --instance n2-highcpu-64 --cost-class "$class" --format json
  done | jq -r '[.costs.class, .costs.monthly, .resource_costs.total.monthly] | @csv'
"ondemand",254572,164126
"sud",203658,131300
"cud-1y",160379,103397
"cud-3y",114561,73859

A word from the Scrooges

For a second opinion, --format scrooge has Ebenezer Scrooge himself read you the estimate. His temper depends on the monthly whole-node cost:

$ uvx skrooge estimate --replicas 1000 --cpu 5810 --mem 9950 --instance n2-highcpu-64 --format scrooge
 ___________________________________________________________
/ BAH! HUMBUG! Wouldst thou see me in the poorhouse?        \
|                                                           |
| Thy 1,000 replicas upon n2-highcpu-64 in us-central1 do   |
| want 5,810 cores and 9,716.8 GiB of memory, and 'tis the  |
| memory that bindeth thee.                                 |
|                                                           |
| 152 whole nodes thou must have, at sud rates: $278.98 the |
| hour, $203,658 the month, and $2,443,898 the year!        |
|                                                           |
| Packed as tight as the coins in my strongbox, at 100%     |
| efficiency, 'twould be $131,300 the month and $1,575,609  |
| the year.                                                 |
|                                                           |
| And if thou art shrinking it, these be thy savings, and   |
| the first good news I have had all day.                   |
|                                                           |
| Are there no prisons? Are there no workhouses? Shrink it, |
\ I say, shrink it!                                         /
 -----------------------------------------------------------
           \
            \       _.-""""-._
             \    .'          `-._
                 /                `-._
                /  .------------.     `-.
               |  /              \       (@)
               | |   ___    ___   |
               | |  ( o )--( o )  |
              (| |       \        |)
              (| |        \       |)       )
               \\|       __)      |/      (
                \|                |      (_)
                 |     .----.     |      |=|
                 |    /      \    |      |=|
                  \              /     __|=|__
                   `-.________.-'     (_______)

Or, for a canny Scot's view, --format mcduck has Scrooge McDuck tally it up from his money bin:

$ uvx skrooge estimate --replicas 30 --cpu 2500 --mem 2024 --instance n2-standard-32 --format mcduck
 ____________________________________________________________
/ Bless me bagpipes! Ye want HOW much?                       \
|                                                            |
| Yer 30 replicas on n2-standard-32 in us-central1 want 75   |
| cores and 59.3 GiB o' memory, and it's the cores that's    |
| got ye by the sporran.                                     |
|                                                            |
| That's 3 whole nodes at sud rates: $3.73 an hour, $2,722 a |
| month, and $32,669 a year!                                 |
|                                                            |
| Packed as tight as the coins in me money bin, at 100%      |
| efficiency, it'd be $1,532 a month and $18,376 a year.     |
|                                                            |
| And if ye're shrinkin' it, those are yer savings, and      |
| that's music tae me ears!                                  |
|                                                            |
| Be tougher than the toughies and smarter than the          |
\ smarties, and ye'll no' need sae many nodes!               /
 ------------------------------------------------------------
           \
            \               _______
             \             |       |
                           |       |
                         __|_______|__
                          /         \
                         /   o       \
                  ___(O)/   /         |~~
                 (______    (         |~~~
                  \_____`.   \        /~~
                         `-----.____.'
                  _          /  >o<  \
                 ( `.       /    |    \
                     |-----|     |     |
                     |      \    o    /
                     |       \   |   /
                     |        |  o  |
                     |        |__|__|
                     |         |   |
                     |      ___|  _|
                     |     <___| <__|

Interactive mode

skrooge tui opens a terminal UI where you can change every input and watch the estimate update as you type. You can estimate several workloads at once and see the net change across all of them, and the selected workload's cost at every pricing tier side by side:

uvx skrooge tui
Key Action
ctrl+n Add a workload (starts as a copy of the selected one)
ctrl+r Remove the selected workload
ctrl+b Switch the workloads table and totals between whole nodes and bin-packed
ctrl+y Copy the workloads table and net change as Markdown, to paste into a pull request or doc
ctrl+s Save the workloads and settings
ctrl+q Quit, leaving a summary table in your terminal

Copying works in terminals that let apps set the clipboard (with OSC 52), which most do, but not macOS's Terminal. In tmux, turn on set-clipboard.

Use tab to move between fields, and the arrow keys in the workloads table to pick which workload you're editing. For each workload, enter its replicas and per-replica CPU and memory as they are before the change and after it, and skrooge works out the change: 10 replicas at 2,000m becoming 20 at 1,500m is 10 more cores. Use 0 replicas before for a new workload, or 0 after for one you're removing. A change that shrinks the resource a workload is bound by is a saving in the totals.

The settings above the workloads table choose the cost model and the CPU and memory packing efficiencies, which apply to every workload. The estimate panel always shows the selected workload under both models. Bin-packed costs treat each resource separately, so a workload that requests more CPU but less memory nets the two out. With whole nodes it would pay for the CPU's extra nodes.

The estimate panel also compares the workload's machine type with the others in its family with as many vCPUs, by the cost of running all its replicas after the change on whole nodes. For a CPU-bound workload on n2-standard-32, n2-highcpu-32 is usually cheaper. Machine types of other sizes or families don't compare fairly: skrooge doesn't count the overhead of each node, so smaller nodes would always look cheaper, and families differ in performance. Bin-packed, every machine type in a family costs the same.

Workloads are loaded from and saved to skrooge.json in the current directory; pass a path to use another file:

uvx skrooge tui capacity-plan.json

The file looks like this, with CPU in milli-cores and memory in MiB per replica. cost_model (whole-nodes or bin-packed), cpu_packing_efficiency, mem_packing_efficiency, and each workload's region and cost_class are optional, and default to whole-nodes, 100, 100, us-central1, and sud:

{
  "cost_model": "bin-packed",
  "cpu_packing_efficiency": 85,
  "mem_packing_efficiency": 55,
  "workloads": [
    {
      "name": "web",
      "replicas_before": 10, "cpu_before": 2500, "mem_before": 2024,
      "replicas_after": 40, "cpu_after": 2500, "mem_after": 2024,
      "instance": "n2-standard-32"
    },
    {
      "name": "worker",
      "replicas_before": 0, "cpu_before": 4000, "mem_before": 16384,
      "replicas_after": 12, "cpu_after": 4000, "mem_after": 16384,
      "instance": "n2-highmem-16",
      "cost_class": "cud-1y"
    }
  ]
}

Command reference

The help for every command:

$ skrooge --help
Usage: skrooge [OPTIONS] COMMAND [ARGS]...

  A quick and dirty kubernetes cost estimator

Options:
  --version  Show the version and exit.
  --help     Show this message and exit.

Commands:
  estimate  Quick estimate of the cost or savings a kubernetes scale...
  regions   List the regions skrooge has pricing for
  tui       Interactively estimate one or more workloads, with live results
$ skrooge estimate --help
Usage: skrooge estimate [OPTIONS]

  Quick estimate of the cost or savings a kubernetes scale update will incur

Options:
  -v, --verbosity LVL             Either CRITICAL, ERROR, WARNING, INFO or
                                  DEBUG
  -r, --replicas INTEGER RANGE    The number of replicas in the deployment.
                                  For an autoscaling one, use --min-replicas
                                  and --max-replicas instead  [x>=1]
  --min-replicas INTEGER RANGE    The fewest replicas an autoscaling
                                  deployment is expected to run  [x>=0]
  --max-replicas INTEGER RANGE    The most replicas an autoscaling deployment
                                  is expected to run  [x>=1]
  --scaling-pattern [business-hours|daytime|diurnal|hourly-spikes|N%]
                                  How an autoscaling deployment's replicas
                                  vary over a week, for its average cost, or
                                  the percentage of the time at max replicas,
                                  like 25%, with min replicas the rest.
                                  Default: business-hours
  -c, --cpu INTEGER               The amount of CPU change in milli-cores
                                  [required]
  -m, --mem INTEGER               The amount of memory change in MiB
                                  [required]
  -i, --instance TEXT             The instance type this deployment is running
                                  on  [required]
  --cost-class [sud|ondemand|preemptible|cud-1y|cud-3y]
                                  The type of cost to calculate. Default: sud
  --region REGION                 The region to use for cost calculation.
                                  Default: us-central1. List them with:
                                  skrooge regions
  -p, --packing-efficiency INTEGER RANGE
                                  For the bin-packed estimate, the percentage
                                  of the cluster's CPU and memory that pods
                                  request on average. Default: 100
                                  [1<=x<=100]
  --cpu-packing-efficiency INTEGER RANGE
                                  Like --packing-efficiency, for CPU only.
                                  Default: --packing-efficiency  [1<=x<=100]
  --mem-packing-efficiency INTEGER RANGE
                                  Like --packing-efficiency, for memory only.
                                  Default: --packing-efficiency  [1<=x<=100]
  -f, --format [english|json|scrooge|mcduck]
                                  The output format. Default: english
  --help                          Show this message and exit.
$ skrooge regions --help
Usage: skrooge regions [OPTIONS]

  List the regions skrooge has pricing for

Options:
  -i, --instance TEXT             Only list regions with pricing for this
                                  instance type
  --cost-class [sud|ondemand|preemptible|cud-1y|cud-3y]
                                  Only list regions with pricing in this cost
                                  class
  --help                          Show this message and exit.
$ skrooge tui --help
Usage: skrooge tui [OPTIONS] [WORKLOADS_FILE]

  Interactively estimate one or more workloads, with live results

  Workloads are loaded from WORKLOADS_FILE if it exists, and saved to it with
  ctrl+s. Default: skrooge.json

Options:
  --help  Show this message and exit.

Development

This project uses uv to manage its environment. To contribute, check out the code and install the dependencies:

cd skrooge
uv sync

To run the tests:

uv run pytest

To lint and format:

uv run ruff check .
uv run ruff format .

The CLI help in the command reference is generated with cog. After changing a command or option, update it with:

uv run cog -r README.md

If you forget, CI's readme check fails. On a pull request from a branch in this repository, CI also commits the regenerated help to the branch for you; on pull requests from forks, run cog yourself.

To run the same checks automatically on each commit, install the pre-commit hooks:

uv tool install pre-commit
pre-commit install

Updating prices

The Update instance data workflow runs daily. It regenerates skrooge/instances.json from GCP's Cloud Billing Catalog API and, when anything changed, opens or updates a pull request from the automation/update-instances branch. The pull request description reports added and removed machine types, regions and cost classes, and flags prices that changed by more than 20%; review those before merging. Spot (preemptible) price changes are listed too, but not flagged, as Spot prices change often. It authenticates with workload identity federation when the GCP_WORKLOAD_IDENTITY_PROVIDER and GCP_SERVICE_ACCOUNT repository variables are set. The service account needs no roles, but its project needs the Cloud Billing API enabled. Otherwise it uses a GCP_BILLING_API_KEY repository secret holding an API key that's restricted to the Cloud Billing API.

To regenerate the data locally, with your gcloud login or an API key:

GCP_BILLING_ACCESS_TOKEN="$(gcloud auth print-access-token)" uv run python -m skrooge.scraper scrape --output new.json
# or: GCP_BILLING_API_KEY=... uv run python -m skrooge.scraper scrape --output new.json
uv run python -m skrooge.scraper diff skrooge/instances.json new.json
mv new.json skrooge/instances.json

If Google rejects your access token for not having a quota project, also set GCP_QUOTA_PROJECT to a project with the Cloud Billing API enabled. The scraper refuses to write output that fails validation, e.g. when a machine family has no prices. Machine type specs are hard-coded in skrooge/machine_types.py; add new machine types there.

Releasing

Releases are made with Sentry's Craft and follow Semantic Versioning. Anything that drops support for a Python version needs at least a minor version bump.

  1. Update the changelog. Craft doesn't edit CHANGELOG.md for this project, so in a PR, rename the ## [Unreleased] heading to ## [X.Y.Z] - YYYY-MM-DD, add a new empty ## [Unreleased] heading above it, and merge it to main. Make sure CI on main is green.
  2. Start the release. In GitHub, go to Actions → Release → Run workflow, run it on main, and enter the new version, e.g. 1.1.0. Enter an explicit version: auto and bump keywords like minor need a newer minVersion in .craft.yml than this project sets. Leave force empty unless you need to release despite an open issue labelled release-blocker.
  3. Craft prepares the release. It creates a release/X.Y.Z branch from main, runs scripts/bump-version.sh (which uses uv version to update pyproject.toml and uv.lock), commits that as release: X.Y.Z, and opens a publish: getsentry/skrooge@X.Y.Z issue in getsentry/publish.
  4. CI builds the package. The push to release/X.Y.Z runs the Publish Python Package workflow, which runs the tests and uploads the built sdist and wheel as an artifact for Craft. The publish issue links to the check runs.
  5. Craft publishes the release. skrooge is on getsentry/publish's auto-approve list, so nobody needs to add the accepted label: once the build passes, Craft uploads the package to PyPI, tags the release and creates a GitHub release, and merges the release branch back into main. Because there's no approval step, check the changelog and version before you start the release.
  6. Check it. Once PyPI has the new version, uvx skrooge@X.Y.Z --version should print it.

About

A quick and dirty kubernetes cost estimator

Topics

Resources

Code of conduct

Security policy

Stars

6 stars

Watchers

26 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages