Cost Insights logo

Backstage Cost Insights Plugin

Created by Spotify

Cost Insights brings cloud cost data into Backstage. It helps engineers see what they spend, why it changed, and where to act. You get daily cost views by team or catalog entity. You can compare periods, track trends, and relate spend to the business metrics you care about. It keeps the conversation in the developer portal instead of another dashboard.

The plugin highlights growth that matters. It can flag project cost spikes, or unlabeled spend that needs fixing. It breaks costs down by product or resource so you can drill into the services that drive the bill. It can express growth as an estimate of engineer time to make trade offs clear. Multiple currencies and a base currency are supported, so teams can speak in terms that fit the org.

This fits common use cases. Troubleshoot a sudden jump. Track the impact of a migration. Guide budget talks with clear numbers. Keep a steady watch without asking engineers to change tools.

Charts showing cloud costs over time and compared to other services.

Installation Instructions

These instructions apply to self-hosted Backstage only.

Install the frontend package

Copy
# run from your Backstage repo root
yarn --cwd packages/app add @backstage-community/plugin-cost-insights

Add required config

Add the required engineer cost. You can add optional fields later.

Copy
# app-config.yaml
costInsights:
  engineerCost: 200000

Optional examples

Copy
# app-config.yaml
costInsights:
  engineerCost: 200000

  products:
    productA:
      name: Some Cloud Product
      icon: storage
    productB:
      name: Some Other Cloud Product
      icon: data

  metrics:
    metricA:
      name: Metric A
      default: true
    metricB:
      name: Metric B

  baseCurrency:
    locale: nl-NL
    options:
      currency: EUR
      minimumFractionDigits: 3

  currencies:
    engineers:
      label: Engineers
      unit: engineers
    usd:
      label: USD
      kind: USD
      unit: dollars
      prefix: $
      rate: 1

  engineerThreshold: 0.5

Implement a Cost Insights client

You need a client that implements the CostInsightsApi. Start simple. You can replace the fetchers with real calls later. Keep this in the app package.

Copy
// packages/app/src/costInsights/CostInsightsClient.ts
import {
  CostInsightsApi,
  costInsightsApiRef,
  Duration,
  ExampleCostInsightsClient,
  Cost,
  MetricData,
  Entity,
  Project,
  Group,
} from '@backstage-community/plugin-cost-insights';

export class CostInsightsClient implements CostInsightsApi {
  // start with the example client to get the UI working
  private readonly demo = new ExampleCostInsightsClient();

  async getLastCompleteBillingDate(): Promise<string> {
    return this.demo.getLastCompleteBillingDate();
  }

  async getUserGroups(userId: string): Promise<Group[]> {
    return this.demo.getUserGroups(userId);
  }

  async getGroupProjects(group: string): Promise<Project[]> {
    return this.demo.getGroupProjects(group);
  }

  async getGroupDailyCost(group: string, intervals: string): Promise<Cost> {
    return this.demo.getGroupDailyCost(group, intervals);
  }

  async getProjectDailyCost(project: string, intervals: string): Promise<Cost> {
    return this.demo.getProjectDailyCost(project, intervals);
  }

  async getDailyMetricData(metric: string, intervals: string): Promise<MetricData> {
    return this.demo.getDailyMetricData(metric, intervals);
  }

  async getProductInsights(options: {
    product: string;
    group: string;
    intervals: string;
    project: string | null;
  }): Promise<Entity> {
    return this.demo.getProductInsights(options);
  }

  async getCatalogEntityDailyCost(entityRef: string, intervals: string): Promise<Cost> {
    return this.demo.getCatalogEntityDailyCost(entityRef, intervals);
  }

  async getAlerts(group: string) {
    return this.demo.getAlerts(group);
  }
}

You can keep the example client while you build your backend. Replace the demo calls with your fetch code when ready.

Wire the API into the app

Register your client in the app api registry.

Copy
// packages/app/src/api.ts
import { createApiFactory } from '@backstage/core-plugin-api';
import { costInsightsApiRef } from '@backstage-community/plugin-cost-insights';
import { CostInsightsClient } from './costInsights/CostInsightsClient';

export const apis = [
  createApiFactory({
    api: costInsightsApiRef,
    deps: {},
    factory: () => new CostInsightsClient(),
  }),
];

Add the Cost Insights page and route

Copy
// packages/app/src/App.tsx
import React from 'react';
import { FlatRoutes } from '@backstage/core-app-api';
import { Route } from 'react-router';
import { CostInsightsPage } from '@backstage-community/plugin-cost-insights';

