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:

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 storage credentials such as S3 access keys and Azure account keys or SAS tokens) 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 storage credentials) 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).
  • barndoor_notification_channel.signing_secret is never refreshed. The platform reveals a webhook channel’s signing secret exactly once, on creation or rotation, and cannot return it again — so state holds whatever was last issued to Terraform, and a plan will never show a difference even if the secret was rotated elsewhere. Rotate through rotate_when_changed (which rotates in place, preserving the channel) so Terraform stays the source of truth. On import the attribute is empty, because Terraform was not the recipient of the reveal; has_signing_secret still reports whether the platform holds one.

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.
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.
Run with TF_LOG=DEBUG — the provider logs request/response detail, including full error bodies, at debug level.