Work in progressThese docs are still being written and are currently mostly AI-generated. Some details may be inaccurate or incomplete.

Terraform provider

Manage Infrawrench's own configuration — budgets and cost policy, probes and alerts, schedules and freezes, accounts, roles and alert routing — as Terraform resources.

The Infrawrench Terraform provider manages Infrawrench’s own configuration as Terraform resources: cost allocation and reporting, monitoring, lifecycle governance, connected accounts and access control, and alert delivery. 47 resources and 6 data sources, each with its own plan, its own drift detection, and its own terraform import.

It is for teams who already keep infrastructure in Terraform and want the rest of their platform configuration to arrive the same way — through a pull request, reviewed, with a plan that says exactly what will change.

It does not manage your cloud resources. Your cloud provider’s own Terraform provider creates the database; this one manages the budget that watches what the database costs, the probe that checks it is up, the schedule that powers it down at night, and the rule that decides who gets paged when it is not.

Screenshot neededA terraform plan output in a terminal showing an infrawrench_budget being updated in place, with the amount_cents and threshold changes highlighted

Which Terraform feature is this?

Three features have Terraform in the name. They point in different directions, and picking the wrong one costs you an afternoon.

FeatureWhat it managesUse it when
Terraform exportYour cloud resources, written out as HCLYou want to adopt existing resources into Terraform, or leave
Config as codeA whole organization’s configuration, as one documentCloning an org, seeding staging, disaster recovery
Terraform provider (this page)Individual Infrawrench objects, as Terraform resourcesBudgets and cost policy belong in your Terraform repo, under review

The short version: Terraform export is about getting your resources out. Config as code moves a whole org at once. The provider manages one object at a time, continuously.

They compose. Config as code is still the right tool for “make staging look like production in one shot”. The provider is the right tool for “the platform team’s budget lives in git and changes go through review”.

Why not just wrap config as code?

A fair question, and it was the first design considered. Config as code already exports, plans and applies configuration, so wrapping it would have meant one implementation of diffing and validation instead of two.

It does not fit, for reasons that are worth knowing because they also tell you when to use each feature:

  • The document doesn’t carry most of these objects. It has sections for budgets, cost centres, tag policy and metric alerts — but not for saved filters, cost reports, cost alerts, scenario models, billing rules, cost exports, business metrics, accounts, roles, API keys, bastions, status pages, schedules, freezes, the alert routing table, or most of the rest.
  • The document addresses things by name, not by id. That’s exactly what makes one document apply cleanly to a fresh organization. It’s also what makes terraform import impossible, and what would turn renaming a budget into destroying and recreating it.
  • Apply is all-or-nothing. It takes the whole document in one transaction. Terraform walks its graph in parallel, so ten resources applying at once would mean ten overlapping rewrites of one document.
  • Deleting one object would mean owning all of them. The document’s replace mode deletes anything it doesn’t name, so removing one budget from Terraform would have destroyed every budget created outside it.

So the provider talks to the API routes directly, one object at a time. That is what buys you per-resource plans, real imports, and deletions that affect exactly one thing.

Install

terraform {
  required_providers {
    infrawrench = {
      source  = "Infrawrench/infrawrench"
      version = "~> 0.1"
    }
  }
}

Authenticate

provider "infrawrench" {
  organization_id = "org_01HXYZABCDEF"
  # api_key comes from INFRAWRENCH_API_KEY
}

Every argument falls back to an environment variable, which is how you should run it in CI so the credential never lands in a .tf file or a saved plan:

ArgumentEnvironment variableDefault
base_urlINFRAWRENCH_BASE_URLhttps://app.infrawrench.com
api_keyINFRAWRENCH_API_KEYrequired
organization_idINFRAWRENCH_ORG_IDrequired

The organization id is the one in your URL when you’re signed in — it starts with org_.

Create the credential on Settings → API keys. The scope picker lists every permission the server recognises, grouped, with a filter box that matches the permission string itself — so you can paste costs: from the table below and tick what you need. The permissions each object needs are the same ones the UI enforces, so a key that can’t edit budgets in the app can’t edit them through Terraform either:

