Cloud cost analysis for Pulumi infrastructure
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 previewoutput - 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:
curl -fsSL https://raw.githubusercontent.com/rshade/finfocus/main/scripts/install.sh | shBuild from source:
git clone https://github.com/rshade/finfocus
cd finfocus
make build
./bin/finfocus --helpManual download (pin to a specific version)
# Linux (amd64)
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/
# macOS (Apple Silicon)
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/
# macOS (Intel)
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/# Windows - installs to a user-local directory, no admin rights required
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"
# Add to PATH for the current session (add to $PROFILE for a permanent effect)
$env:PATH = "$installDir;$env:PATH"Export your infrastructure plan to JSON:
cd your-pulumi-project
pulumi preview --json > plan.jsonSee all costs at a glance with the interactive dashboard. From inside any Pulumi project directory, just run finfocus with no arguments:
finfocusOr invoke the subcommand directly:
finfocus overviewFor CI/CD or when you manage the export step yourself:
# Pre-export state and plan
pulumi stack export > state.json
pulumi preview --json > plan.json
# Launch overview with pre-exported files
finfocus overview --pulumi-state state.json --pulumi-json plan.json --plain --yesExample 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
Run from the folder containing your Pulumi program:
finfocus --webFinFocus starts a local server, prints its session URL, and opens your browser.
Use --no-browser on SSH or CI, and --port 8484 when you need a fixed port.
The Overview, Cost, Recommendations, and Estimate views use the same Go engine
and plugins as the CLI and TUI. Keyboard navigation, resource details, cluster
expansion, and light/dark themes are available in the browser.
Sources follow finfocus overview: auto-detect the current Pulumi project and
stack, or pass --pulumi-json plan.json / --pulumi-state state.json.
--stack, --from, --to, --adapter, and --filter are also accepted.
These source flags, --port, and --no-browser are root-local companions to
--web; they are rejected without it. --web is mutually exclusive with --mcp.
Terraform inputs are outside the web UI's current scope.
The Cost view's group selector and tag filter, and the Recommendations view's Include dismissed toggle, add browser controls for existing CLI flags. The TUI fixes grouping at launch and has no include-dismissed toggle. Estimate changes are temporary what-if calculations; the dashboard cannot change infrastructure or dismiss recommendations. Use Run preview for an on-demand Pulumi preview and restart the session to load fresh actual costs.
Keep the printed URL private: its token opens an authenticated local session. The server binds to loopback, replaces the token in the address bar with an HTTP-only session cookie, and checks request origins. Encrypted stacks use a masked browser passphrase prompt with retries. Stop the server with Ctrl+C; closing a browser tab leaves it running.
See the web UI guide for launch options, storage behavior, and browser test commands.
Projected Costs - Estimate costs before deployment:
finfocus cost projected --pulumi-json plan.jsonForecast - 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.
finfocus cost forecast --pulumi-json plan.json --growth-type linear --growth-rate 0.10Check Budget - Verify if plan fits within budget:
# Configure budget in ~/.finfocus/config.hujson (see Configuration section)
# Then check projected cost against budget
finfocus cost projected --pulumi-json plan.json
# Exit with non-zero code if budget exceeded (CI/CD integration)
finfocus cost projected --pulumi-json plan.json --exit-on-threshold
# Custom exit code (default: 1)
finfocus cost projected --pulumi-json plan.json --exit-on-threshold --exit-code 2
# Filter budget scope (global, provider:<name>, tag:<key>=<value>, type:<resource-type>)
finfocus cost projected --pulumi-json plan.json --budget-scope "provider:aws"View Recommendations - Find savings opportunities:
finfocus cost recommendations --pulumi-json plan.json$ 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$ 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": [...]
}- Export - Generate Pulumi plan JSON with
pulumi preview --json - Parse - Extract resource definitions and properties
- Query - Fetch cost data via plugins or local specifications
- Aggregate - Calculate totals with grouping and filtering options
- 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.
# yaml-language-server: $schema=https://rshade.github.io/finfocus/schemas/config.json
cost:
budgets:
amount: 500.00
currency: USD
alerts:
- threshold: 80
type: actual
- threshold: 100
type: forecastedSee 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:
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:
# AWS credentials (for aws-public and aws-ce plugins)
export FINFOCUS_PLUGIN_AWS_ACCESS_KEY_ID="your-access-key"
export FINFOCUS_PLUGIN_AWS_SECRET_ACCESS_KEY="your-secret-key"
# Azure credentials (for azure-public plugin)
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:projecton both the old and new resources, billing APIs return the full month's costs.Important: Pulumi does not automatically tag cloud resources with
pulumi:projectorpulumi: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)
# Pulumi.dev.yaml
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",
},
})
# Use this provider for all resources
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_projectinstead ofpulumi:project.
Option A: Stack configuration
# Pulumi.dev.yaml
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-provisionedlabel to all resources. Opt out withgcp: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:
# ~/.finfocus/config.hujson
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.
# ~/.finfocus/config.hujson
cost:
history:
enabled: true # default: true
retention_days: 90 # how long to keep entries (default: 90)# Filter by resource type
finfocus cost projected --pulumi-json plan.json --filter "type=aws:ec2/instance"# Table format (default)
finfocus cost projected --pulumi-json plan.json --output table
# JSON for API integration
finfocus cost projected --pulumi-json plan.json --output json
# NDJSON for streaming/pipeline processing
finfocus cost projected --pulumi-json plan.json --output ndjsonFinFocus provides commands to manage configuration:
# Initialize configuration (creates ~/.finfocus/config.hujson)
finfocus config init [--force]
# Set configuration values
finfocus config set cost.budgets.amount 500.00
finfocus config set output.format json
# Get configuration values
finfocus config get cost.budgets.amount
# List all configuration
finfocus config list [--as json|yaml]
# Validate configuration
finfocus config validate [--file path] [--output text|json] [--verbose]
# Inspect effective routing configuration
finfocus config routes list [--output table|json]
# Simulate plugin routing for a resource type
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:
# Install multiple plugins
finfocus plugin install aws-public
finfocus plugin install gcp-public
# View plugin capabilities
finfocus plugin list --verbose
# NAME VERSION PROVIDERS CAPABILITIES SPEC PATH
# aws-public 1.0.0 aws ProjectedCosts, ActualCosts 0.5.5 ~/.finfocus/plugins/aws-public/1.0.0/...
# gcp-public 1.0.0 gcp ProjectedCosts, ActualCosts 0.5.5 ~/.finfocus/plugins/gcp-public/1.0.0/...
# Cost calculation automatically routes resources
finfocus cost projected --pulumi-json plan.json
# AWS resources → aws-public
# GCP resources → gcp-publicFor 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: 30Key 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
finfocus config validate [--file path] [--output text|json]
# Output (success):
# Configuration Validation
# Syntax valid
# Budget configuration valid
# Configuration is valid--verbose prints loaded output, plugin, and routing settings after a valid text report.
See the Routing Configuration Guide for detailed examples and troubleshooting.
# List installed plugins
finfocus plugin list
# List with detailed capabilities
finfocus plugin list --verbose
# Install plugins
finfocus plugin install aws-public
finfocus plugin install vantage
# Inspect plugin capabilities
finfocus plugin inspect aws-public
# Validate plugin installation
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:
# Start the analyzer server (prints port to stdout for Pulumi handshake)
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:
# Global debug flag
finfocus --debug cost projected --pulumi-json plan.json
# Environment variable
export FINFOCUS_LOG_LEVEL=debug
export FINFOCUS_LOG_FORMAT=json # json or consoleFinFocus 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:
# Emit command schema in AX format (default)
finfocus __schema
# Emit command schema in MCP format
finfocus __schema --as=mcpFinFocus 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.
# Serve over stdio (what MCP clients launch); the two forms are equivalent
finfocus --mcp
finfocus mcp-server
# Serve over HTTP (loopback-only by default)
finfocus mcp-server --transport=http --addr=127.0.0.1:8080
# Serve over HTTP from any address (requires explicit opt-in)
finfocus mcp-server --transport=http --addr=0.0.0.0:8080 --allow-non-loopbackRegister it with Claude Code:
claude mcp add finfocus -- finfocus --mcpOr 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:
- 👤 End Users: User Guide - How to install and use FinFocus
- 💰 Budgets: Budget Guide - Configure alerts and thresholds
- 📜 Resource History: Resource History Guide - Accurate month-long costs
- 💡 Recommendations: Recommendations Guide - Optimization insights
- ♿ Accessibility: Accessibility Guide - UI configuration
- 🛠️ Engineers: Developer Guide - How to extend and contribute
- 🏗️ Architects: Architect Guide - System design and integration
- 🧪 E2E Testers: E2E Testing Guide - Setup and execution
- 💼 Business/CEO: Business Value - ROI and competitive advantage
Quick Links:
- 🚀 5-Minute Quickstart
- 📖 Full Documentation Index
- 🔌 Available Plugins - AWS Public Pricing and more
- 🛠️ Plugin Development
- 🏗️ System Architecture
- 💬 FAQ & Support
We welcome contributions! See our development documentation:
- CONTRIBUTING.md - Development setup and guidelines
- CLAUDE.md - AI assistant development context
- Architecture Documentation - Internal package 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.
- finfocus-spec - Protocol definitions and schemas
- finfocus-plugin-aws-public - AWS public pricing plugin
- finfocus-action - GitHub Action that posts cost estimates on pull requests
- finfocus-demo - Live demo of finfocus-action; see PR #1 for the output
- agent-skills - Generic cost workflow skills (multi-tool)
Getting Started: Try the examples directory for sample Pulumi plans and pricing specifications.
{ "cost": { "budgets": { "global": { "amount": 500, "currency": "USD", "alerts": [ { "threshold": 80, "type": "actual", "notifications": [{"type": "slack", "url": "${FINFOCUS_NOTIFY_SLACK_URL}"}] } ] } } } }