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
  • 🔌 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:

Resource Type Status Actual(MTD) Projected Delta Drift% Recs
my-instance aws:ec2/instance:I... ✓ $12.40 $15.00 $2.60 +8% 2
my-bucket aws:s3/bucket:Bucket ✓ $0.83 $1.00 $0.17 0% 0
my-db aws:rds/instance:I... ✓ $48.20 $50.00 $1.80 -3% 1
Total Actual (MTD): $61.43 Projected Monthly: $66.00 Potential Savings: $45.00

Full documentation: docs/commands/overview.md

Projected Costs - Estimate costs before deployment:

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

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.yaml.

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

See Budget Guide for full configuration details.

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 [--format json|yaml]
finfocus config validate [--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.yaml:

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
#
#

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
PluginStatusDescription
aws-publicAvailableAWS public pricing data
aws-ceIn DevelopmentAWS Cost Explorer integration
azure-publicIn DevelopmentAzure public pricing data
kubecostPlannedKubernetes cost analysis

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

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:

SkillDescription
finfocus-installInstall CLI, detect providers, setup plugins and config
finfocus-analyzer-setupConfigure Pulumi Analyzer for inline cost estimation
finfocus-routingConfigure plugin routing with priority and fallback

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.