ObjectsReadWrite
Budgetsbudgets:readbudgets:write
Cost centres, allocation rules, saved filters, reports, folders, alerts, scenario modelscosts:readcosts:write
Tag policyresources:readorg:settings:write
Billing rules, cost exportscosts:readorg:settings:write

Billing rules and cost exports are the odd ones: reading them needs only costs:read, but changing them needs org settings. A key scoped to costs:write will read them and fail to write them.

Beyond cost, each area uses the permission the matching page in the app uses — resources:write for probes, status pages, sleep schedules and log queries; metric-alerts:write for metric alerts; dashboards:write for custom graphs; freezes:write for change freezes; bastions:write, ssh-keys:write, apikeys:write and team:role:write for the access resources; and org:settings:write for alert routing, Slack, Teams and the weekly digest. The full table is in the provider’s README.

Two of those need care. Alert routing, Slack, Teams and the digest have no separate read permission — their GET is gated on org:settings:write too, so a read-only key cannot even refresh them. And an account needs three: accounts:write to connect, secrets:write to rotate its credentials, accounts:delete to disconnect.

apikeys:write and team:role:write are the two the picker does not offer, because a key holding them would be refused anyway — see below.

Two resources need a signed-in credential

An API key reaches everything in the provider except two things, whatever scopes you give it:

ResourceWith an API keyWhy
infrawrench_api_keyClosed entirelyA key that can mint keys can mint a longer-lived one and outlive its own revocation
infrawrench_roleReadable, not writableA key shouldn’t manufacture durable authority for other principals

If you manage either, keep them in a separate Terraform root that a person applies with a WorkOS access token — which is the separation the restriction is arguing for anyway. The provider recognises this particular 403 and tells you which resource is affected, rather than leaving you staring at a permission error that isn’t about permissions.

Screenshot neededThe Create API Key dialog under Settings → API keys, filter box containing “costs:”, with Costs (read) and Costs (write) ticked and the selected-count showing 2

A worked example

One file: a saved filter, the cost centre it feeds, the rule that allocates to it, a budget, and an alert.

resource "infrawrench_saved_filter" "platform" {
  name        = "Platform team"
  description = "Everything tagged team=platform, across every provider."

  filter {
    dimension = "tag"
    tag_key   = "team"
    op        = "in"
    values    = ["platform"]
  }
}

resource "infrawrench_cost_centre" "platform" {
  name        = "Platform"
  description = "Shared infrastructure owned by the platform team."
}

resource "infrawrench_allocation_rule" "platform_tag" {
  cost_centre_id = infrawrench_cost_centre.platform.id
  priority       = 100

  match {
    tag_key   = "team"
    tag_value = "platform"
  }
}

resource "infrawrench_budget" "platform" {
  name            = "Platform monthly"
  amount_cents    = 4500000
  currency        = "USD"
  saved_filter_id = infrawrench_saved_filter.platform.id
  cost_basis      = "amortized"

  threshold {
    type    = "actual"
    percent = 80
  }

  threshold {
    type    = "forecast"
    percent = 100
  }
}

resource "infrawrench_cost_alert" "platform_spike" {
  name              = "Platform week-on-week spike"
  cadence           = "weekly"
  direction         = "increase"
  threshold_percent = 25

  filter {
    dimension = "tag"
    tag_key   = "team"
    op        = "in"
    values    = ["platform"]
  }
}

Referencing account ids by hand is a mistake waiting to happen, so read them instead:

data "infrawrench_accounts" "aws" {
  plugin_id = "aws"
}

resource "infrawrench_allocation_rule" "first_aws_account" {
  cost_centre_id = infrawrench_cost_centre.platform.id
  priority       = 200

  match {
    account_id = data.infrawrench_accounts.aws.accounts[0].id
  }
}

Beyond cost

The same file can carry the monitoring, governance and alert-delivery configuration a platform team would otherwise click together by hand.

# One rule written once covers every instance the team creates afterwards:
# resources are selected by query, never by id.
resource "infrawrench_metric_alert" "cpu" {
  name        = "Platform CPU sustained"
  tag_key     = "team"
  tag_value   = "platform"
  metric_key  = "CPU %"
  comparator  = ">"
  threshold   = 90
  for_minutes = 20
}

