Skip to content
c3xdevPublic

About

Open source cloud cost estimation for Terraform, Terragrunt, and CloudFormation

Topics

Resources

Contributing

Stars

35 stars

Watchers

0 watching

Forks

Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Latest commit

Β 

History

97 Commits

Folders and files

Repository files navigation

c3x

c3x

Know what your Terraform costs before you apply it.

Open source cost estimation for Terraform, OpenTofu, Terragrunt and CloudFormation. No API key, no SaaS account, no telemetry.

Release CI Go License GitHub Marketplace

Documentation Β· Quickstart Β· CI/CD Β· Resource catalog Β· Blog

c3x estimate printing a per-resource cost breakdown for a two-resource Terraform project, totalling $202.47 per month

c3x parses your infrastructure code statically. No terraform init, no providers, no cloud credentials, no state access, so it is fast enough to sit in front of every pull request.

For input you don't control, such as pull requests from forks, use untrusted-input mode: --no-remote-modules, or C3X_NO_REMOTE_MODULES=true for every command. The GitHub Action turns it on automatically for pull requests from forks. In that mode c3x fetches no remote modules, reads no files, keeps local modules inside the scanned directory, and ignores anything in the repository's .c3x.toml that could redirect pricing, credentials or file access (only region, currency, format, budgets and an in-project usage files are honoured). Every parse is also bounded β€” at most 10,000 instances per resource, 200,000 resources, 5,000 module expansions and two minutes β€” so a hostile configuration fails fast instead of exhausting the machine. Those limits are checked between resources and modules; a single expression, such as nested for comprehensions, is evaluated to completion and can still use significant CPU and memory. If you run c3x as a service on untrusted input, give it container memory and CPU limits and a wall-clock timeout as well.

Install

Method Command
Homebrew brew install c3xdev/tap/c3x
Script curl -fsSL https://c3x.dev/install.sh | sh
Docker docker pull ghcr.io/c3xdev/c3x
Go go install github.com/c3xdev/c3x/cmd/c3x@latest

Windows, and anyone who would rather not pipe a script into a shell, can take the archive straight from the releases page, which carries macOS, Linux and Windows builds alongside checksums.txt. The container image is on GHCR.

The script detects your OS and architecture and checks the published SHA-256, aborting on a mismatch. It skips that check if it cannot fetch checksums.txt or finds no sha256 tool, so verify the download yourself if you need the guarantee.

Verify the install with c3x doctor, which checks the catalog parses, the pricing API answers, and the cache directory is writable.

Cost on every pull request

Point c3x at a Terraform plan and it comments the cost change on the PR. The summary leads with the dollar delta, and the per-resource breakdown collapses so a large estimate does not flood the thread.

c3x comment github --path plan.json

πŸ’° C3X report

Monthly cost increased by $314.82 (+30.3%) πŸ“ˆ

Baseline New Change
$1038.32/mo $1353.14/mo πŸ”Ί +$314.82
Estimate details

🟑 Modified

Resource Baseline Current Ξ”
aws_instance.web $285.32 $565.64 πŸ”Ί +$280.32
aws_db_instance.main $753 $787.5 πŸ”Ί +$34.5

The delta comes from the plan itself. A plan carries both the prior and the planned state of every resource, so there is no baseline file to manage and no base-branch checkout. Works on GitHub, GitLab, Bitbucket and Azure DevOps, and auto-detects the repository and PR number in CI.

Flag What it does
--comment-tag Keeps one comment per environment or directory on the same PR
--recreate Posts a fresh comment at the bottom instead of editing in place
--expand Posts the breakdown expanded instead of collapsed
--baseline Diffs against a saved baseline rather than the plan's prior state

In CI

The GitHub Action is on the Marketplace as C3X Cost Estimation:

name: Cost estimate
on: [pull_request]
permissions:
  contents: read
  pull-requests: write   # post the comment
  id-token: write        # optional: comment as c3x-cloud[bot]
jobs:
  c3x:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: c3xdev/c3x@v0
        with:
          path: .
          budget-delta: "50"   # fail the check if this PR adds more than $50/mo

@v0 follows every release. To have comments posted as c3x-cloud[bot] instead of github-actions, install the C3X Cloud app on the repository and keep id-token: write. The Action then fetches a short-lived token scoped to that one repository; without the app or the permission it posts as github-actions. Set branded-comments: false to never request one.

The same gates work from the CLI: --budget caps the absolute monthly cost and --budget-delta caps the increase a single change may introduce. Both exit non-zero, so a pull request that blows the budget fails the build.

If your pipeline already runs terraform plan, give c3x the plan. A directory is evaluated statically, with no cloud credentials. That is convenient, but values Terraform only knows at plan time are estimated: data sources, module outputs computed from them, and some counts. Those lines are marked unresolved_attribute, and a resource whose count or for_each rests on a data source (one NAT gateway per availability zone) is marked assumed_count. The plan has the real values:

