/infrawallet

Control your cloud costs just in the way how you control your bank accounts

Primary LanguageTypeScriptApache License 2.0Apache-2.0

logo InfraWallet

Control your cloud costs just in the way how you control your bank accounts

InfraWallet

Highlights

  • Flexible aggregation options for cloud costs across multiple platforms and accounts*
  • Cost categorization for aggregating expenses across different cloud vendors with configurable category mappings
  • Swift response times with cached cost data, ensuring rapid access to financial insights fetched from cloud platforms
  • Easy configuration and deployment as a Backstage plugin, both frontend and backend plugins are production-ready

*This version prioritizes AWS and Azure as the primary cloud vendors, but the framework is designed to be extensible to support others. Feel free to contribute to the project or wait for the next version with expanded cloud vendor support.

Getting started

Define Cloud Accounts in app-config.yaml

The configuration schema of InfraWallet is defined in the plugins/infrawallet-backend/config.d.ts file. Users need to configure their cloud accounts in the app-config.yaml in the root folder.

AWS

For AWS, InfraWallet relies on an IAM role to fetch cost and usage data using AWS Cost Explorer APIs. Thus before adding the configurations, AWS IAM role and policy need to be set up. If you have multiple AWS accounts, you should create a role in each account and configure trust relationships for it. The role to be assumed in an AWS account needs the following permission:

{
  "Statement": [
    {
      "Action": "ce:GetCostAndUsage",
      "Effect": "Allow",
      "Resource": "*",
      "Sid": ""
    }
  ],
  "Version": "2012-10-17"
}

After getting the IAM-related resources ready, put the following configuration into app-config.yaml:

backend:
  infraWallet:
    integrations:
      aws:
        - name: <unique_name_of_this_account>
          accountId: '<12-digit_account_ID>' # quoted as a string
          assumedRoleName: <name_of_the_AWS_IAM_role_to_be_assumed>
          accessKeyId: <access_key_ID_of_AWS_IAM_user_that_assumes_the_role> # optional, only needed when an IAM user is used to assume the role
          accessKeySecret: <access_key_secret_of_AWS_IAM_user_that_assumes_the_role> # optional, only needed when an IAM user is used to assume the role

The AWS client in InfraWallet is implemented using AWS SDK for JavaScript. If accessKeyId and accessKeySecret are defined in the configuration, it uses the configured IAM user to assume the role. Otherwise, the client follows the credential provider chain documented here.

Azure

In order to manage Azure costs, an application needs to be registered on Azure. InfraWallet is only tested with subscription-level cost data. After creating the application, users need to go to the Subscriptions page, choose the target subscription and then visit the Access control (IAM) page. Assign the Cost Management Reader role to the created application. Create a new client secret for the application, and add the following configurations in app-config.yaml:

backend:
  infraWallet:
    integrations:
      azure:
        - name: <unique_name_of_this_account>
          subscriptionId: <Azure_subscription_ID>
          tenantId: <Azure_tenant_ID>
          clientId: <Client_ID_of_the_created_application>
          clientSecret: <Client_secret_of_the_created_application>

Adjust Category Mappings if Needed

The category mappings are stored in the plugin's database. If there is no mapping found in the DB when initializing the plugin, the default mappings will be used. The default mappings can be found in the plugins/infrawallet-backend/seeds/init.js file. You can adjust this seed file to fit your needs, or update the database directly later on.

Install the Plugin

If Backstage New Backend System is enabled

  1. add InfraWallet frontend
# From your Backstage root directory
yarn --cwd packages/app add @electrolux-oss/plugin-infrawallet

modify packages/app/src/App.tsx and add the following code

...
import { InfraWalletPage } from '@electrolux-oss/plugin-infrawallet';
...
<FlatRoutes>
    ...
    <Route path="/infrawallet" element={<InfraWalletPage />} />
</FlatRoutes>
...
  1. add InfraWallet backend
# From your Backstage root directory
yarn --cwd packages/backend add @electrolux-oss/plugin-infrawallet-backend

modify packages/backend/src/index.ts and add the following code before backend.start();