resource "infrawrench_probe" "api" {
  name = "API health"
  url  = "https://api.example.com/health"
}

resource "infrawrench_status_page" "public" {
  title     = "Acme status"
  published = true

  component {
    probe_id = infrawrench_probe.api.id
    label    = "API"
  }
}

# A custom graph is source code, so it lives beside the rest of your source code.
resource "infrawrench_custom_graph" "burn" {
  name   = "Platform burn"
  source = file("${path.module}/graphs/burn.ts")
}

# A permission set whose diff is the point: adding a grant is a reviewed line.
resource "infrawrench_role" "finance" {
  name        = "Finance"
  description = "Read spend, own budgets, touch nothing else."
  permissions = ["costs:read", "budgets:read", "budgets:write", "invoices:read"]
}

Powering a development machine down outside working hours is the cheapest saving there is, and the one nobody remembers to apply by hand. Resolve the resource rather than hard-coding its id:

data "infrawrench_resources" "api" {
  account_id       = data.infrawrench_accounts.aws.accounts[0].id
  resource_type_id = "ec2_instance"
  name_contains    = "api"
}

resource "infrawrench_schedule" "api_nights" {
  resource_id  = data.infrawrench_resources.api.resources[0].id
  account_id   = data.infrawrench_resources.api.resources[0].account_id
  days_of_week = [1, 2, 3, 4, 5]
  stop_time    = "19:00"
  start_time   = "08:00"
  timezone     = "Europe/Berlin"
}
Screenshot neededA terraform plan output showing infrawrench_schedule and infrawrench_probe resources being created, with the projected monthly saving visible in the plan

Alert routing is one resource, in order

Alert routing is a single ordered table rather than one resource per rule, because order is the semantics: the list is evaluated top to bottom and is first-match-wins unless a rule sets continue_on_match. A rule cannot meaningfully be written without saying where it sits, so per-rule resources would have needed a position attribute and then a way to stop two configurations claiming the same slot.

data "infrawrench_slack_installations" "workspace" {}

resource "infrawrench_slack_channel" "platform" {
  installation_id = data.infrawrench_slack_installations.workspace.installations[0].id
  channel_id      = "C0123456789"
  channel_name    = "platform-alerts"
}

resource "infrawrench_alert_routing" "org" {
  rule {
    name = "Spend goes to the platform channel"

    condition {
      field  = "trigger"
      op     = "in"
      values = ["budgetAlerts", "anomalyAlerts", "costChangeAlerts"]
    }

    destination {
      kind       = "slack"
      channel_id = infrawrench_slack_channel.platform.id
    }
  }

  rule {
    name = "Anything critical also wakes phones"

    condition {
      field    = "severity"
      op       = "gte"
      severity = "critical"
    }

    destination {
      kind = "push"
    }

    # Held overnight, not dropped — a held alert is delivered when the window
    # closes, and critical is exempt from the hold entirely.
    quiet_hours {
      timezone        = "Europe/Berlin"
      start_minute    = 1320 # 22:00
      end_minute      = 420  # 07:00 — an overnight window
      days            = [1, 2, 3, 4, 5]
      urgent_override = "critical"
    }

    # Nobody acknowledged in fifteen minutes? Widen it.
    escalation {
      after_minutes = 15

      destination {
        kind       = "slack"
        channel_id = infrawrench_slack_channel.platform.id
      }
    }
  }

  rule {
    name = "Swallow drift chatter"

    condition {
      field  = "trigger"
      op     = "in"
      values = ["resourceDrift"]
    }
    # No destination: an enabled rule with nowhere to go silences the category
    # without deleting the rules that would otherwise catch it.
  }
}

Connecting the Slack workspace itself is an OAuth flow, which a Terraform provider cannot perform. Install the app once on Settings → Alerts, then read the installation with data.infrawrench_slack_installations.

Because the write replaces the whole table, the resource has to carry every field a rule can hold — including quiet hours and escalation policies you may have set up in the app before adopting Terraform. Import the resource first and read what comes back: whatever the configuration does not say, the next apply clears.

Adopting what you already have

Nobody starts from an empty organization, so every object with an id imports:

