# Best Practices

Patterns for running the Barndoor Terraform provider in production, and the platform semantics worth knowing before you rely on it.

## One owner per object

Give every Barndoor object exactly one owner: **either Terraform or the portal, never both**. If an admin edits a Terraform-managed policy in the app, the next `terraform plan` shows a diff and the next apply reverts their change — usually a surprise for both sides.

Practical rules:

* Agree which product areas are code-managed and note it where admins will see it (a policy's `description` or `tags = ["terraform-managed"]` work well).
* To *reference* portal-managed objects from Terraform without owning them, use the data sources: [`barndoor_mcp_server`](https://registry.terraform.io/providers/barndoor-ai/barndoor/latest/docs/data-sources/mcp_server), [`barndoor_agent`](https://registry.terraform.io/providers/barndoor-ai/barndoor/latest/docs/data-sources/agent), [`barndoor_policy`](https://registry.terraform.io/providers/barndoor-ai/barndoor/latest/docs/data-sources/policy), [`barndoor_llm_provider`](https://registry.terraform.io/providers/barndoor-ai/barndoor/latest/docs/data-sources/llm_provider), [`barndoor_dlp_detection_engine`](https://registry.terraform.io/providers/barndoor-ai/barndoor/latest/docs/data-sources/dlp_detection_engine), plus catalog lookups [`barndoor_mcp_server_directory`](https://registry.terraform.io/providers/barndoor-ai/barndoor/latest/docs/data-sources/mcp_server_directory) and [`barndoor_agent_directory`](https://registry.terraform.io/providers/barndoor-ai/barndoor/latest/docs/data-sources/agent_directory).
* To *take ownership* of a portal-created object, import it (below) — from then on, hands off in the UI.

## Secrets and state hygiene

* Supply `client_secret` via `BARNDOOR_CLIENT_SECRET`, never in configuration. In CI, use your platform's secret store (see [CI/CD](#ci%2Fcd-with-github-actions)).
* Mark your own secret inputs (`api_key` variables and the like) as `sensitive = true`.
* Barndoor's write-only credential attributes (`barndoor_llm_provider.api_key`, MCP server OAuth secrets, log-export access keys) are stored in the platform's secret store and **never returned by the API** — but the values you supplied still live in your Terraform **state file**. Treat state as secret: use a remote backend with encryption and restricted access.
* Rotate the provider credential on a schedule that fits your team. Rotation keeps the client ID and invalidates only the secret — update `BARNDOOR_CLIENT_SECRET` wherever it's stored, and remember one credential is active per organization, so every pipeline shares it.

## Pin the provider version

```hcl theme={null}
tf
terraform {
  required_providers {
    barndoor = {
      source  = "barndoor-ai/barndoor"
      version = "~> 0.3"
    }
  }
}
```

`~> 0.3` accepts patch and minor updates within the 0.x line as they release. Check the [changelog](https://github.com/barndoor-ai/terraform-provider-barndoor/blob/main/CHANGELOG.md) before widening the range.

## Importing existing configuration

Every resource supports `terraform import`, so an organization configured through the portal can move to code incrementally. With Terraform 1.5+, prefer `import` blocks — they're reviewable and plannable:

```hcl theme={null}
import {
  to = barndoor_policy.search_read_only
  id = "6f1c9312-8f3e-4bde-9c2b-b3a1d1cf34a4"
}
```

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

Most resources import by their UUID, with these exceptions:

| Resource                                                                    | Import ID                                                                                                                 |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `barndoor_connection`                                                       | The **server's** ID or slug (a server has at most one tenant-wide connection)                                             |
| `barndoor_log_export`                                                       | Your organization ID, or `{organization_id}/{export_type}`                                                                |
| `barndoor_llm_model_pricing`                                                | The rule's logical identity: `{model_provider}\|{model_pattern}`, or a bare `{model_pattern}` for provider-unscoped rules |
| `barndoor_dlp_org_config`, `barndoor_idp`, `barndoor_llm_governance_config` | Your organization ID (they're org singletons)                                                                             |

Two things to expect right after an import:

* **Write-only attributes come back empty.** Credentials the platform never returns (OAuth secrets, API keys, log-export access keys) can't be populated by import. The first plan after importing will propose re-sending any such values present in your configuration — that in-place update is expected and safe.
* **Some resources only adopt via import.** `barndoor_idp`, `barndoor_dlp_field_control_policy`, and live `barndoor_llm_model_pricing` rules deliberately refuse to create over an existing object, so out-of-band setup is never silently overwritten — the error message directs you to import instead.

## Destroy is not always delete

`terraform destroy` maps to whatever removal means for each object on the platform:

| Resource                                                    | What destroy actually does                                                                       |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `barndoor_policy`                                           | **Archives** the policy (terminal lifecycle state, kept for audit; never hard-deleted)           |
| `barndoor_llm_model_pricing`                                | Archives the rule with full version history; recreating the same rule restores it                |
| `barndoor_mcp_server`                                       | Soft-deletes the server; its connections and stored credentials are torn down, the name is freed |
| `barndoor_agent`                                            | Unregisters the agent; the platform archives dependent policies                                  |
| `barndoor_log_export`                                       | Removes the destination configuration; the platform-provisioned export slot survives             |
| `barndoor_dlp_org_config`, `barndoor_llm_governance_config` | Resets the org singleton to platform defaults; nothing is deleted                                |
| Everything else                                             | Deletes the object                                                                               |

<Warning>
  Renames of immutable fields are replacements, and replacements are destroys. For example, `barndoor_policy.mcp_server_id` is immutable — changing it archives the existing policy and creates a new one. Read the plan output for `-/+` markers before applying.
</Warning>

## Plans that look like drift but aren't

* `barndoor_llm_model_pricing.id` **changes on every price update** — pricing is an append-only versioned store, and each change creates a new version. That's by design, not drift.
* `barndoor_llm_rate_limit`: removing `requests_per_minute` or `tokens_per_minute` from configuration **clears that metric on the platform** — the plan is telling the truth; make sure it's what you meant.
* After importing resources with write-only credentials, the first plan re-sends those values (see [Importing](#importing-existing-configuration)).

## CI/CD with GitHub Actions

The standard shape — plan on pull requests, apply on merge to main:

```yaml .github/workflows/terraform.yml theme={null}
name: terraform
on:
  pull_request:
  push:
    branches: [main]

env:
  BARNDOOR_BASE_URL: https://platform.barndoor.ai
  BARNDOOR_TOKEN_URL: https://auth.barndoor.ai/realms/barndoor/protocol/openid-connect/token
  BARNDOOR_CLIENT_ID: ${{ vars.BARNDOOR_CLIENT_ID }}
  BARNDOOR_ORGANIZATION_ID: ${{ vars.BARNDOOR_ORGANIZATION_ID }}
  BARNDOOR_CLIENT_SECRET: ${{ secrets.BARNDOOR_CLIENT_SECRET }}

jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
      - run: terraform plan -input=false
        if: github.event_name == 'pull_request'
      - run: terraform apply -input=false -auto-approve
        if: github.ref == 'refs/heads/main'
```

Notes:

* Only `BARNDOOR_CLIENT_SECRET` is a secret; the other four values are identifiers and can live in plain repository variables.
* Because one provider credential is active per organization, CI and humans share it — if you rotate it, update the CI secret in the same change.
* Use a remote state backend (S3, GCS, Terraform Cloud, …) so CI runs and local runs see the same state.

## Troubleshooting

* **Error: ... requires the organization admin role**  
  The credential reached the API but was refused. Confirm `organization_id` matches the organization the credential was generated in (copy both from **Settings → API Tokens**), and that the credential hasn't been revoked.
* **Token request failed: invalid_client or unauthorized_client**  
  The token endpoint rejected the client ID/secret pair — the secret is wrong, was rotated out from under you, or the credential was revoked. Mint a fresh secret by rotating in **Settings → API Tokens**.
* **base_url must be the platform host root**  
  `base_url` may not carry a path suffix; the provider appends each service's API prefix itself. Use the bare origin.
* **409 Conflict on create**  
  You're creating something that already exists and has a uniqueness constraint — a duplicate policy name, a second rate limit on the same scope and traffic type, a second connection on a server. Either import the existing object or change the conflicting field.
* **Blocks vs. attribute syntax: 'Blocks of type X are not expected here'**  
  Nested values in this provider are **attributes**, assigned with `=` — for example `destination = { ... }` on `barndoor_log_export`. Writing them as blocks (`destination { ... }`) is a syntax error.
* **Intermittent failures under heavy applies**  
  Since v0.4.0 the provider retries transient failures (rate limiting, gateway errors, connection blips) with exponential backoff, so most blips never surface. If an apply still fails partway, re-run `terraform apply` — it converges from wherever it stopped.
* **Seeing the full API exchange**  
  Run with `TF_LOG=DEBUG` — the provider logs request/response detail, including full error bodies, at debug level.
