Guides

Manage AccessFlow with Terraform.

Last updated

What you are building

Everything you configured by clicking — datasources, review plans, routing, masking and row-security policies, AI configs, notification channels — can be declared in HCL instead and applied from CI. This guide gets you from nothing to a first apply.

The provider is published as bablsoft/accessflow and works with both Terraform and OpenTofu; the two registries carry the same namespace and name. You need an AccessFlow instance reachable from wherever you run terraform, and about fifteen minutes.

Can AccessFlow be managed with Terraform?

Yes. The official provider manages datasources, review plans, routing, row-security and masking policies, SQL review rulesets and custom rules, AI configs and notification channels over the REST API, authenticating with a service-account API key. Requests themselves — queries, API calls, deployments — are runtime objects and are deliberately not Terraform-managed.

1. Get a service-account API key

Terraform needs credentials that do not depend on anyone's browser session. The intended path is a service account: an account that can never sign in interactively (by password or single sign-on) and authenticates only with an API key. There are two ways to create one.

From the admin screens. Sidebar → Security & Access → Identity → Service accounts → Create service account. Pick a role that holds the management permissions for what your Terraform code owns — a custom role, or Admin if it manages everything. Then, on the account's API keys tab, click Issue key and copy the af_… value, which is shown once. Keys issued this way can be rotated with a grace period and revoked at any time. See service accounts.

Declared in configuration — the better fit when Terraform is what provisions everything else, so the account must exist before anyone signs in. You generate the key yourself, and AccessFlow only ever stores its hash. Generate it first — it is just a random token with an af_ prefix:

create the secret
kubectl create secret generic accessflow-bootstrap-secrets \
  --from-literal=admin-password="$(openssl rand -base64 24)" \
  --from-literal=ci-api-key="af_$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')"

Then declare the account in your Helm values. Bootstrap creates it on the next start:

values.yaml
bootstrap:
  enabled: true
  serviceAccounts:
    - email: terraform@acme.example.com
      displayName: Terraform CI
      role: ADMIN   # always set it — narrow it if Terraform manages less
      apiKeyName: terraform
      apiKeySecretRef:
        name: accessflow-bootstrap-secrets
        key: ci-api-key
      # Optional ISO-8601 instant; never expires when omitted.
      # apiKeyExpiresAt: "2027-01-01T00:00:00Z"

Outside Kubernetes the same fields are environment variables — ACCESSFLOW_BOOTSTRAP_SERVICE_ACCOUNTS_0_EMAIL, _DISPLAY_NAME, _ROLE, _API_KEY_NAME, _API_KEY and the optional _API_KEY_EXPIRES_AT. When role is left out it defaults to Admin, so set it explicitly. The role and display name are applied again whenever the configuration changes, so you can narrow the account later by editing role and restarting. Rotating a declared key is a matter of changing the secret and restarting AccessFlow: bootstrap re-imports it in place on startup. For the same reason it cannot be revoked from the admin screens — the next change would bring it back — so retire it in the configuration, or deactivate the account.

Just evaluating? Any user can create a key for themselves at Profile settings → API keys. The raw value is shown once. It works fine for a first apply, but a personal key inherits that person's account — use a service account for anything that outlives the experiment.

2. Configure the provider

Two arguments, both optional in HCL because each falls back to an environment variable — which is what you want in CI, so the key never lands in a state file or a diff.

main.tf
terraform {
  required_providers {
    accessflow = {
      source = "bablsoft/accessflow"
    }
  }
}

# endpoint and api_key also read ACCESSFLOW_ENDPOINT and ACCESSFLOW_API_KEY.
provider "accessflow" {
  endpoint = "https://accessflow.example.com"
  api_key  = var.accessflow_api_key
}

variable "accessflow_api_key" {
  type      = string
  sensitive = true
}

An explicit HCL value wins over the environment variable. Supply neither and the provider stops with a message naming the argument and the variable it looked for.

3. Declare something and apply it

Start with a review plan and a datasource that references it — the smallest pair that does something real.

