Skip to content

Project README

Cloud cost analysis for Pulumi infrastructure

CI codecov Go Report Card Go Version Release License: Apache-2.0

Cloud cost analysis for Pulumi infrastructure - Calculate projected and actual infrastructure costs without modifying your Pulumi programs.

FinFocus is a CLI tool that analyzes Pulumi infrastructure definitions to provide accurate cost estimates, budget enforcement, and historical cost tracking through a flexible plugin-based architecture.

Cloud cost surprises are the norm. Teams deploy infrastructure with Pulumi but have no visibility into what it will cost until the bill arrives. FinFocus closes that gap:

  • Shift-left on costs — See projected costs before you deploy, directly from pulumi preview output
  • No code changes required — Works with any existing Pulumi project via JSON export
  • Budget guardrails — Enforce spending limits in CI/CD pipelines with non-zero exit codes
  • Plugin architecture — Swap pricing sources without changing your workflow
  • Single dashboard — Interactive TUI combining actual spend, projected costs, drift analysis, and recommendations
  • 🔭 Unified Overview: Interactive dashboard combining actual costs, projected costs, drift analysis, and recommendations in a single view
  • 📊 Projected Costs: Estimate monthly costs before deploying infrastructure
  • 💰 Budgets & Alerts: Hierarchical budgets (global, provider, tag, type) with CI/CD thresholds
  • 💡 Recommendations: Actionable cost optimization insights and savings opportunities
  • ♿ Accessibility: High-contrast, plain text, and adaptive terminal UI modes
  • 💰 Actual Costs: Track historical spending with detailed breakdowns
  • ☸️ Cluster Cost Allocation: Split Kubernetes cluster cost by namespace, controller, pod, node, or label with conservation guarantees
  • 🔌 Plugin-Based: Extensible architecture supporting multiple cost data sources
  • 🧪 E2E Testing: Comprehensive guide for validating infrastructure costs against real cloud resources
  • 📈 Advanced Analytics: Resource grouping, filtering, and aggregation
  • 📱 Multiple Formats: Table, JSON, and NDJSON output options
  • 🔍 Smart Filtering: Filter by resource type, tags, or custom expressions
  • 🏗️ No Code Changes: Works with existing Pulumi projects via JSON output

Install script (recommended) – auto-detects OS/architecture and downloads the latest release:

Terminal window
curl -fsSL https://raw.githubusercontent.com/rshade/finfocus/main/scripts/install.sh | sh

Build from source:

Terminal window
git clone https://github.com/rshade/finfocus
cd finfocus
make build
./bin/finfocus --help
Manual download (pin to a specific version)
Terminal window
curl -L https://github.com/rshade/finfocus/releases/download/v0.3.3/finfocus-v0.3.3-linux-amd64.tar.gz -o finfocus.tar.gz
tar -xzf finfocus.tar.gz
chmod +x finfocus
sudo mv finfocus /usr/local/bin/
curl -L https://github.com/rshade/finfocus/releases/download/v0.3.3/finfocus-v0.3.3-macos-arm64.tar.gz -o finfocus.tar.gz
tar -xzf finfocus.tar.gz && chmod +x finfocus && sudo mv finfocus /usr/local/bin/
curl -L https://github.com/rshade/finfocus/releases/download/v0.3.3/finfocus-v0.3.3-macos-amd64.tar.gz -o finfocus.tar.gz
tar -xzf finfocus.tar.gz && chmod +x finfocus && sudo mv finfocus /usr/local/bin/
Terminal window
Invoke-WebRequest -Uri "https://github.com/rshade/finfocus/releases/download/v0.3.3/finfocus-v0.3.3-windows-amd64.zip" -OutFile finfocus.zip
Expand-Archive finfocus.zip -DestinationPath .
$installDir = "$env:LocalAppData\Programs\finfocus"
New-Item -ItemType Directory -Force -Path $installDir | Out-Null
Move-Item finfocus.exe "$installDir\finfocus.exe"
$env:PATH = "$installDir;$env:PATH"

Export your infrastructure plan to JSON:

Terminal window
cd your-pulumi-project
pulumi preview --json > plan.json

