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 nextterraform 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
descriptionortags = ["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_secretviaBARNDOOR_CLIENT_SECRET, never in configuration. In CI, use your platform’s secret store (see CI/CD). - Mark your own secret inputs (
api_keyvariables and the like) assensitive = 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_SECRETwherever 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 supportsterraform 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:
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 livebarndoor_llm_model_pricingrules 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:
Plans that look like drift but aren’t
barndoor_llm_model_pricing.idchanges 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: removingrequests_per_minuteortokens_per_minutefrom 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
- Only
BARNDOOR_CLIENT_SECRETis 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
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.base_url must be the platform host root
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
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'
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
Intermittent failures under heavy applies
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.Seeing the full API exchange
Seeing the full API exchange
Run with
TF_LOG=DEBUG — the provider logs request/response detail, including full error bodies, at debug level.