export const AppRoutes = () => (
  <FlatRoutes>
    {/* other routes */}
    <Route path="/cost-insights" element={<CostInsightsPage />} />
  </FlatRoutes>
);

Add a sidebar entry

Copy
// packages/app/src/components/Root/Root.tsx
import React, { PropsWithChildren } from 'react';
import MonetizationOn from '@material-ui/icons/MonetizationOn';
import {
  SidebarPage,
  Sidebar,
  SidebarItem,
  SidebarLogo,
  SidebarGroup,
  SidebarDivider,
  SidebarScrollWrapper,
  SidebarSpace,
  SidebarSettings,
} from '@backstage/core-components';
import { UserSettingsSignInAvatar } from '@backstage/plugin-user-settings';

export const Root = ({ children }: PropsWithChildren<{}>) => (
  <SidebarPage>
    <Sidebar>
      <SidebarLogo />
      {/* your existing groups */}
      <SidebarDivider />
      <SidebarScrollWrapper>
        <SidebarItem icon={MonetizationOn} to="cost-insights" text="Cost Insights" />
      </SidebarScrollWrapper>
      <SidebarDivider />
      <SidebarGroup label="Settings" icon={<UserSettingsSignInAvatar />} to="/settings">
        <SidebarSettings />
      </SidebarGroup>
      <SidebarSpace />
    </Sidebar>
    {children}
  </SidebarPage>
);

Show costs on entity pages

You can show per entity spend in your catalog pages.

Copy
// packages/app/src/components/catalog/EntityPage.tsx
import React from 'react';
import { Grid } from '@material-ui/core';
import { EntitySwitch, isKind } from '@backstage/plugin-catalog';
import { EntityCostInsightsContent } from '@backstage-community/plugin-cost-insights';

export const OverviewContent = () => (
  <Grid container spacing={3}>
    {/* other cards */}
    <Grid item xs={12}>
      <EntitySwitch>
        <EntitySwitch.Case if={isKind('Component')}>
          <EntityCostInsightsContent />
        </EntitySwitch.Case>
      </EntitySwitch>
    </Grid>
  </Grid>
);

New frontend system setup

If your app uses the new frontend system, use the alpha export. Keep your client in the api registry the same way.

Copy
// packages/app/src/App.tsx
import React from 'react';
import { createApp } from '@backstage/frontend-app-api';
import { createApiFactory } from '@backstage/core-plugin-api';
import costInsights from '@backstage-community/plugin-cost-insights/alpha';
import { costInsightsApiRef } from '@backstage-community/plugin-cost-insights';
import MonetizationOn from '@material-ui/icons/MonetizationOn';
import { CostInsightsClient } from './costInsights/CostInsightsClient';

const app = createApp({
  features: [
    costInsights.provide('page:cost-insights', { path: '/cost-insights' }),
    costInsights.provide('nav-item:cost-insights', {
      title: 'Cost Insights',
      icon: MonetizationOn,
      routeRef: costInsights.routes.root,
    }),
    costInsights.provide(
      'api:cost-insights',
      createApiFactory({
        api: costInsightsApiRef,
        deps: {},
        factory: () => new CostInsightsClient(),
      }),
    ),
  ],
});

export default app.createRoot();

Provide a backend with the new backend system

The plugin does not ship a backend. You can add a small backend module that exposes the cost data your client needs. This keeps provider credentials off the browser.

Install any SDKs you plan to call from the backend. Here is an example for the AWS Cost Explorer client.

Copy
yarn --cwd packages/backend add @aws-sdk/client-cost-explorer

Create a backend module that mounts a router.

Copy
// packages/backend/src/plugins/costInsights.ts
import { Router } from 'express';
import { CostExplorerClient, GetCostAndUsageCommand } from '@aws-sdk/client-cost-explorer';

export async function createCostInsightsRouter(): Promise<Router> {
  const router = Router();

  const ce = new CostExplorerClient({
    region: process.env.AWS_REGION,
    credentials: {
      accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? '',
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? '',
    },
  });

  router.get('/group-daily-cost', async (req, res) => {
    const { accountId, start, end } = req.query as Record<string, string>;
    const cmd = new GetCostAndUsageCommand({
      TimePeriod: { Start: start, End: end },
      Metrics: ['UnblendedCost'],
      Filter: { Dimensions: { Key: 'LINKED_ACCOUNT', Values: [accountId] } },
      Granularity: 'DAILY',
    });

    const data = await ce.send(cmd);
    // map AWS response to the Cost type expected by the frontend
    const aggregation =
      data.ResultsByTime?.map(d => ({
        date: d.TimePeriod?.Start ?? '',
        amount: Number(d.Total?.UnblendedCost?.Amount ?? 0),
      })) ?? [];

    res.json({ aggregation });
  });

  return router;
}