main.tf
resource "accessflow_review_plan" "standard" {
  name                    = "standard"
  description             = "AI review then one human approval; reads auto-approved."
  requires_ai_review      = true
  requires_human_approval = true
  min_approvals_required  = 1
  approval_timeout_hours  = 24
  auto_approve_reads      = true

  approvers = [
    {
      user_id = "11111111-1111-1111-1111-111111111111"
      stage   = 1
    }
  ]
}

resource "accessflow_datasource" "prod_postgres" {
  name          = "prod-postgres"
  db_type       = "POSTGRESQL"
  host          = "postgres.prod.internal"
  port          = 5432
  database_name = "app"
  username      = "af_reader"
  password      = var.prod_postgres_password # write-only
  ssl_mode      = "REQUIRE"

  require_review_writes = true
  ai_analysis_enabled   = true
  review_plan_id        = accessflow_review_plan.standard.id
}

Then the usual three commands — tofu works identically:

apply
export ACCESSFLOW_ENDPOINT="https://accessflow.example.com"
export ACCESSFLOW_API_KEY="af_…"

terraform init
terraform plan
terraform apply
Secrets do not appear in a plan. Database passwords, AI API keys and notification channel config are write-only: AccessFlow never returns them, so Terraform cannot detect that one changed outside its state. Change the value in your variable and it is applied; change it in the AccessFlow UI and Terraform will not notice.

What the provider manages

ResourceManages
accessflow_datasourceA governed database connection — host, credentials, SSL mode, and the review plan and AI config attached to it.
accessflow_review_planAn approval policy: AI review, human approval, minimum approvals, timeout, and the approver rows.
accessflow_routing_policyAttribute-based routing — auto-approve, auto-reject, require more approvals, or escalate.
accessflow_row_security_policyA row-level predicate, nested under a datasource.
accessflow_masking_policyA column masking strategy, nested under a datasource.
accessflow_ai_configAn AI analyzer configuration. Its API key is write-only.
accessflow_notification_channelA notification channel — email, Slack, webhook, Discord, Telegram, Microsoft Teams or PagerDuty. Changing its type replaces the resource.
accessflow_sql_review_rulesetA SQL review ruleset — which built-in and custom rules are off, warn, or block for one environment. Set a datasource's environment to bind it; a datasource with no environment, or one whose environment has no ruleset, uses the organization-wide default.
accessflow_sql_review_ruleA custom SQL review rule — your own criteria (which statements, tables, functions or patterns to flag), the message reviewers see, and how serious it is by default. Once saved, list it in any ruleset alongside the built-in rules.

Two data sources — accessflow_datasource and accessflow_review_plan — look an existing object up by id.

Not covered by the provider. API connectors and deployment pipelines are configured in the app, not in HCL. Their runtime half has its own automation: see Gate a CI/CD pipeline.

Every resource supports terraform import. The two nested policies take a composite id:

import
# Nested policies import as datasource_id/policy_id.
terraform import accessflow_row_security_policy.tenant_isolation \
  11111111-2222-3333-4444-555555555555/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee

Running it from CI, without Terraform

If you only need one or two operations in a pipeline, the ready-made CI wrappers are lighter than standing up Terraform state. Both authenticate with the same service-account key.

.gitlab-ci.yml
include:
  - remote: "https://raw.githubusercontent.com/bablsoft/accessflow/main/ci-templates/gitlab/accessflow.gitlab-ci.yml"

provision_prod_db:
  stage: provision
  extends: .accessflow_provision_datasource
  variables:
    ACCESSFLOW_ENDPOINT: "https://accessflow.example.com"
    AF_NAME: "prod-postgres"
    AF_DB_TYPE: "POSTGRESQL"
    AF_HOST: "postgres.prod.internal"
    AF_PORT: "5432"
    AF_DATABASE_NAME: "app"
    AF_USERNAME: "af_reader"
    AF_PASSWORD: "$PROD_DB_PASSWORD"
    AF_SSL_MODE: "REQUIRE"

Note the two names that are not AF_-prefixed: ACCESSFLOW_ENDPOINT and ACCESSFLOW_API_KEY. Set the key as a masked, protected variable rather than inline. The equivalent GitHub Actions are provision-datasource and run-query; a run-query job submits the SQL, waits for the decision, and executes it once approved.

Full argument reference for every resource, the CI action inputs and the release process live in Infrastructure as Code.