See all costs at a glance with the interactive dashboard. From inside any Pulumi project directory, just run finfocus with no arguments:

Terminal window
finfocus

Or invoke the subcommand directly:

Terminal window
finfocus overview

For CI/CD or when you manage the export step yourself:

Terminal window
pulumi stack export > state.json
pulumi preview --json > plan.json
finfocus overview --pulumi-state state.json --pulumi-json plan.json --plain --yes

Example plain text output (--plain). Amounts are sample data:

RESOURCE TYPE STATUS ACTUAL(MTD) PROJECTED DELTA DRIFT% RECS WARN
-------- ---- ------ ----------- --------- ----- ------ ---- ----
my-instance aws:ec2/instance:Inst... ✓ active $12.40 $15.00 +$6.20 +18% ⚠ 2 drift
my-bucket aws:s3/bucket:Bucket ✓ active $0.83 $1.00 - - - -
my-db aws:rds/instance:Inst... ✓ active $48.20 $50.00 -$8.40 -15% ⚠ 1 drift
SUMMARY prod 3 resources $61.43 USD $66.00 USD -$2.20 USD

Full documentation: Overview command

Projected Costs - Estimate costs before deployment:

Terminal window
finfocus cost projected --pulumi-json plan.json

Forecast - Project that monthly cost forward. Plugins report a growth model. --growth-rate 0.10 is 10% per month. The chart is plain text, and --output json returns the same points with timestamps.

Terminal window
finfocus cost forecast --pulumi-json plan.json --growth-type linear --growth-rate 0.10

Check Budget - Verify if plan fits within budget:

Terminal window
finfocus cost projected --pulumi-json plan.json
finfocus cost projected --pulumi-json plan.json --exit-on-threshold
finfocus cost projected --pulumi-json plan.json --exit-on-threshold --exit-code 2
finfocus cost projected --pulumi-json plan.json --budget-scope "provider:aws"

View Recommendations - Find savings opportunities:

Terminal window
finfocus cost recommendations --pulumi-json plan.json
Terminal window
$ finfocus cost projected --pulumi-json examples/plans/aws-simple-plan.json
Budget: $500.00 (75% used)
[=====================>......] $375.00 / $500.00
RESOURCE ADAPTER MONTHLY CURRENCY NOTES
aws:ec2/instance:Instance aws-spec $375.00 USD t3.xlarge
aws:s3/bucket:Bucket none $0.00 USD No pricing info
Terminal window
$ finfocus cost actual --pulumi-json plan.json --from 2025-01-01 --group-by type --output json
{
"summary": {
"totalMonthly": 45.67,
"currency": "USD",
"byProvider": {"aws": 45.67},
"byService": {"ec2": 23.45, "s3": 12.22, "rds": 10.00}
},
"resources": [...]
}
  1. Export - Generate Pulumi plan JSON with pulumi preview --json
  2. Parse - Extract resource definitions and properties
  3. Query - Fetch cost data via plugins or local specifications
  4. Aggregate - Calculate totals with grouping and filtering options
  5. Output - Present results in table, JSON, or NDJSON format

FinFocus uses plugins to fetch cost data from various sources:

  • Cost Plugins: Query cloud provider APIs (AWS Public Pricing, AWS Cost Explorer, Azure, etc.)
  • Spec Files: Local YAML/JSON pricing specifications as fallback
  • Plugin Discovery: Automatic detection from ~/.finfocus/plugins/