Register the router with the new backend system entry point.

Copy
// packages/backend/src/index.ts
import { createBackend } from '@backstage/backend-defaults';
import { createCostInsightsRouter } from './plugins/costInsights';

const backend = createBackend();

backend.addRouter('/api/cost-insights', await createCostInsightsRouter());

backend.start();

Point your frontend client to this backend route. Replace the demo calls.

Copy
// packages/app/src/costInsights/CostInsightsClient.ts
export class CostInsightsClient implements CostInsightsApi {
  async getGroupDailyCost(group: string, intervals: string): Promise<Cost> {
    // pick your own mapping from group to account id
    const accountId = group;
    const [start, end] = intervals.split('/');
    const res = await fetch(
      `/api/cost-insights/group-daily-cost?accountId=${encodeURIComponent(
        accountId,
      )}&start=${encodeURIComponent(start)}&end=${encodeURIComponent(end)}`,
    );
    if (!res.ok) throw new Error(`Failed to fetch group daily cost`);
    return await res.json();
  }

  // implement the other methods the same way
}

Provide a backend with the classic backend

Create a router and mount it under the api path.

Copy
// packages/backend/src/plugins/costInsights.ts
import { Router } from 'express';

export default async function createPlugin(): Promise<Router> {
  const router = Router();

  router.get('/ping', async (_req, res) => {
    res.json({ ok: true });
  });

  // add your endpoints here
  // for example /group-daily-cost like in the new backend example above

  return router;
}

Mount it in the backend index.

Copy
// packages/backend/src/index.ts
import costInsights from './plugins/costInsights';

// inside main bootstrap
const apiRouter = Router();
apiRouter.use('/cost-insights', await costInsights());
app.use('/api', apiRouter);

Update your frontend client to call /api/cost-insights/... like in the new backend example.

Build real data into the client over time

You can implement the CostInsightsApi methods against any provider or internal billing store.

If you use AWS, the AWS SDK client can answer the methods your frontend expects. The Cost Explorer GetCostAndUsage command gives you daily totals and grouped totals. Cache where possible to keep requests low.

Copy
// example snippet inside your backend
const cmd = new GetCostAndUsageCommand({
  TimePeriod: { Start: '2021-01-01', End: '2021-02-01' },
  Metrics: ['UnblendedCost'],
  Granularity: 'DAILY',
  GroupBy: [{ Type: 'DIMENSION', Key: 'SERVICE' }],
});
const data = await ce.send(cmd);

Tips while you wire this up

  • Start with ExampleCostInsightsClient so your page renders
  • Replace methods one by one with calls to your backend
  • Keep engineerCost in config up to date so the UI can compute changes
  • Add products and metrics when you have data to back them

What the plugin exports that you can use

Import these from the app where you need them.

Copy
// page
import { CostInsightsPage } from '@backstage-community/plugin-cost-insights';

// api ref and types
import {
  costInsightsApiRef,
  CostInsightsApi,
  Duration,
  EntityCostInsightsContent,
} from '@backstage-community/plugin-cost-insights';

// new frontend system
import costInsights from '@backstage-community/plugin-cost-insights/alpha';

Place CostInsightsPage in your top level routes so everyone can reach it. Place EntityCostInsightsContent in your entity pages so teams can see cost near their components.

Things to Know

To learn more about the Cost Insights plugin and how it is used inside Spotify, check out this RedMonk interview with Cost Insights product manager Janisa Anandamohan and her engineering colleague Tim Hansen. We also have brief notes from the video in this edition of our newsletter.

Changelog

This changelog is produced from commits made to the Cost Insights 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.

Breaking changes

  • Remove alpha New Frontend System pages CostInsightsProjectGrowthInstructionsPage and CostInsightsLabelDataflowInstructionsPage. Refactor new frontend system support to match the current flow. #4839 Merged 1 month ago

Documentation

  • Update README links to point to the community plugins repository. #3931 Merged 5 months ago

Maintenance

  • Remove unused dev dependency canvas. #3565 Merged 6 months ago
  • Reduce false positives in knip reports by using a workspace based config via repo tools 0.13.0. #3018 Merged 7 months ago

Set up Backstage in minutes with Roadie