---
title: Terraform
path: integrations/terraform
status: published
---

# Terraform

Provision ScaiVault resources — secrets, policies, rotation policies, PKI roles, dynamic engines — declaratively. The Terraform provider wraps the same REST API the SDKs use, so what you can do via curl you can do via `tf apply`.

## Install the provider

```hcl
terraform {
  required_providers {
    scaivault = {
      source  = "scailabs/scaivault"
      version = "~> 1.0"
    }
  }
}

provider "scaivault" {
  base_url      = "https://scaivault.scailabs.ai"
  token         = var.scaivault_token  # or env SCAIVAULT_TOKEN
  partner_id    = var.partner_id       # optional
  tenant_id     = var.tenant_id        # optional
}
```

For Terraform Cloud / Atlantis / CI runs, set `SCAIVAULT_TOKEN` as an environment variable. Don't put it in `.tfvars`.

## Anti-pattern: managing secret *values* in Terraform

```hcl
# DON'T DO THIS
resource "scaivault_secret" "stripe" {
  path = "environments/production/billing/stripe"
  data = {
    secret_key = "sk_live_xxx"  # secret in state file
  }
}
```

The secret value ends up in Terraform state, defeating the point of using ScaiVault. Two better patterns:

**Pattern A:** Provision the metadata (path, type, rotation policy) but not the value. Use `lifecycle.ignore_changes` on `data`:

```hcl
resource "scaivault_secret" "stripe" {
  path        = "environments/production/billing/stripe"
  secret_type = "api_key"
  metadata = {
    tags  = ["billing", "stripe", "production"]
    owner = "team:billing"
  }
  options = {
    rotation_policy_id = scaivault_rotation_policy.api_keys_quarterly.id
    max_versions       = 10
  }
  lifecycle {
    ignore_changes = [data]
  }
  data = {
    secret_key = "PLACEHOLDER"  # written once by Terraform, owned by humans/automation afterwards
  }
}
```

The placeholder gets written once; subsequent rotations update the value without Terraform fighting them.

**Pattern B:** Generate via `secret_policy`. Terraform doesn't see the value at all:

```hcl
resource "scaivault_secret_policy" "strong_password" {
  name        = "strong-password"
  policy_type = "password"
  fields = [{
    name      = "password"
    generator = "random"
    config = {
      length         = 32
      charset        = "alphanumeric+symbols"
      require_upper  = true
      require_digit  = true
      require_symbol = true
    }
  }]
}

resource "scaivault_secret" "db_password" {
  path             = "environments/production/billing/db-password"
  secret_type      = "kv"
  generate_from    = scaivault_secret_policy.strong_password.id
  rotation_policy  = scaivault_rotation_policy.weekly.id
}
```

`generate_from` writes the secret by running the generator, returning only metadata to Terraform. The value never enters state.

## What to manage in Terraform

The boundary that works in practice:

| Resource | Manage in Terraform? | Why |
|----------|---------------------|-----|
| Access policies | Yes | Versioned, reviewed, code-review-friendly |
| Policy bindings | Yes | Same |
| Rotation policies | Yes | Schedule and grace period are config, not data |
| Secret policies (value generation) | Yes | Templates are config |
| PKI roles | Yes | Config |
| PKI CAs | Yes (creation), No (root key) | The CA cert is config; the key stays in ScaiVault |
| Dynamic engines | Yes | Connection config |
| Dynamic roles | Yes | SQL templates are config |
| Federation backends | Yes | Connection config |
| Webhooks | Yes | URL and event list |
| Subscriptions | Sometimes | If long-lived, yes; if per-pod, manage from app |
| Secret values | **No** | Lives in ScaiVault; managed by services or rotation |
| Service accounts | In ScaiKey, not ScaiVault | Identity is upstream |

## Example: production billing service setup

