> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vantage.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# FinOps as Code

> Use AI and Terraform together to manage Vantage configuration at enterprise scale.

The console is the right place to start, but once you have dozens of teams, hundreds of reports, and allocation logic driving chargeback, changes need review, history, and repeatability. FinOps as Code manages your Vantage configuration the same way you manage infrastructure: in version control, through pull requests, with a plan you review before anything changes. The [Vantage Terraform provider](https://registry.terraform.io/providers/vantage-sh/vantage/latest) and [API](/api) are the system of record. AI tools like the [FinOps Agent](/vantage_finops_agent), the [Vantage MCP](/vantage_mcp), or any coding assistant draft the configuration but never apply it on their own.

One rule to remember: **AI proposes, Terraform commits, with a human in the loop.**

This guide shows when FinOps as Code makes sense and several use cases you can adapt. For prompting the FinOps Agent directly, see the companion [FinOps AI Prompt Guide](/guides/finops_ai_prompt_guide).

## When to Use FinOps as Code

The console and code are complements, not competitors. The console is built for exploration and answering questions quickly. Code is built for configuration that has to stay correct over time. Most organizations build something in the console first, then promote it to code once it becomes part of a regular process. (This particular pattern is shown in [Use Case 5](#use-case-5-import-console-resources-into-terraform).)

Use code when a configuration is:

* **Shared:** Cost Reports, Folders, and Dashboards that multiple teams rely on for recurring reviews.
* **Durable:** Anything that should persist beyond the person who created it and remain intact through team changes.
* **Sensitive:** Allocation logic like [Virtual Tags](/tagging) and [Billing Rules](/sql_billing_rules) that reclassifies historical cost and feeds chargeback. These changes deserve a reviewed diff.
* **Repeated:** Work you do the same way every time, like onboarding a new team. Doing it once by hand is fine, but doing it for the fifteenth team is not.
* **Subject to review or audit:** When you need a record of who changed what, when, and why.
* **Created at scale:** Hundreds of teams, accounts, or tag values that are tedious and error-prone to click through.

Stay in the console when work is:

* **Exploratory:** Ad-hoc cost questions, one-off investigations, and "what is driving this?" analysis.
* **A one-off:** A report you need once and will not maintain.
* **A learning exercise:** Trying a feature to understand how it behaves before committing to a pattern.

<Tip>
  If you find yourself rebuilding the same console configuration by hand more than twice, that is the signal to move it into code.
</Tip>

## Prerequisites

* A Vantage account with at least one connected provider.
* The [Vantage Terraform provider](https://registry.terraform.io/providers/vantage-sh/vantage/latest) configured with a Read/Write [API token](/api/authentication). If you do not use Terraform, you can manage the same resources through the corresponding [Vantage API](/api) endpoints.
* A Git repo with a Terraform workflow your team already uses for plan and apply.

## Use Cases

Each use case below follows the same shape: a source of truth your organization already maintains, such as a CSV of teams, a YAML org chart, or an export from your identity provider that drives the Vantage configuration. AI drafts the change, you review the plan, and code applies it. This keeps the business logic in one place and scales from five teams to five hundred without changing the workflow.

<CardGroup cols={2}>
  <Card title="Onboard a New Team" icon="user-plus" href="#use-case-1-onboard-a-new-team">
    Create a folder, Cost Report, team, access grant, and budget for every team from one file.
  </Card>

  <Card title="Generate Virtual Tags" icon="tags" href="#use-case-2-generate-virtual-tags-from-a-source-of-truth-file">
    Turn a CSV or YAML mapping into a cross-provider allocation tag.
  </Card>

  <Card title="Layered Allocation" icon="layer-group" href="#use-case-3-layered-allocation-for-chargeback">
    Merge several source tags into one canonical chargeback dimension, in priority order.
  </Card>

  <Card title="Durable Reports" icon="floppy-disk" href="#use-case-4-turn-an-investigation-into-a-durable-report">
    Promote a recurring investigation into a committed, version-controlled report.
  </Card>

  <Card title="Import from Console" icon="file-import" href="#use-case-5-import-console-resources-into-terraform">
    Bring resources you already built in the console under Terraform management.
  </Card>

  <Card title="Version and Roll Back" icon="code-branch" href="#use-case-6-version-and-roll-back-allocation-config">
    Snapshot your allocation config to Git so you can review every change and roll back.
  </Card>
</CardGroup>

### Use Case 1: Onboard a New Team

Every time a new team starts up, you need the same handful of Vantage resources: a folder to organize their reports, a Cost Report filtered to their accounts, a team with an access grant so they can see their data, and a budget to track spend. Keep the team definitions in one file, and let Terraform create every resource from it.

```yaml teams.yaml theme={null}
teams:
  platform:
    workspace_token: wrkspc_abc123
    aws_account_ids: ["123456789012"]
    monthly_budget_usd: 75000
  security:
    workspace_token: wrkspc_abc123
    aws_account_ids: ["234567890123", "345678901234"]
    monthly_budget_usd: 40000
```

Ask your AI assistant to generate the configuration from that file:

<Prompt
  description={`Using our existing Vantage Terraform patterns, generate only the HCL to onboard the teams in teams.yaml.
For each team, create a folder, a Cost Report filtered to its AWS accounts, a team, an access grant to its folder, and a monthly budget.
Iterate over the file with for_each. Do not create new modules or change existing resources. Return only the diff and your assumptions.`}
  actions={["copy"]}
>
  Using our existing Vantage Terraform patterns, generate only the HCL to onboard the teams in teams.yaml.
  For each team, create a folder, a Cost Report filtered to its AWS accounts, a team, an access grant to its folder, and a monthly budget.
  Iterate over the file with for\_each. Do not create new modules or change existing resources. Return only the diff and your assumptions.
</Prompt>

It returns one set of resources per team, driven by `for_each`:

```hcl onboarding.tf expandable theme={null}
locals {
  teams = yamldecode(file("teams.yaml")).teams
}

resource "vantage_folder" "team" {
  for_each        = local.teams
  title           = "${each.key} Costs"
  workspace_token = each.value.workspace_token
}

resource "vantage_cost_report" "team" {
  for_each     = local.teams
  title        = "${each.key} AWS Costs"
  folder_token = vantage_folder.team[each.key].token
  filter       = "costs.provider = 'aws' AND costs.account_id IN (${join(", ", [for id in each.value.aws_account_ids : "'${id}'"])})"
  groupings    = "service,region"
}

resource "vantage_team" "team" {
  for_each         = local.teams
  name             = each.key
  description      = "Managed by Terraform"
  workspace_tokens = [each.value.workspace_token]
}

resource "vantage_access_grant" "team" {
  for_each       = local.teams
  team_token     = vantage_team.team[each.key].token
  resource_token = vantage_folder.team[each.key].token
  access         = "allowed"
}

resource "vantage_budget" "team" {
  for_each          = local.teams
  name              = "${each.key} Monthly Budget"
  workspace_token   = each.value.workspace_token
  cost_report_token = vantage_cost_report.team[each.key].token
  periods = [
    {
      start_at = "2026-01-01"
      amount   = each.value.monthly_budget_usd
    }
  ]
}
```

Review the [VQL](/vql) filter before merging. Team onboarding PRs usually succeed or fail based on whether the filter matches the right accounts, tags, and workspace. Creating teams and access grants through Terraform requires Enterprise RBAC and appropriate permissions. For more on teams and access grants, see the [RBAC setup guide](/guides/rbac_setup).

To onboard a single new team later, add one entry to `teams.yaml` and re-run `terraform plan`. Because every resource is keyed by team name with `for_each`, Terraform creates only the new team's resources and leaves the existing teams untouched. The plan shows additions, not a rebuild.

<Note>
  The exact schema for each resource is in the [Vantage provider docs on the Terraform Registry](https://registry.terraform.io/providers/vantage-sh/vantage/latest/docs). `vantage_budget` periods take a `start_at` and `amount`; `end_at` is optional. Add one period per month, or generate periods, to cover the full budget window. Validate generated code against the provider docs before applying.
</Note>

### Use Case 2: Generate Virtual Tags from a Source-of-Truth File

Allocation is where code review matters most. A [Virtual Tag](/tagging) change can reclassify months of historical cost across every report that groups by that tag. The most maintainable pattern is to keep the mapping that defines the tag, such as teams, accounts, or namespaces, in a file your organization already owns, and generate the tag values from it. The file is the system of record. `terraform plan` recomputes the tag whenever it changes. Start with the list of teams your organization already maintains:

```csv teams.csv theme={null}
team
platform
security
data-platform
core-services
```

This example builds one cross-provider `team` Virtual Tag that normalizes the AWS `team` tag and the Kubernetes `organization/team` label into a single dimension. Terraform reads the CSV and produces one value per team.

```hcl team_tag.tf theme={null}
locals {
  teams = csvdecode(file("teams.csv"))
}

resource "vantage_virtual_tag_config" "team" {
  key            = "team"
  overridable    = true
  backfill_until = "2026-01-01"

  values = [
    for t in local.teams : {
      name   = t.team
      filter = "(costs.provider = 'aws' AND tags.name = 'team' AND tags.value = '${t.team}') OR (costs.provider = 'kubernetes' AND tags.name = 'organization/team' AND tags.value = '${t.team}')"
    }
  ]
}
```

Or skip writing the HCL and have AI generate it from your file:

<Prompt description={`Using the Vantage Terraform provider, write a vantage_virtual_tag_config that reads teams.csv (one team per row) and creates a cross-provider "team" Virtual Tag. For each team, generate a value whose VQL matches the AWS tag "team" and the Kubernetes label "organization/team" with that team's name. Return only the HCL.`} actions={["copy"]}>
  Using the Vantage Terraform provider, write a vantage\_virtual\_tag\_config that reads teams.csv (one team per row) and creates a cross-provider "team" Virtual Tag. For each team, generate a value whose VQL matches the AWS tag "team" and the Kubernetes label "organization/team" with that team's name. Return only the HCL.
</Prompt>

Before applying, confirm the `backfill_until` date, verify the tag keys match what your providers actually emit, and keep allocation changes in their own PR. Do not bury them alongside unrelated report updates. Because each value adds processing work, review the [Scaling Virtual Tags guide](/guides/scaling_tagging) before generating tags with hundreds of values, and see [tagging examples](/tagging_examples) for more filter patterns.

### Use Case 3: Layered Allocation for Chargeback

Large organizations rarely get a clean allocation dimension from a single source. A team or product name might come from a manually maintained override, a Kubernetes namespace, a provider tag, or a [Business Metric](/business_metrics), and you need one canonical value with a clear order of precedence. You can build this entirely from Virtual Tags: define a few narrow source tags, then merge them into one standardized key using [tag key collapsing](/tagging_examples#tag-key-collapsing-examples), where the first source in the list that has a value wins.

First, define the source tags. Each one captures a single way a `team` value can be derived. Keep them narrow and independent.

```hcl team_sources.tf theme={null}
# Source 1: high-priority manual overrides
resource "vantage_virtual_tag_config" "team_overrides" {
  key         = "team_overrides"
  overridable = true
  values = [
    {
      name   = "platform"
      filter = "costs.provider = 'aws' AND costs.account_id = '123456789012'"
    }
  ]
}

# Source 2: derive team from Kubernetes namespace
resource "vantage_virtual_tag_config" "team_from_namespace" {
  key         = "team_from_namespace"
  overridable = true
  values = [
    {
      name   = "data-platform"
      filter = "costs.provider = 'kubernetes' AND tags.name = 'namespace' AND tags.value IN ('data-ingest', 'data-warehouse')"
    }
  ]
}
```

Then collapse the sources into one canonical key. The collapsing tag merges the source keys in priority order—list the most authoritative source first, with a raw provider tag at the bottom as a fallback. In this example, the final `{ key = "team" }` entry is the native provider tag key, not another `vantage_virtual_tag_config` resource.

```hcl team.tf theme={null}
resource "vantage_virtual_tag_config" "team" {
  key            = "team"
  overridable    = true
  backfill_until = "2026-01-01"

  depends_on = [
    vantage_virtual_tag_config.team_overrides,
    vantage_virtual_tag_config.team_from_namespace,
  ]

  collapsed_tag_keys = [
    { key = "team_overrides" },      # manual overrides win
    { key = "team_from_namespace" }, # then namespace-derived values
    { key = "team" }                 # finally, the raw provider tag
  ]
}
```

The result is a single `team` dimension you can group and filter by across providers, with deterministic precedence.

<Note>
  Order is precedence: when more than one source has a value for the same cost, the source higher in the list wins. If one of your sources is a cost-based or Business Metric allocation, mind the depth and per-provider rules for allocation chains (up to three allocated tags deep). The [Scaling Virtual Tags guide](/guides/scaling_tagging) covers precedence, nesting, and processing cost in detail. To manage Business Metrics themselves as code, see [Business Metrics in Terraform](https://www.vantage.sh/blog/business-metric-api-terraform).
</Note>

### Use Case 4: Turn an Investigation into a Durable Report

Someone from finance asks why data transfer costs jumped last month. You dig into it with the [FinOps Agent](/vantage_finops_agent) or a [Vantage MCP](/vantage_mcp) client and find the answer. Then the thread disappears into chat history and the same question comes back next month.

If a question is going to recur, turn the answer into a Terraform-managed Cost Report.

<Steps>
  <Step title="Investigate">
    Use the FinOps Agent or a Vantage MCP client to identify the services, accounts, regions, and tags driving the cost change.
  </Step>

  <Step title="Draft the report definition">
    Ask AI to turn the investigation into a [VQL](/vql) filter and grouping strategy, and to explain its assumptions rather than creating anything directly.

    <Prompt description={`Based on this cost investigation, write a Vantage VQL filter and suggested groupings for a Cost Report that tracks these costs going forward. Explain your assumptions and return only the filter and groupings; do not create the report.`} actions={["copy"]}>
      Based on this cost investigation, write a Vantage VQL filter and suggested groupings for a Cost Report that tracks these costs going forward. Explain your assumptions and return only the filter and groupings; do not create the report.
    </Prompt>
  </Step>

  <Step title="Commit the report">
    ```hcl data_transfer.tf theme={null}
    resource "vantage_cost_report" "aws_data_transfer" {
      title           = "AWS Data Transfer"
      workspace_token = var.workspace_token
      filter          = "costs.provider = 'aws' AND costs.category = 'Data Transfer'"
      groupings       = "service,account_id,region"
    }
    ```
  </Step>

  <Step title="Wire it into a review cadence">
    Add the report to a [Dashboard](/dashboards) or set up a [Report Notification](/report_notifications) so the stakeholder gets a recurring update without having to ask again.
  </Step>
</Steps>

### Use Case 5: Import Console Resources into Terraform

You do not need to start from scratch. Many teams build their initial reporting structure in the console and only move to Terraform once a resource becomes part of a standing process. Examples include a chargeback report that runs monthly, a Virtual Tag that feeds executive Dashboards, or a team structure that needs to stay in sync with your identity provider.

<Steps>
  <Step title="Get the resource token">
    Copy the token from the console URL (e.g., `rprt_1abc23456c7c8a90`) or query the [API](/api) or a Terraform data source.
  </Step>

  <Step title="Define the resource in Terraform">
    ```hcl chargeback.tf theme={null}
    resource "vantage_cost_report" "monthly_chargeback" {
      title = "Monthly Chargeback"
    }
    ```
  </Step>

  <Step title="Import">
    ```bash theme={null}
    terraform import vantage_cost_report.monthly_chargeback rprt_1abc23456c7c8a90
    ```
  </Step>

  <Step title="Reconcile the plan">
    Run `terraform plan`. The first plan will likely show differences between your HCL and the live resource. Update the configuration until the plan is clean. If the diff is confusing, paste it into your AI assistant:

    <Prompt description={`Here is the terraform plan output for a Vantage resource I just imported. Explain which fields are computed defaults I can ignore and which are real differences I need to set in my configuration to make the plan clean.`} actions={["copy"]}>
      Here is the terraform plan output for a Vantage resource I just imported. Explain which fields are computed defaults I can ignore and which are real differences I need to set in my configuration to make the plan clean.
    </Prompt>
  </Step>
</Steps>

<Warning>
  Not every Vantage resource supports import. Check the [Vantage provider docs on the Terraform Registry](https://registry.terraform.io/providers/vantage-sh/vantage/latest) and the individual [resource pages](https://registry.terraform.io/providers/vantage-sh/vantage/latest/docs) before attempting an import.
</Warning>

### Use Case 6: Version and Roll Back Allocation Config

Your allocation logic—the [Virtual Tags](/tagging) that drive showback and chargeback—is some of the most valuable configuration in Vantage, and the most tedious to reconstruct by hand if a change does not work out. When it lives in code and Git, every version is saved automatically: you can see exactly what changed between any two points in time and return to a known-good version without rebuilding it from memory.

This is the payoff of [importing your resources](#use-case-5-import-console-resources-into-terraform). Once a Virtual Tag is defined in code, Git becomes its version history.

You do not have to hand-write the starting configuration. List your Virtual Tag tokens (from the console URL or the `vantage_virtual_tag_configs` data source), write a Terraform `import` block for each one, and let Terraform generate the HCL for you.

<Note>
  Terraform `import` blocks and `terraform plan -generate-config-out` require Terraform 1.5 or later.
</Note>

```hcl virtual_tags_import.tf theme={null}
import {
  to = vantage_virtual_tag_config.team
  id = "vtag_1a2b3c4d5e6f7890"
}

import {
  to = vantage_virtual_tag_config.environment
  id = "vtag_0987654321fedcba"
}
```

Generate the configuration from the live resources, then reconcile until the plan is clean:

```bash theme={null}
terraform plan -generate-config-out=virtual_tags.tf
```

Terraform writes the current definition of each tag into `virtual_tags.tf`. Reconcile the generated configuration until the plan is clean, then run `terraform apply` to complete the import into state. Commit `virtual_tags.tf` after the import is complete. It is now your baseline version, and the manual work of capturing the existing setup is done once and never again.

From here, every change to allocation goes through a pull request, so Git records what changed, when, and why.

<Steps>
  <Step title="Change in a branch">
    Edit the tag configuration and open a PR. The `terraform plan` in the PR shows the exact before and after of the allocation change.
  </Step>

  <Step title="Tag a release (optional)">
    Tag the merge commit (for example, `vtags-2026-06-01`) so you have named versions to return to.
  </Step>

  <Step title="Roll back when needed">
    If a change does not pan out, revert to a previous commit and apply it. Vantage restores the Terraform-defined configuration and reprocesses affected allocation tags. There is nothing to rebuild from the audit log or from memory.

    ```bash theme={null}
    git revert <commit>
    terraform apply
    ```
  </Step>
</Steps>

<Note>
  Keep allocation configuration in its own file and its own PRs (see [Guardrails](#guardrails)). A clean, isolated history is what makes a confident rollback possible.
</Note>

## Using AI Effectively

AI is what makes FinOps as Code fast: it reads your source files, drafts the HCL, and explains its reasoning so that you spend your time reviewing a diff instead of writing boilerplate. To get accurate output:

* **Give it the source of truth and the patterns:** Point the assistant at your CSV/YAML file and an existing resource in your repo so generated code matches your conventions.
* **Ask for a diff and assumptions, not actions:** Have AI return only the HCL changes and the assumptions it made. Keep it from creating or modifying resources directly.
* **Make it validate against the provider docs:** Ask the assistant to check generated code against the [Vantage provider docs on the Terraform Registry](https://registry.terraform.io/providers/vantage-sh/vantage/latest/docs). Schemas change (e.g., `vantage_budget` uses `periods`, `vantage_dashboard` uses `widgets`) and a stale example will not apply cleanly.
* **Let the plan be the proof:** A clean `terraform plan` is the real test. If the plan is noisy or fails, feed it back to the assistant and ask it to reconcile.

The [FinOps Agent](/vantage_finops_agent) is especially useful here because it understands your workspace, RBAC scope, and cost data.

## Guardrails

These are worth writing into your team's operating agreement, not just this guide:

* AI never runs `terraform apply`. A human reviews the plan and merges the PR.
* Every PR that touches Vantage resources includes a `terraform plan` output.
* Allocation changes (Virtual Tags, Billing Rules) go in their own PRs, separate from reporting or access changes. These affect historical cost data and should be reviewed with that in mind.
* VQL filters get a dedicated review check. A wrong `costs.account_id` or missing `tags.value` is the most common source of bad reports.

## Frequently Asked Questions

<AccordionGroup>
  <Accordion title="Can I use the console and code at the same time?">
    Yes. Most teams build and explore in the console, then move the configuration that becomes a standing process into code. The risk is editing a code-managed resource in the console, which causes drift (see below). Decide per resource where the source of truth lives, and keep code-managed resources out of the console for edits.
  </Accordion>

  <Accordion title="What happens if someone edits a code-managed resource in the console?">
    The next `terraform plan` will show a diff between your HCL and the live resource, and `terraform apply` will revert the console change to match the code. Treat the plan as a drift detector: if you see unexpected changes, someone edited the resource outside of code. For resources you want to manage in code, make code the only place they are changed.
  </Accordion>

  <Accordion title="How should I handle API tokens and Terraform state?">
    Pass the Vantage API token through an environment variable or your secrets manager, never commit it. Use a remote, shared [Terraform backend](https://developer.hashicorp.com/terraform/language/backend) so your team works from the same state and avoids drift, and keep `.terraform/` and local state files out of version control.
  </Accordion>

  <Accordion title="Do allocation changes reprocess historical cost data?">
    Yes. Editing a Virtual Tag or Billing Rule reapplies it across the data in its [backfill window](/guides/scaling_tagging), which can reclassify months of cost across every report that uses it. Keep allocation changes in their own PR, set the smallest backfill you actually need, and review the plan with this in mind.
  </Accordion>

  <Accordion title="Which resources can I manage as code?">
    Cost Reports, Folders, Dashboards, Virtual Tags, Cost Allocation Segments, teams, access grants, budgets, Cost Alerts, Billing Rules for eligible MSP accounts, Business Metrics, and more. See the [Vantage provider docs on the Terraform Registry](https://registry.terraform.io/providers/vantage-sh/vantage/latest/docs) for the full list and each resource's schema, and check there before assuming a resource supports [import](#use-case-5-import-console-resources-into-terraform).
  </Accordion>
</AccordionGroup>

## References

<Tabs>
  <Tab title="Product Documentation">
    * [Vantage Terraform Provider (Terraform Registry)](https://registry.terraform.io/providers/vantage-sh/vantage/latest)
    * [Vantage Terraform Resource Docs (Terraform Registry)](https://registry.terraform.io/providers/vantage-sh/vantage/latest/docs)
    * [VQL Reference](/vql)
    * [Virtual Tags](/tagging) and [tagging examples](/tagging_examples)
  </Tab>

  <Tab title="Tutorials and Examples">
    * [FinOps as Code examples repo](https://github.com/vantage-sh/finops-as-code)
    * [Automating cost allocation tags with Terraform](https://www.vantage.sh/blog/terraform-automate-cost-tags)
    * [Okta access automation with Terraform](https://www.vantage.sh/blog/okta-terraform)
    * [Business Metrics in the API and Terraform](https://www.vantage.sh/blog/business-metric-api-terraform)
  </Tab>
</Tabs>