- run: terraform plan -out=tfplan && terraform show -json tfplan > plan.json
- uses: c3xdev/c3x@v0
  with:
    path: plan.json
    budget-delta: "50"   # the plan's prior state is the baseline

See the CI/CD guide for GitLab CI, Bitbucket Pipelines, Azure Pipelines, Atlantis and Spacelift recipes.

Commands

Command What it does
c3x estimate Monthly cost of a directory, .tf file, or plan JSON
c3x diff Cost change against a saved baseline, with a delta gate for CI
c3x top Ranks resources by cost. --format targets emits -target= flags for a teardown
c3x comment Posts the estimate to a pull or merge request
c3x recommend Suggests savings: right-sizing, cheaper families, unused resources
c3x policy eval Evaluates cost policies written in Rego
c3x pricing sync Warms the local price cache for fully offline runs
c3x usage sync Writes measured usage (GB stored, ...) from your cloud account's metrics; see Usage
c3x doctor Pre-flight checks, usable as a CI gate
c3x supported-resources Lists every supported resource kind

Full flag reference: c3x.dev/docs/cli.

Usage from your cloud account

Requests, GB stored and similar usage-driven lines are $0 until someone writes a usage file. c3x usage sync measures them instead:

terraform show -json > state.json
c3x usage sync --state state.json     # writes c3x-usage.synced.yml
c3x estimate                          # reads it, no flags needed

pricing sync is not usage sync. c3x pricing sync warms the cache of prices (rates). c3x usage sync fetches quantities from your own account. Estimates never call a cloud API: only usage sync does, run by you, with your credentials, making read-only calls. It refuses to run in untrusted-input mode (--no-remote-modules), so a pull request from a fork cannot trigger calls with the runner's credentials. Credentials come from the AWS SDK's default chain (environment, shared config, SSO, instance or task role); c3x doctor checks them once a synced file is in use.

It writes only the generated file, replacing it whole on every run, and never reads or writes your hand-written usage_path file, so a re-sync cannot overwrite an edit of yours. Pricing commands read both, and yours wins per resource and per key (see Configuration). Values it cannot establish are skipped and listed, in the output and under errors: in the file; --strict makes that an error exit. A run that measures nothing at all (no credentials, no --state, a missing permission) fails and leaves the existing file as it was, rather than replacing it with an empty one.