FinFocus is configured via ~/.finfocus/config.hujson. Configuration files use Hujson format (JSON with // comments and trailing commas allowed). Legacy config.yaml files are automatically migrated to Hujson format on first read.

cost:
budgets:
amount: 500.00
currency: USD
alerts:
- threshold: 80
type: actual
- threshold: 100
type: forecasted

See Budget Guide for full configuration details.

Send a Slack message or a JSON event to an HTTPS webhook when an alert threshold is crossed. Add destinations to an alert in the global config:

{
"cost": {
"budgets": {
"global": {
"amount": 500,
"currency": "USD",
"alerts": [
{
"threshold": 80,
"type": "actual",
"notifications": [{"type": "slack", "url": "${FINFOCUS_NOTIFY_SLACK_URL}"}]
}
]
}
}
}
}

Notifications are opt-in per run (--notify or FINFOCUS_NOTIFY=true), never change the exit code, and only expand ${FINFOCUS_NOTIFY_*} variables from the global config. See Budget Notifications.

For sensitive values like API keys and credentials, use environment variables:

Terminal window
export FINFOCUS_PLUGIN_AWS_ACCESS_KEY_ID="your-access-key"
export FINFOCUS_PLUGIN_AWS_SECRET_ACCESS_KEY="your-secret-key"
export FINFOCUS_PLUGIN_AZURE_SUBSCRIPTION_ID="your-subscription-id"

The naming convention is: FINFOCUS_PLUGIN_<PLUGIN_NAME>_<KEY_NAME> in uppercase.

For accurate month-long cost tracking — especially when resources are replaced or destroyed mid-month — FinFocus can query cloud billing APIs by resource tags. This requires your Pulumi-managed resources to carry consistent tags.

Why this matters: When a resource is replaced (e.g., EC2 instance swap), the old cloud ID disappears from Pulumi state. Without tags, FinFocus can only see costs for the current resource. With tags like pulumi:project on both the old and new resources, billing APIs return the full month’s costs.

Important: Pulumi does not automatically tag cloud resources with pulumi:project or pulumi:stack. These are stack-level metadata only. You must explicitly configure resource tagging using one of the methods below.

AWS — Provider Default Tags

Option A: Stack configuration (static values)

config:
aws:defaultTags:
tags:
pulumi:project: my-app
pulumi:stack: dev
Environment: development
CostCenter: "12345"

Option B: Explicit provider in code (dynamic values)

TypeScript:

const provider = new aws.Provider("tagged", {
defaultTags: {
tags: {
"pulumi:project": pulumi.getProject(),
"pulumi:stack": pulumi.getStack(),
"Environment": "dev",
},
},
});
// Use this provider for all resources
const bucket = new aws.s3.Bucket("data", {}, { provider });

Go:

provider, _ := aws.NewProvider(ctx, "tagged", &aws.ProviderArgs{
DefaultTags: &aws.ProviderDefaultTagsArgs{
Tags: pulumi.StringMap{
"pulumi:project": pulumi.String(ctx.Project()),
"pulumi:stack": pulumi.String(ctx.Stack()),
"Environment": pulumi.String("dev"),
},
},
})
// Use this provider for all resources
bucket, _ := s3.NewBucket(ctx, "data", nil, pulumi.Provider(provider))

Python:

provider = aws.Provider("tagged", default_tags={
"tags": {
"pulumi:project": pulumi.get_project(),
"pulumi:stack": pulumi.get_stack(),
"Environment": "dev",
},
})
bucket = aws.s3.Bucket("data", opts=pulumi.ResourceOptions(provider=provider))

After tagging: Activate pulumi:project and pulumi:stack as Cost Allocation Tags in the AWS Billing Console. Tags take ~24 hours to appear in Cost Explorer.

GCP — Provider Default Labels

GCP labels must be lowercase with hyphens/underscores only (no colons). Use pulumi_project instead of pulumi:project.

Option A: Stack configuration

config:
gcp:defaultLabels:
pulumi_project: my-app
pulumi_stack: dev
environment: development
cost_center: "12345"

Option B: Explicit provider in code

TypeScript:

const provider = new gcp.Provider("tagged", {
defaultLabels: {
pulumi_project: pulumi.getProject(),
pulumi_stack: pulumi.getStack(),
environment: "dev",
},
});

Go:

provider, _ := gcp.NewProvider(ctx, "tagged", &gcp.ProviderArgs{
DefaultLabels: pulumi.StringMap{
"pulumi_project": pulumi.String(ctx.Project()),
"pulumi_stack": pulumi.String(ctx.Stack()),
"environment": pulumi.String("dev"),
},
})

Python:

provider = gcp.Provider("tagged", default_labels={
"pulumi_project": pulumi.get_project(),
"pulumi_stack": pulumi.get_stack(),
"environment": "dev",
})

Note: GCP v8.0+ automatically adds the goog-pulumi-provisioned label to all resources. Opt out with gcp:addPulumiAttributionLabel = false.

Azure — Stack Transformations

Azure Native does not have a provider-level defaultTags option. Use a stack transformation to inject tags into all taggable resources:

TypeScript:

pulumi.runtime.registerStackTransformation((args) => {
if (args.props["tags"] !== undefined) {
args.props["tags"] = {
...args.props["tags"],
"pulumi:project": pulumi.getProject(),
"pulumi:stack": pulumi.getStack(),
"Environment": "dev",
};
}
return { props: args.props, opts: args.opts };
});

Go:

ctx.RegisterStackTransformation(func(args *pulumi.ResourceTransformationArgs) *pulumi.ResourceTransformationResult {
if tags, ok := args.Props["tags"]; ok {
if tagMap, ok := tags.(pulumi.StringMap); ok {
tagMap["pulumi:project"] = pulumi.String(ctx.Project())
tagMap["pulumi:stack"] = pulumi.String(ctx.Stack())
tagMap["Environment"] = pulumi.String("dev")
args.Props["tags"] = tagMap
}
}
return &pulumi.ResourceTransformationResult{Props: args.Props, Opts: args.Opts}
})

Python:

def auto_tags(args: pulumi.ResourceTransformationArgs):
if "tags" in args.props:
args.props["tags"] = {
**args.props["tags"],
"pulumi:project": pulumi.get_project(),
"pulumi:stack": pulumi.get_stack(),
"Environment": "dev",
}
return pulumi.ResourceTransformationResult(args.props, args.opts)
pulumi.runtime.register_stack_transformation(auto_tags)

FinFocus configuration for tag-based cost queries:

cost:
allocation:
enabled: true
tags:
- "pulumi:project"
- "pulumi:stack"
- "Environment"
- "CostCenter"

FinFocus maintains a resource history database (~/.finfocus/history/history.db) that tracks which cloud resource IDs existed over time. This enables accurate month-long cost queries even when resources are replaced or destroyed mid-month.

How it works: Every time FinFocus runs, it snapshots the current state of your resources. Over time, this builds a timeline of all cloud IDs each logical resource has had. When you query actual costs for a full month, FinFocus queries billing APIs for all historical IDs — not just the current one.

Important: Unlike the cache (cache.db), the history database is not safe to delete. Deleting it loses the resource identity timeline, reducing cost accuracy for past periods. Back up ~/.finfocus/history/ if you back up your infrastructure configuration.

cost:
history:
enabled: true # default: true
retention_days: 90 # how long to keep entries (default: 90)
Terminal window
finfocus cost projected --pulumi-json plan.json --filter "type=aws:ec2/instance"
Terminal window
finfocus cost projected --pulumi-json plan.json --output table
finfocus cost projected --pulumi-json plan.json --output json
finfocus cost projected --pulumi-json plan.json --output ndjson

FinFocus provides commands to manage configuration:

Terminal window
finfocus config init [--force]
finfocus config set cost.budgets.amount 500.00
finfocus config set output.format json
finfocus config get cost.budgets.amount
finfocus config list [--as json|yaml]
finfocus config validate [--file path] [--output text|json] [--verbose]
finfocus config routes list [--output table|json]
finfocus config routes test aws:ec2:Instance [region] [--output table|json]

FinFocus intelligently routes resources to appropriate plugins based on provider, resource patterns, and feature capabilities.

Resources automatically route to plugins based on their supported providers:

Terminal window
finfocus plugin install aws-public
finfocus plugin install gcp-public
finfocus plugin list --verbose
finfocus cost projected --pulumi-json plan.json

Declarative Routing (Advanced Configuration)

Section titled “Declarative Routing (Advanced Configuration)”

For advanced control, configure plugin routing in ~/.finfocus/config.hujson:

routing:
plugins:
# Route recommendations to AWS Cost Explorer (higher accuracy)
- name: aws-ce
features:
- Recommendations
priority: 20
fallback: true
# Route projected costs to AWS Public (no credentials needed)
- name: aws-public
features:
- ProjectedCosts
- ActualCosts
priority: 10
fallback: true
# Route EKS resources to specialized plugin (highest priority)
- name: eks-costs
patterns:
- type: glob
pattern: "aws:eks:*"
priority: 30

Key Features:

  • Priority-Based Selection: Higher priority plugins are queried first (default: 0)
  • Automatic Fallback: If a plugin fails, automatically try the next priority
  • Pattern Matching: Use glob or regex patterns to route specific resource types
  • Feature Routing: Assign different plugins for different capabilities
Terminal window
finfocus config validate [--file path] [--output text|json]

--verbose prints loaded output, plugin, and routing settings after a valid text report.

See the Routing Configuration Guide for detailed examples and troubleshooting.

Terminal window
finfocus plugin list
finfocus plugin list --verbose
finfocus plugin install aws-public
finfocus plugin install vantage
finfocus plugin inspect aws-public
finfocus plugin validate
Plugin Status Description
aws-public Available AWS public pricing data
aws-ce In Development AWS Cost Explorer integration
azure-public In Development Azure public pricing data
opencost Available Kubernetes allocation cost from OpenCost or Kubecost
jev Available Opt-in recommendation scorer using TypeSafe AI’s Jev model. Needs TYPESAFE_API_KEY and sends recommendation data (pseudonymized by default) to TypeSafe. See the plugin README and the scoring guide

FinFocus provides zero-click cost estimation during pulumi preview via the Pulumi Analyzer protocol:

Terminal window
finfocus analyzer serve [--debug]

When integrated with Pulumi, costs are automatically calculated and displayed as advisory diagnostics during preview, without modifying your Pulumi programs. The analyzer uses ADVISORY enforcement and never blocks deployments.

Enable debug output for troubleshooting:

Terminal window
finfocus --debug cost projected --pulumi-json plan.json
export FINFOCUS_LOG_LEVEL=debug
export FINFOCUS_LOG_FORMAT=json # json or console

FinFocus supports agent and tooling integration through several features:

All commands support these global flags for programmatic integration:

  • --format json|human: Output format selection (JSON for machine consumption, human for terminal)
  • --dry-run: Preview changes without making them (affects: plugin install/update/remove, analyzer install/uninstall, config init/set, plugin upgrade, recommendation operations)
  • --yes: Skip all confirmation prompts (equivalent to approving interactive operations)
  • --idempotency-key: Opaque retry-deduplication key for distributed systems

FinFocus can be introspected by agents and tools:

Terminal window
finfocus __schema
finfocus __schema --as=mcp

FinFocus has a built-in Model Context Protocol (MCP) server. MCP clients such as Claude Code and Claude Desktop can call finfocus commands as tools, for example finfocus-cost-projected and finfocus-plugin-list. Tool calls return JSON.

Terminal window
finfocus --mcp
finfocus mcp-server
finfocus mcp-server --transport=http --addr=127.0.0.1:8080
finfocus mcp-server --transport=http --addr=0.0.0.0:8080 --allow-non-loopback

Register it with Claude Code:

Terminal window
claude mcp add finfocus -- finfocus --mcp

Or add it to Claude Desktop’s claude_desktop_config.json:

{
"mcpServers": {
"finfocus": { "command": "/path/to/finfocus", "args": ["--mcp"] }
}
}

The built-in server replaces the standalone finfocus-mcp (formerly pulumicost-mcp) server. See the MCP guide for the tool list, output and safety arguments, and the mapping from the old server’s tools.

Complete documentation is available in the docs/ directory:

Quick Links:

We welcome contributions! See our development documentation:

Apache-2.0 - See LICENSE for details.

FinFocus ships agent skills for AI coding assistants (Claude Code, Gemini CLI, etc.) that automate common workflows:

Skill Description
finfocus-install Install CLI, detect providers, setup plugins and config
finfocus-analyzer-setup Configure Pulumi Analyzer for inline cost estimation
finfocus-routing Configure plugin routing with priority and fallback
finfocus-budget Configure budget thresholds, health, and CI exit codes
finfocus-diagnose Debug connectivity, config resolution, cache, and zero cost

See agent-skills/README.md for the full list and planned skills.


Getting Started: Try the examples directory for sample Pulumi plans and pricing specifications.