terraform import infrawrench_budget.platform      b1f2c3d4-...
terraform import infrawrench_cost_centre.platform 9a8b7c6d-...
terraform import infrawrench_probe.api            2c3d4e5f-...
terraform import infrawrench_role.finance         1a2b3c4d-...

The id is in the URL when you open the object in the app, and in the --json output of the CLI.

Organization singletons — the ones there is only ever one of, with no id of their own — import under the organization id:

terraform import infrawrench_tag_policy.this       org_01HXYZABCDEF
terraform import infrawrench_alert_routing.org     org_01HXYZABCDEF

That covers tag policy, alert routing, currency settings, the anomaly and efficiency alert settings, the drift, expiry and posture alert settings, session recording, the weekly digest, and the Jira and Linear connections.

Two resources import under something other than their own id. A report notification hangs off its report, so it takes <report-id>/<notification-id>; a workflow schedule takes the id of the workflow it belongs to.

Secrets don’t come back

Several resources hold write-only material that no route returns. Importing them works — recovering the secret does not. After importing, put it back in your configuration; where the API accepts an omitted credential as “keep the stored one”, you can leave it out instead.

ResourceNot recoverableWhat you can still read
infrawrench_cost_exportaccess key, secret, webhook URLhas_credentials, credential_hint
infrawrench_accountthe whole credentials mapnothing
infrawrench_api_keykey — returned once, at creationprefix
infrawrench_ssh_keyprivate_key — returned oncepublic_key, fingerprint
infrawrench_bastiontoken — returned oncetoken_prefix
infrawrench_msteams_webhookurlurl_hint
infrawrench_jira_integrationapi_tokentoken_hint
infrawrench_linear_integrationapi_keykey_hint
infrawrench_deploy_triggeranswersnothing

The provider can’t detect drift on any of these. That’s a property of the API rather than a limitation of the provider — the values genuinely aren’t returned, deliberately.

One warning about state files

infrawrench_api_key, infrawrench_ssh_key in generate mode, and infrawrench_bastion each write a credential into your Terraform state in plaintext, because the API returns it exactly once and never again.

Use them only with a state backend you’d put any other secret in — encrypted, access-controlled, not a local file in a repository. Prefer piping the value straight into the secret store that consumes it rather than into a Terraform output.

What’s managed

Cost allocation and reporting

ResourceManages
infrawrench_budgetBudgets and their alert thresholds
infrawrench_cost_centreCost centres
infrawrench_allocation_ruleThe rules that map spend onto cost centres
infrawrench_tag_policyRequired tags and their enforcement
infrawrench_saved_filterSaved cost filters
infrawrench_cost_reportCost reports
infrawrench_cost_report_folderReport folders
infrawrench_cost_report_notificationScheduled delivery of a report to Slack, Teams or email
infrawrench_cost_alertCost change alerts
infrawrench_cost_annotationNotes pinned to a date on cost charts
infrawrench_scenario_modelScenario models
infrawrench_billing_ruleBilling rules
infrawrench_cost_exportScheduled cost exports
infrawrench_business_metricUnit-cost denominators — the definition, not the values
infrawrench_managed_accountManaged accounts an MSP bills
infrawrench_currency_settingsThe organization’s display currency
infrawrench_exchange_rateOne stated rate, effective from a day
infrawrench_anomaly_settingsAnomaly detection thresholds
infrawrench_efficiency_alert_settingsCommitment and unit-cost alert thresholds

Monitoring

ResourceManages
infrawrench_probeSynthetic probes
infrawrench_status_pagePublic status pages and their components
infrawrench_metric_alertMetric threshold alerts
infrawrench_log_queryLog workspace saved queries and match alerts
infrawrench_custom_graphCustom graphs, source and all

Lifecycle governance

ResourceManages
infrawrench_scheduleSleep schedules
infrawrench_change_freezeChange freezes
infrawrench_drift_alert_settingsWhat the change timeline notifies about
infrawrench_expiry_alert_settingsExpiry radar lead time
infrawrench_posture_alert_settingsWhether posture findings notify
infrawrench_session_recording_settingsSession recording and retention
infrawrench_network_flow_settingsWhether network flow costs are collected

Accounts and access

