Skip to main content
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, barndoor_agent, barndoor_policy).
  • 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).
  • 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

~> 0.3 accepts patch and minor updates within the 0.x line as they release. Check the changelog 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:
Most resources import by their UUID, with these exceptions: 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:
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.

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).

CI/CD with GitHub Actions

The standard shape — plan on pull requests, apply on merge to main:
.github/workflows/terraform.yml
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

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.
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 may not carry a path suffix; the provider appends each service’s API prefix itself. Use the bare origin.
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.
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.
The provider makes one HTTP request per operation with no client-side retry. If a large apply trips transient errors, re-run terraform apply — it converges from wherever it stopped. Consider -parallelism=1 for very large first-time applies.
Run with TF_LOG=DEBUG — the provider logs request/response detail, including full error bodies, at debug level.