```hcl
# A rotation policy
resource "scaivault_rotation_policy" "api_keys_quarterly" {
  name           = "api-keys-quarterly"
  interval       = "90d"
  grace_period   = "48h"
  warn_before    = "7d,1d"
  auto_generate  = false
  webhook_ids    = [scaivault_webhook.rotation_alerts.id]
}

# A weekly rotation that auto-generates strong passwords
resource "scaivault_rotation_policy" "weekly_db_passwords" {
  name           = "weekly-db-passwords"
  interval       = "7d"
  grace_period   = "24h"
  auto_generate  = true
  secret_policy_id = scaivault_secret_policy.strong_password.id
}

# Webhook receiver for rotation events
resource "scaivault_webhook" "rotation_alerts" {
  name   = "rotation-alerts"
  url    = "https://ops.acme.example/scaivault/webhook"
  secret = var.webhook_signing_secret  # stored in Terraform Cloud as sensitive
  events = ["rotation.due", "rotation.overdue", "rotation.failed"]
  filters = {
    path_prefix = "environments/production/"
  }
}

# Access policy: billing service reads its own secrets
resource "scaivault_policy" "billing_prod_reader" {
  name        = "billing-prod-reader"
  description = "Production billing service reads its credentials"
  rules = [{
    path_pattern = "environments/production/billing/**"
    permissions  = ["read"]
  }]
}

resource "scaivault_policy_binding" "billing_sa" {
  policy_id      = scaivault_policy.billing_prod_reader.id
  identity_type  = "service_account"
  identity_id    = "sa:billing-prod"
}

# Secrets — metadata only, values managed elsewhere
resource "scaivault_secret" "stripe" {
  path        = "environments/production/billing/stripe"
  secret_type = "api_key"
  metadata    = { tags = ["stripe", "production"], owner = "team:billing" }
  options     = { rotation_policy_id = scaivault_rotation_policy.api_keys_quarterly.id }
  lifecycle { ignore_changes = [data] }
  data        = { secret_key = "PLACEHOLDER" }
}

resource "scaivault_secret" "database" {
  path           = "environments/production/billing/database-password"
  secret_type    = "kv"
  generate_from  = scaivault_secret_policy.strong_password.id
  rotation_policy = scaivault_rotation_policy.weekly_db_passwords.id
  metadata       = { tags = ["database", "production"] }
}
```

## Reading secrets in Terraform

There's a data source for read access, useful for fetching values *that already exist* to feed into other resources (e.g., into an AWS provider that needs an API key):

```hcl
data "scaivault_secret" "stripe_webhook_secret" {
  path = "environments/production/billing/stripe-webhook"
}

resource "aws_api_gateway_authorizer" "stripe" {
  # ...
  identity_validation_expression = data.scaivault_secret.stripe_webhook_secret.data.signing_secret
}
```

**The value lands in Terraform state.** If your state file is backed by S3+KMS with restricted access, this is acceptable for build-time configuration. For values you don't want in state, *don't* read them via data source — have the application read them at runtime instead.

## State backend security

Whichever way you go, treat the Terraform state as sensitive material:

- Encrypted-at-rest backend (S3+KMS, Terraform Cloud with workspace-level encryption, GCS+CMEK).
- IAM scoped tightly — only Terraform runners and a small admin set.
- State lock backend (DynamoDB, gcs locks) — concurrent modification of policies is a bad time.
- No state files in git, even encrypted.

## CI integration

Typical pipeline:

1. `terraform plan` runs on every PR. Output reviewed.
2. On merge, `terraform apply` runs against the production workspace.
3. Token comes from a short-lived ScaiKey-issued JWT, scoped to whatever the runner needs (often quite broad — provisioning policies and rotation policies needs `admin` for that subset of operations).
4. After apply, Terraform Cloud runs a sanity-check job that reads back a few critical policies via `data.scaivault_policy` and asserts shape.

## Common questions

**"Can I import existing ScaiVault resources into Terraform?"** Yes:

```bash
terraform import scaivault_policy.billing_prod_reader pol_abc123
terraform import scaivault_rotation_policy.weekly_db_passwords rot_weekly
```

The provider supports import for all resource types. Run `terraform plan` after import to see drift between state and config; close the gap by adjusting either side.

**"What about workspaces?"** Use Terraform workspaces (or Terraform Cloud workspaces) to separate environments. The provider config can be parameterized: `tenant_id = var.tenant_id`, set per-workspace.

**"Drift?"** Inevitable. A human flips a policy via the admin UI; Terraform reports drift on next plan. Either revert the change or codify it. Bias toward codifying — humans aren't authoritative about what's intended.

**"How do I handle dependencies between resources?"** Standard Terraform dependency tracking. `scaivault_policy_binding` depends on `scaivault_policy` via interpolation; Terraform sequences correctly. The provider's API doesn't have batch operations, so a large initial apply can take a few minutes for ~100 resources.

## What's next

- [GitHub Actions](./github-actions) — wiring CI/CD to read secrets.
- [Kubernetes](./kubernetes) — once Terraform sets up the policies, runtime consumes them.
- [Policies guide](../api-guides/policies) — what the rules and conditions actually mean.