ResourceManages
infrawrench_accountConnected cloud accounts
infrawrench_bastionBastion agent enrollments
infrawrench_roleCustom roles and permissions
infrawrench_api_keyAPI keys
infrawrench_ssh_keySSH keys, imported or generated
infrawrench_ssh_snippetSaved SSH fan-out commands
infrawrench_deploy_triggerRedeploy-on-push triggers for Infrafile projects
infrawrench_workflow_scheduleThe cron on an existing workflow

Alert delivery

ResourceManages
infrawrench_alert_routingThe whole ordered alert routing table, quiet hours and escalation policies included
infrawrench_on_call_scheduleAn on-call rotation a routing rule can name as a destination
infrawrench_slack_channelSlack channels as destinations
infrawrench_msteams_webhookTeams webhooks as destinations
infrawrench_digest_settingsWhen the weekly digest is sent
infrawrench_digest_recipientAn email address the digest goes to
infrawrench_jira_integrationThe Jira connection
infrawrench_linear_integrationThe Linear connection

Data sources

Data sourceReads
infrawrench_accountsConnected accounts — for resolving account ids
infrawrench_pluginsAvailable plugins — for resolving plugin ids
infrawrench_cost_centresCost centres, including ones not managed by Terraform
infrawrench_resourcesSynced resources — for resolving a probe or schedule target
infrawrench_permissionsThe permission catalogue roles and keys grant from
infrawrench_slack_installationsConnected Slack workspaces

Things worth knowing

Objects deleted outside Terraform come back. Every resource does a real read on refresh. If someone deletes a budget in the app, the next plan shows it as needing to be created again rather than failing.

Live figures aren’t in state. A budget’s month-to-date spend, an alert’s last-fired time, a probe’s uptime, an export’s last run — none of it appears as a Terraform attribute. It changes constantly, and putting it in state would make every plan noisy for no benefit. Read those from the app, the CLI, or the API.

Singletons don’t really get destroyed. There is always exactly one anomaly settings row, one routing table, one tag policy — so terraform destroy can’t remove them. Each one does the sensible thing instead, and it differs on purpose:

  • The cost settings and alert routing restore the shipped defaults. An organization routing nothing at all is a worse state than the default one.
  • Currency settings clear the display currency, which turns conversion off. Your stated exchange rates survive, so you can turn it back on without re-entering them.
  • Session recording is deliberately left running. Silently disabling an audit control because someone deleted a resource block is not a safe default.
  • The drift, expiry and posture alert settings are left alone — they have no documented shipped values to restore to.
  • Network flow collection is turned off, and it’s the one singleton where destroy deliberately changes something. Collection runs queries your cloud provider bills to your own account; leaving it running for a resource you deleted would keep spending your money with nothing in Terraform left to explain why.

Deletes can be refused. Saved filters and scenario models something still points at return a conflict rather than being deleted, and the error names what’s referencing them. A role members still hold, and a managed account with invoices against it, are refused the same way. Repoint or reassign first — Terraform won’t decide for you which permission set those people should get instead.

Some things are create-only. A probe’s linked resource, a schedule’s resource and account, a Slack channel’s workspace, a deploy trigger’s repo and branch — the API has no route to change them, so Terraform replaces instead. The plan says so; it’s worth reading before applying to a bastion, where replacing means a new enrollment token and an agent that has to be restarted with it.

Credentials are marked sensitive. Export keys, account credentials, API keys, SSH private keys, bastion tokens and webhook URLs never appear in plan output in plaintext. They do appear in state — see the warning above.

Billing rule amounts are in the major currency unit. Everything else in Infrawrench counts cents; a fixed billing rule’s amount is dollars. It’s the one inconsistency, and it is inherited from the API.

Custom graph source isn’t type-checked at plan time. The API has a checker, but calling it from a plan would mean your code had to compile before Terraform would tell you what it was about to change. A graph whose source doesn’t compile is stored and fails when it renders.

  • Config as code — move a whole organization’s configuration as one document
  • Terraform export — write your cloud resources out as HCL
  • The CLI — the same objects from a terminal
  • HTTP API — what the provider calls underneath

Supported providers

44 providers · 340+ resource types across cloud, infrastructure, databases, and more.