VeeCode Infracost brings cloud cost awareness into your Backstage. It reads estimates produced by Infracost for your Terraform projects and shows them in a clean view inside the portal. You see projected totals for your provider. You can explore how each resource contributes to the bill. This gives your team a shared source of truth for cost during everyday work.
The plugin adds a dedicated Infracost page to your entities. It presents a high level summary with charts. You can drill into cost components for each resource to understand drivers. The goal is to make costs visible where engineers already spend time so you do not need to jump between tools.
Common use cases include design reviews for new infra, pull request checks for risky changes, and routine cost hygiene. Teams use it to spot expensive instance types early, compare options, and discuss trade offs with data. It suits self hosted Backstage setups that provision with Terraform. If your catalog tracks clusters, services, or databases, the plugin can surface their estimated monthly spend and the breakdown that supports it. This helps FinOps work feel like part of normal delivery rather than an afterthought.
Installation Instructions
These instructions apply to self-hosted Backstage only.
Install the frontend plugin
-
Add the package to your app
If you use yarn three
yarn workspace app add @veecode-platform/backstage-plugin-infracost
If you use other yarn versions
yarn add --cwd packages/app @veecode-platform/backstage-plugin-infracost
-
Add the Infracost tab to your entity page
Edit packages/app/src/components/catalog/EntityPage.tsx
// other imports import { InfracostOverviewPage, isInfracostAvailable } from '@veecode-platform/backstage-plugin-infracost'; import { EntityLayout } from '@backstage/plugin-catalog'; // import your existing pages too const clusterPage = ( <EntityLayout> <EntityLayout.Route path="/" title="Overview"> <ClusterOverviewPage /> </EntityLayout.Route> {/* other routes */} <EntityLayout.Route if={isInfracostAvailable} path="/infracost" title="Infracost"> <InfracostOverviewPage /> </EntityLayout.Route> </EntityLayout> ); export default clusterPage;
You can add the route to any entity page you prefer. Cluster is an example.
Install the backend plugin new backend system
You need the Infracost backend plugin from VeeCode. The exact package name and module export come from the backend readme. The steps below show where to wire it into the new backend system.
-
Add the backend package
yarn add --cwd packages/backend @veecode-platform/backstage-plugin-infracost-backend
-
Register the backend module
Edit packages/backend/src/index.ts
import { createBackend } from '@backstage/backend-defaults'; // Replace this import with the module export from the backend plugin readme // Some plugins export a backend module function that you pass to backend.add import { infracostModule } from '@veecode-platform/backstage-plugin-infracost-backend'; const backend = createBackend(); // Add other modules already in your app here // Register Infracost backend module backend.add(infracostModule()); backend.start();
If the backend plugin exports a different helper name, use that. Keep the registration in the main backend index so it loads on start.
Install the backend plugin legacy backend system
If your app still uses the legacy backend with express routers, wire the plugin like this.
-
Add the backend package
yarn add --cwd packages/backend @veecode-platform/backstage-plugin-infracost-backend
-
Create the plugin router
Create packages/backend/src/plugins/infracost.ts
import { PluginEnvironment } from '../types'; // Replace the import below with the correct createRouter or factory from the backend plugin readme import { createRouter } from '@veecode-platform/backstage-plugin-infracost-backend'; export default async function createPlugin(env: PluginEnvironment) { return await createRouter({ logger: env.logger, config: env.config, discovery: env.discovery, reader: env.reader, database: env.database, permissions: env.permissions, }); }
-
Mount the router
Edit packages/backend/src/index.ts
import infraRouter from './plugins/infracost'; async function main() { const env = useHotMemoize(module, () => createEnv('plugin')); const apiRouter = Router(); // other routers apiRouter.use('/infracost', await infraRouter(env)); // start service with apiRouter } main().catch(err => { process.exit(1); });
If the plugin readme shows a different mount path, use that path. The example uses
/infracost
under the main api router.
Configure your catalog entities
You need two things in your catalog
-
The main component entity needs an annotation
Add infracost project to the component. The value must match the component name that holds the estimate.
apiVersion: veecode.backstage.io/v1alpha1 kind: Cluster metadata: name: cluster-ec2 annotations: github.com/project-slug: test/cluster-ec2 backstage.io/techdocs-ref: dir:. infracost/project: cluster-ec2 spec: type: ec2 lifecycle: experimental owner: group:default/admin
-
An Infracost entity that references the estimate file
You can add it manually or automate it with a workflow. Both paths are below.
Manual estimate generation
-
Generate the estimate JSON at the project root after you have a terraform plan
infracost breakdown --path plan_cache_cli.json --format json --out-file infracost-base.json
-
Move the estimate and create the Infracost entity
Create a folder named .content in the repo root. Move infracost-base.json into .content. Then create .content/infracost.yaml
apiVersion: veecode.backstage.io/v1alpha1 kind: Infracost metadata: name: cluster-ec2 annotations: backstage.io/techdocs-ref: dir:. spec: type: FinOps lifecycle: experimental owner: group:default/admin estimate: $text: ./infracost-base.json
-
Move your main entity into .content and fix techdocs path if needed
If you move catalog-info.yaml into .content and rename it to match your kind, update techdocs to point one level up
apiVersion: veecode.backstage.io/v1alpha1 kind: Cluster metadata: name: cluster-ec2 annotations: github.com/project-slug: test/cluster-ec2 backstage.io/techdocs-ref: dir:.. infracost/project: cluster-ec2 spec: type: ec2 lifecycle: experimental owner: group:default/admin
-
Add a location at the repo root that points to every entity file inside .content
Create catalog-info.yaml at the repo root
apiVersion: backstage.io/v1alpha1 kind: Location metadata: name: cluster-ec2-location description: Importing components spec: targets: - ./.content/*.yaml
Automated estimate with GitHub Actions
This workflow runs terraform plan then creates infracost-base.json. It writes the Infracost entity file and commits artifacts into .content.
Create .github/workflows/infracost-estimate.yml
name: infracost-estimate
on:
push:
branches:
- "*"
- "*/*"
- "**"
workflow_dispatch:
env:
PATH_INFRACOST: ./.content
jobs:
infracost:
name: Infracost
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: hashicorp/setup-terraform@v2
- name: Setup Infracost
uses: infracost/actions/setup@v2
with:
api-key: ${{ secrets.INFRACOST_API_KEY }}
path: |
.terraform/**
.terraform.lock.hcl
plan_cache.json
key: terraform-lock-${{ steps.extract_branch.outputs.branch }}
- name: Checkout base branch
uses: actions/checkout@v3
with:
ref: '${{ github.event.pull_request.base.ref }}'
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Terraform Init
id: init
run: |
terraform init
terraform plan -no-color -out plan_cache.json
# Generate Infracost JSON file as the baseline.
- name: Generate Infracost cost estimate baseline
run: |
infracost breakdown --show-skipped --path plan_cache.json
- name: Generate Infracost cost estimate Json
run: |
infracost breakdown --path plan_cache.json --format json --out-file ${{ env.PATH_INFRACOST }}/infracost-base.json
- name: Generate component infracost.yaml
run: |
echo 'apiVersion: veecode.backstage.io/v1alpha1
kind: Infracost
metadata:
name: cluster-ec2 # component name
annotations:
backstage.io/techdocs-ref: dir:.
spec:
type: FinOps
lifecycle: experimental
owner: "group:default/admin"
estimate:
$text: ./infracost-base.json' > ${{ env.PATH_INFRACOST }}/infracost.yaml
- name: Publish generated artifacts
uses: stefanzweifel/git-auto-commit-action@v5
with:
file_pattern: "${{ env.PATH_INFRACOST }}"
commit_user_name: ${{ secrets.GH_USERNAME }}
commit_user_email: ${{ secrets.GH_EMAIL }}
commit_author: ${{ secrets.GH_USERNAME }}<${{ secrets.GH_EMAIL }}>
commit_message: "Publish infracost estimate"
push_options: '--force'
If you choose a different folder, update the location targets and the PATH_INFRACOST variable.
What the backend stores
Once the backend plugin is running and the Infracost entity points to infracost-base.json, the backend processor persists the data in the database. Then the UI tab reads it and shows the cost estimate for the selected entity.
Changelog
This changelog is produced from commits made to the Infracost plugin since a year ago, and based on the code located here. It may not contain information about all commits. Releases and version bumps are intentionally omitted. This changelog is generated by AI.
Features
- Add shared icon components. Update all icon usages. merged 3 months ago
Bug fixes
- Add overflow scroll to tables. Prevent cut off on small screens. merged 11 months ago
- Add hide or show toggle in chart preview. merged 11 months ago
Compatibility
- Bump Backstage core to v1.35.0 from v1.32.4. merged 8 months ago
Maintenance
- Switch to the MUI icons material package. Remove the react icons package. merged 3 months ago
- Update to Yarn v4. Include an asset handling fix. merged 11 months ago
Breaking changes
- None
Set up Backstage in minutes with Roadie
Focus on using Backstage, rather than building and maintaining it.