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

# Manage Murmur with Terraform

> Manage workspaces, placements, personas, and the rest of your catalog as code with the Murmur Terraform provider, alongside the cloud infrastructure they run on.

The Murmur Terraform provider, [`prassoai/murmur`](https://registry.terraform.io/providers/prassoai/murmur/latest), manages your tenant's [catalog](https://docs.murmur.dev/catalog/overview.md) from Terraform. Every resource you would write with [`murmur set`](https://docs.murmur.dev/cli/set) (a [workspace](https://docs.murmur.dev/concepts/workspaces.md), a [placement](https://docs.murmur.dev/concepts/placement.md), an [agent persona](https://docs.murmur.dev/catalog/agent-persona.md), a [tenant binding](https://docs.murmur.dev/catalog/tenant-binding.md)) becomes a `murmur_catalog_resource` that you review in a plan and apply like any other infrastructure.

The provider's attribute reference is on the [Registry](https://registry.terraform.io/providers/prassoai/murmur/latest/docs). This guide shows how the pieces fit together.

## Install the provider

```hcl theme={null}
terraform {
  required_providers {
    murmur = {
      source  = "prassoai/murmur"
      version = "~> 0.1"
    }
  }
}

provider "murmur" {
  tenant = "github_app/your-org" # "<provider>/<org>" from the tenant block in .murmur/murmur.yaml
  auth   = "api_key"              # reads MURMUR_API_KEY
}
```

`terraform init` downloads the provider from the Registry and records its checksums in `.terraform.lock.hcl`. Commit that file.

`auth` chooses who Terraform acts as. Shared configurations and CI [apply as a service profile](#apply-as-a-service-profile). To try the provider from your own machine, [apply as yourself](#apply-as-yourself).

## Apply as yourself

With `auth = "gh"`, Terraform calls Murmur as you, with exactly the [permissions](https://docs.murmur.dev/security/permissions.md) you hold, using the GitHub CLI's login on the machine running Terraform:

```hcl theme={null}
provider "murmur" {
  tenant = "github_app/your-org"
  auth   = "gh"
}
```

Use it for a first plan from your own machine. Tenants that sign in with Google apply as a service profile.

## Apply as a service profile

A [service profile](https://docs.murmur.dev/concepts/service-profiles.md) is a non-human identity. Terraform authenticates as one with an [API key](https://docs.murmur.dev/security/api-keys.md), and the profile holds exactly the permissions the configuration needs. An organization admin sets it up once.

### 1. Create the service profile

```bash theme={null}
cat <<'EOF' | murmur set service-profile terraform
name: terraform
description: "Applies the catalog from Terraform"
EOF
```

A name and description are all the profile needs: Terraform uses it to read and write catalog resources.

### 2. Grant it the permissions the configuration needs

Grant the profile its permissions with a [tenant binding](https://docs.murmur.dev/catalog/tenant-binding.md) that names the profile as `service-profile:terraform`. Each Terraform operation needs one [permission](https://docs.murmur.dev/security/permission-reference.md) on the resource's kind:

| Terraform operation | Permission |
| - | - |
| `plan`, `refresh`, `import` | `{kind}.read` |
| `apply` creating a resource | `{kind}.create` |
| `apply` updating a resource | `{kind}.edit` |
| `destroy`, or removing a resource from configuration | `{kind}.delete` |

Two cases need a second permission:

* Updating a workspace's `grants` also needs `workspace.administer`, checked against the grants saved before the change. The binding below includes it.
* A resource named `{workspace}/{slug}` lives inside a workspace and also needs `workspace.use` on that workspace. A workspace's own grants confer it: the [suggested defaults](https://docs.murmur.dev/catalog/workspace.md#access-grants) give it to every service profile through `service-profile:*`. For a workspace whose grants leave that out, name `service-profile:terraform` in them.

List each kind your configuration manages. For workspaces, placements, and agent personas:

```bash theme={null}
cat <<'EOF' | murmur set tenant-binding terraform
name: terraform
description: "Catalog resources managed by Terraform"
grant:
  users:
    - service-profile:terraform
  inline:
    permissions:
      - workspace.read
      - workspace.create
      - workspace.edit
      - workspace.administer
      - workspace.delete
      - placement.read
      - placement.create
      - placement.edit
      - placement.delete
      - agent-persona.read
      - agent-persona.create
      - agent-persona.edit
      - agent-persona.delete
EOF
```

List each verb Terraform uses. In a tenant binding, `workspace.*` also confers `workspace.use` on every workspace, including those whose grants leave the profile out.

### 3. Create an API key for the profile

Go to **Organization Settings → Automations → API Keys**, click **Create API key**, and choose the `terraform` service profile. The full key, starting `mur_`, is shown once. See [API keys](https://docs.murmur.dev/security/api-keys.md) for the full steps.

### 4. Apply with the key

Store the key as the `MURMUR_API_KEY` secret of the CI system that runs Terraform, and keep `auth = "api_key"` in the provider block:

```yaml theme={null}
- name: Terraform apply
  env:
    MURMUR_API_KEY: ${{ secrets.MURMUR_API_KEY }}
  run: terraform apply -auto-approve
```

Run `terraform plan` on pull requests and `terraform apply` on merge, and keep state in a [remote backend](https://developer.hashicorp.com/terraform/language/backend). Restrict access to that state as you would to the catalog itself: it holds a copy of every resource it manages.

The provider also accepts the key inline as `api_key`. Prefer the environment variable, which keeps the key out of configuration.

## Manage catalog resources

Each resource names a `kind` and a `name`, plus its body. JSON-backed kinds take `payload`; markdown-backed kinds such as `agent-persona` take `markdown`:

```hcl theme={null}
resource "murmur_catalog_resource" "reviewer" {
  kind     = "agent-persona"
  name     = "reviewer"
  markdown = file("${path.module}/agent-persona/reviewer.md")
}

resource "murmur_catalog_resource" "backend" {
  kind    = "workspace"
  name    = "backend"
  payload = jsonencode(yamldecode(file("${path.module}/workspace/backend.yaml")))

  # Image rollouts update imageRef; Terraform leaves it alone.
  ignore_fields = ["imageRef"]
}
```

`payload` is compared as parsed JSON, so a plan shows changed values and ignores key order and formatting. The files hold the same YAML you would pipe to `murmur set`. To manage a whole directory of them with two resources and `for_each`, see [Managing a Catalog from Files](https://registry.terraform.io/providers/prassoai/murmur/latest/docs/resources/catalog_resource#managing-a-catalog-from-files) on the Registry.

Every workspace that bakes or rolls out [images](https://docs.murmur.dev/concepts/images.md) should set `ignore_fields = ["imageRef"]`. The first apply still needs an `imageRef` in the payload; after that, Terraform keeps whatever image the workspace is running.

Set secrets with [`murmur secret set`](https://docs.murmur.dev/cli/secret-set). Terraform state is stored in plaintext, so the provider rejects credential-bearing kinds such as `secret`.

## Infrastructure and placement in one configuration

The [customer placement](https://docs.murmur.dev/guides/customer-placements.md) modules emit a ready-made `placement` output. Feed it to the provider instead of piping it through the CLI, and the cloud resources, the placement, and the workspace that uses it all come from one apply:

```hcl theme={null}
module "murmur_placement" {
  source = "git::https://github.com/prassoai/terraform-modules.git//modules/gcp-wif?ref=v0.2.0"

  project_id          = "your-murmur-project"
  tenant_id           = "github_app/your-org"
  vm_service_accounts = ["murmur-vm@your-murmur-project.iam.gserviceaccount.com"]

  placement_name   = "my-gcp"
  placement_zones  = ["us-central1-a", "us-central1-b"]
  placement_subnet = "projects/your-murmur-project/regions/us-central1/subnetworks/agents"
}

locals {
  placement = module.murmur_placement.placement
}

resource "murmur_catalog_resource" "placement" {
  kind = "placement"
  name = local.placement.name

  # Admit my-workspace to every identity the placement carries.
  payload = jsonencode(merge(local.placement, {
    service_account_bindings = [
      for binding in local.placement.service_account_bindings : merge(binding, {
        workspace_use = { scope = "WORKSPACE_USE_SCOPE_SELECTED", workspaces = ["my-workspace"] }
      })
    ]
  }))
}

resource "murmur_catalog_resource" "workspace" {
  kind = "workspace"
  name = "my-workspace"
  payload = jsonencode({
    name            = "my-workspace"
    placement       = murmur_catalog_resource.placement.name
    environment_ref = "default"
    service_account = "murmur-vm@your-murmur-project.iam.gserviceaccount.com"
    image_ref       = "my-recipe-0f5e1c8a2b3d4e5f"
    repos           = [{ clone_url = "https://github.com/your-org/your-repo", base_branch = "main" }]
  })

  ignore_fields = ["imageRef"]
}
```

The workspace refers to the placement resource, so Terraform creates the placement first. The AWS module (`modules/aws-wif`) emits the same `placement` output. Baking the image the workspace runs is still a [`murmur bake`](https://docs.murmur.dev/cli/bake); see step 3 of the placement guide for [GCP](https://docs.murmur.dev/guides/customer-placements.md#3-rebake-your-images-into-your-project) or [AWS](https://docs.murmur.dev/guides/customer-placements.md#3-rebake-your-images-into-your-account).

## Bring existing resources under Terraform

Resources you already created with the CLI or dashboard can be imported by `<kind>/<name>`:

```bash theme={null}
terraform import murmur_catalog_resource.backend workspace/backend
```

Or declare it in configuration:

```hcl theme={null}
import {
  to = murmur_catalog_resource.backend
  id = "workspace/backend"
}
```

Import reads the live resource. A resource created with the CLI or dashboard gets one update in its next plan: the claim that makes Terraform its owner (`managed_by` becomes `terraform`). Apply it; after that the plan is empty.

## Who owns a resource

Each resource has one writer. Once Terraform manages it, an edit in the dashboard or CLI shows up as drift on the next plan, and applying writes the Terraform value back. If another system manages the resource, the apply fails and names that system; set `force_ownership = true` for a single apply to take it over.

Every write is conditional on the version Terraform last saw. If someone changes a resource between your plan and your apply, the apply fails with a conflict and leaves their change in place. Run `terraform plan` again to review it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.