...
// InfraWallet backend
backend.add(import('@electrolux-oss/plugin-infrawallet-backend'));
...
backend.start();
  1. add cloud account credentials to app-config.yaml Here is an example of the configuration for AWS and Azure accounts:
backend:
  infraWallet:
    integrations:
      azure:
        - name: <unique_name_of_this_account>
          subscriptionId: ...
          tenantId: ...
          clientId: ...
          clientSecret: ...
        - name: <unique_name_of_this_account>
          subscriptionId: ...
          tenantId: ...
          clientId: ...
          clientSecret: ...
      aws:
        - name: <unique_name_of_this_account>
          accountId: '<12-digit_account_ID_as_string>'
          assumedRoleName: ...
          accessKeyId: ...
          accessKeySecret: ...
        - name: <unique_name_of_this_account>
          accountId: '<12-digit_account_ID_as_string>'
          assumedRoleName: ...
          accessKeyId: ...
          accessKeySecret: ...
  1. add InfraWallet to the sidebar (optional)

modify packages/app/src/App.tsx and add the following code

...
import AccountBalanceWalletIcon from '@material-ui/icons/AccountBalanceWallet';
...
    <Sidebar>
      ...
      <SidebarGroup label="Menu" icon={<MenuIcon />}>
        <SidebarItem
          icon={AccountBalanceWalletIcon}
          to="infrawallet"
          text="InfraWallet"
        />
      </SidebarGroup>
      ...
    </Sidebar>

If the legacy Backstage backend system is used

The 2nd step above (adding the backend) is different and it should be like the following.

# From your Backstage root directory
yarn --cwd packages/backend add @electrolux-oss/plugin-infrawallet-backend

create a file infrawallet.ts in folder packages/backend/src/plugins/ with the following content.

import { createRouter } from '@electrolux-oss/plugin-infrawallet-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';

export default async function createPlugin(
  env: PluginEnvironment,
): Promise<Router> {
  return await createRouter({
    logger: env.logger,
    config: env.config,
    cache: env.cache.getClient(),
    database: env.database,
  });
}

then modify packages/backend/src/index.ts

...
import infraWallet from './plugins/infrawallet';
...
async function main() {
  ...
  const infraWalletEnv = useHotMemoize(module, () => createEnv('infrawallet'));
  ...
  apiRouter.use('/infrawallet', authMiddleware, await infraWallet(infraWalletEnv));
  ...
}

Local Development

First of all, make sure you are using either Node 18 or Node 20 for this project. Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running yarn dev in the root directory, and then navigating to /infrawallet.

You can also serve the plugin in isolation by running yarn start in the plugin directory. This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. It is only meant for local development, and the setup for it can be found inside a plugin's dev directory (e.g., plugins/infrawallet/dev).

How to Support a New Cloud Vendor?

In InfraWallet, all the cost data fetched from different cloud providers are transformed into a generic format:

export type Report = {
  id: string; // the unique ID of a cloud account which is defined in the app-config.yaml file
  [dimension: string]: string | { [period: string]: number } | undefined; // other dimensions such as category, service, a tag, etc.
  reports?: {
    [period: string]: number; // the reports which are in the following format ["period": cost], such as ["2024-01": 12.23, "2024-02": 23.21]
  };
};

For example, here is a report returned from InfraWallet backend:

{
  "id": "my-aws-dev-account",
  "provider": "aws",
  "category": "Infrastructure",
  "service": "EC2",
  "reports": {
    "2024-01": 12.23,
    "2024-02": 23.21
  }
}

The aggregation is done by the frontend after getting all the needed cost reports. This means that as long as the backend returns more cost reports in the same format, InfraWallet can always aggregate and visualize the costs.

When adding a new cloud vendor, you need to implement a client based on the interface InfraWalletApi. Check AwsClient.ts and AzureClient.ts as examples.

Roadmap

  • Make IAM user optional for AWS credentials
  • Support filters besides grouping bys
  • Support Google Cloud Costs
  • WebUI for managing category mappings
  • Enable users to select a subset of configured cloud accounts as a wallet
  • Support different currencies