Today: AWS S3 bucket storage (standard_storage_gb, from CloudWatch's daily BucketSizeBytes). It needs cloudwatch:GetMetricData, which AWS bills per metric requested. The real bucket name comes from --state; without it a literal name in the configuration is used. S3 requests, Lambda, NAT gateway, CloudFront and GCP follow; see the design and ROADMAP section K.

How much to trust a number

c3x prices at public on-demand list rates, in USD, before any discount (reserved instances, savings plans, committed use, negotiated rates). For compute and managed databases in the provider's main region, expect estimates within a few percent of the list price; treat them as an upper bound on what a team with commitments pays.

When part of an estimate rests on an assumption, c3x says so on the line and in the total, marked ⚠, in every output format including the PR comment:

Caveat Meaning
region_fallback No price for your region under the catalog's filters, so the reference region's (us-east-1, eastus, us-central1) is shown
no_price The lookup matched nothing, so a non-free resource shows $0
usage_not_provided A usage-driven cost (requests, GB processed, LCUs) is $0 because no usage was given; supply it with --usage, or measure it with c3x usage sync
unresolved_attribute An attribute the price depends on could not be evaluated statically, so a default was used; a plan JSON input avoids this
assumed_count The resource's count or for_each (or its module's) is computed from a data source c3x cannot query, such as data.aws_availability_zones, so the number of instances rests on a placeholder (three zones in the provider's region); the caveat names the value assumed, and a plan JSON input gives the exact count
stale_price The pricing API was unreachable, so a cached price past its freshness window was used

--strict makes any of these fail the run with exit code 3, distinct from a budget breach (1), so a pipeline can require an estimate with no assumptions in it. JSON output carries caveat_count and per-line caveats.

OpenTofu

c3x reads OpenTofu projects directly. .tofu files are loaded alongside .tf, and a main.tofu replaces the main.tf of the same name, as OpenTofu does, so a codebase that ships both is not counted twice. JSON-syntax files (.tf.json, .tofu.json) and override files (override.tf, *_override.tf, and their .tofu and JSON forms) are read too, with override files merged into the blocks they override. Provider for_each is supported: each instance of a resource is priced in the region of the provider instance it uses. Plan JSON from tofu show -json works the same as Terraform's.

File functions

file(), templatefile(), fileset() and the other filesystem functions are off by default, because on a pull request from a fork the configuration is untrusted, and a file read could leak something an earlier CI step wrote into the workspace. Configurations that load settings from files, such as yamldecode(file("${path.module}/fleet.yaml")), get a warning naming the function instead of a silently wrong estimate.

For your own repository, turn them on with --allow-file-functions or C3X_ALLOW_FILE_FUNCTIONS=true. Reads stay inside the project directory, with symlinks resolved first. The setting is not read from a project's .c3x.toml, since a pull request can edit that file, and --no-remote-modules always turns it off.

Output formats

text, markdown, json, junit, html, csv and sarif. JUnit feeds CI dashboards, SARIF feeds code-scanning surfaces, and JSON is the machine-readable contract:

c3x estimate --format json | jq .project_total

Catalog

1,340 recognized resource kinds across AWS, Azure and Google Cloud, served from pricing.c3x.dev. Of those, 263 carry a price and the rest are legitimately free. At estimate time c3x loads the catalog remote-first: the live /catalog bundle, then the on-disk cache, then an embedded snapshot for fully offline use. Every kind is classified:

Class Meaning
live Every dimension prices from the upstream API and tracks vendor price changes
static Inline-rate fallback for products the upstream does not expose cleanly
free Legitimately free, such as public ACM certificates
zero / errored A hard CI failure if non-empty

Regenerate the health report against the live API with go run ./cmd/verify_catalog, which prints the current classification counts and fails on anything unpriced.

Performance

Workload Time
Parse a 500-resource monorepo (vars, locals, count, for_each, modules) 10ms (Apple M3 Pro)
Cold estimate against pricing.c3x.dev 3 to 5 seconds (network-bound)
Warm estimate (SQLite cache hit) 35ms

From internal/parser/terraform/bench_test.go. CI runs the benchmark on every push, so regressions fail the build.

Configuration

c3x reads from five layers, in increasing priority:

  1. Built-in defaults
  2. User config: ~/.config/c3x/config.toml
  3. Project config: ./.c3x.toml
  4. Environment variables prefixed C3X_
  5. CLI flags

Project config example (.c3x.toml):

region = "us-east-1"
format = "markdown"
budget = 1000.0              # same as --budget; --budget 0 overrides it
budget_delta = 50.0          # same as --budget-delta on c3x diff
usage_path = "c3x-usage.yml" # relative to this file; used by every command
synced_usage_path = "c3x-usage.synced.yml" # generated usage; see below

[pricing]
endpoint = "https://pricing.c3x.dev/graphql"

The other keys are currency, offline, no_cache, cache_path, no_remote_modules, and pricing.token (keep that one out of the repository). c3x warns about any key it doesn't recognise.

Usage can come from two files. usage_path is the one you write. synced_usage_path is a generated snapshot (c3x-usage.synced.yml, picked up automatically when it sits next to .c3x.toml) with the same schema, which you never edit by hand. Every command that prices reads both, and your file wins per resource and per key: a value you write is never replaced by a generated one, and a generated value fills in what you leave out. A value measured for one resource beats a defaults: entry for its whole kind.

Self-hosting the pricing API? Point pricing.endpoint at it and set C3X_PRICING_TOKEN if it runs with API_KEY enabled. See c3x-pricing-api.

Architecture
cmd/c3x/                 # CLI entry (cobra + viper)
cmd/verify_catalog/      # Catalog health harness
internal/
β”œβ”€β”€ domain/              # Core types: Resource, Cost, Estimate, Diff
β”œβ”€β”€ config/              # 5-layer configuration resolution
β”œβ”€β”€ observability/       # slog + OpenTelemetry tracer
β”œβ”€β”€ catalog/             # TOML resource definitions + Registry
β”œβ”€β”€ expr/                # Expression evaluator (expr-lang/expr wrapped)
β”œβ”€β”€ parser/              # IaC parser dispatcher
β”‚   β”œβ”€β”€ terraform/       # HCL parsing (hashicorp/hcl/v2)
β”‚   └── plan/            # Terraform plan JSON
β”œβ”€β”€ pricing/             # HTTPSource β†’ DiskCache β†’ MemoCache chain
β”œβ”€β”€ calculator/          # Orchestrator
└── render/              # text / markdown / json renderers

resources/               # Embedded TOML catalog snapshot, the
                         # offline fallback shipped in the binary
testdata/corpus/         # Real-world Terraform used by integration tests

Module dependencies flow inward toward domain. The depguard linter enforces the boundaries in CI, so an import that crosses the wrong line fails the lint job before review.

Stability

c3x is pre-1.0 and versioned honestly. Flags, environment variables and config keys are not removed without a deprecation cycle, enforced by a CLI-surface snapshot test that fails CI on a silent removal. See COMPATIBILITY.md.

Contributing

Issues and pull requests are welcome. CONTRIBUTING.md covers the layout, the catalog format, and how to add a resource kind.

License

Apache License 2.0, see LICENSE.

About

Open source cloud cost estimation for Terraform, Terragrunt, and CloudFormation

Topics

Resources

Contributing

Stars

35 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages