skrooge
A quick and dirty kubernetes cost estimator
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_countandconstrained_resource: how many whole nodes the change needs, and whethercoresormemorydecides itcosts: the whole-node cost,hourly,monthlyandyearly, with its costclassandregionresource_costs: the bin-packed cost, withhourly,monthlyandyearlycosts forcores,memoryand thetotal. It'snullwhen the instance type's price can't be split into CPU and memory, as for GPU machine types.delta_cpuanddelta_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.
- Update the changelog. Craft doesn't edit
CHANGELOG.mdfor 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 tomain. Make sure CI onmainis green. - 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:autoand bump keywords likeminorneed a newerminVersionin.craft.ymlthan this project sets. Leave force empty unless you need to release despite an open issue labelledrelease-blocker. - Craft prepares the release. It creates a
release/X.Y.Zbranch frommain, runsscripts/bump-version.sh(which usesuv versionto updatepyproject.tomlanduv.lock), commits that asrelease: X.Y.Z, and opens apublish: getsentry/skrooge@X.Y.Zissue in getsentry/publish. - CI builds the package. The push to
release/X.Y.Zruns the Publish Python Package workflow, which runs the tests and uploads the built sdist and wheel as an artifact for Craft. Wait for it to pass; the publish issue links to the check runs. - Approve the release. Add the
acceptedlabel to the publish issue. Craft then uploads the package to PyPI, tags the release and creates a GitHub release, and merges the release branch back intomain. Nothing is published until the label is added, so to abandon a release, close the publish issue and delete therelease/X.Y.Zbranch. - Check it. Once PyPI has the new version,
uvx skrooge@X.Y.Z --versionshould print it.
Metadata
Release files for skrooge 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| skrooge-2.0.0.tar.gz | 306.6 kB | Without provenance | Details for skrooge-2.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| skrooge-2.0.0-py3-none-any.whl | Python 3 | none | any | Details for skrooge-2.0.0-py3-none-any.whl |
Total release size: 606.1 kB
Release files / skrooge-2.0.0.tar.gz
| Download URL | skrooge-2.0.0.tar.gz |
|---|---|
| Size | 306.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
08cc7f2d6e1903b45fe3dd2069f5c1a18a1d0359da431ccf71aa88cb4b08a0cb
|
|
BLAKE2b-256 checksum How to use checksums |
4a8c3781c906148fb0bf5cf83db423dd420f04486e8460b048a05ab0c4e48f8c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.11.2
|
Release files / skrooge-2.0.0-py3-none-any.whl
| Download URL | skrooge-2.0.0-py3-none-any.whl |
|---|---|
| Size | 299.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
96ec38336951be81c3abf6438872bb8ea94cdcf58f2fc97ba128df907aeefbcf
|
|
BLAKE2b-256 checksum How to use checksums |
4a754a16d95cec11f49c189ef5d9931bacdc1adf31c9d853ee3443adc42697a7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.11.2
|
Security for skrooge 2.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.