Documentation

Infrastructure as Code.

Last updated

Infrastructure as Code (Terraform / OpenTofu & CI Actions)

Beyond the env-driven GitOps bootstrap, AccessFlow ships an official Terraform / OpenTofu provider (bablsoft/accessflow) and reusable GitHub Actions + a GitLab CI template for managing governance resources declaratively over the REST API. Both authenticate with an API key — the provider manages datasources, review plans, routing / row-security / masking policies, AI configs, and notification channels with the same authoritative-upsert semantics as the bootstrap reconciler.

Can AccessFlow be managed with Terraform?

Yes. The official bablsoft/accessflow provider works with both Terraform and OpenTofu, and manages datasources, review plans, routing, row-security and masking policies, AI configs, and notification channels. It authenticates with a service-account API key and upserts declaratively, the same way the GitOps bootstrap reconciler does.

Service-account API keys

A pipeline needs credentials without an interactive login. Bootstrap can seed a service account — an API-key-only user (password login disabled) whose raw key you supply from a Secret (only its hash is stored, rotated in place when it changes). Set these operator env vars (or the equivalent Helm bootstrap.serviceAccounts[] with an apiKeySecretRef):

  • ACCESSFLOW_BOOTSTRAP_SERVICE_ACCOUNTS_0_EMAIL
  • ACCESSFLOW_BOOTSTRAP_SERVICE_ACCOUNTS_0_DISPLAY_NAME
  • ACCESSFLOW_BOOTSTRAP_SERVICE_ACCOUNTS_0_ROLE — default ADMIN
  • ACCESSFLOW_BOOTSTRAP_SERVICE_ACCOUNTS_0_API_KEY_NAME
  • ACCESSFLOW_BOOTSTRAP_SERVICE_ACCOUNTS_0_API_KEY — the raw af_-prefixed token
  • ACCESSFLOW_BOOTSTRAP_SERVICE_ACCOUNTS_0_API_KEY_EXPIRES_AT — optional ISO-8601

You can also mint a key interactively at POST /api/v1/me/api-keys.

Terraform / OpenTofu provider

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

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

resource "accessflow_datasource" "prod" {
  name     = "prod-postgres"
  db_type  = "POSTGRESQL"
  host     = "postgres.prod.internal"
  port     = 5432
  ssl_mode = "REQUIRE"
}

Works with both tofu and terraform, and is published at registry.terraform.io/providers/bablsoft/accessflow (and the OpenTofu registry as bablsoft/accessflow). Write-only secrets (password, api_key, notification config values) are never returned by the API, so the provider applies changes to them but can't detect drift — treat the HCL as the source of truth.

Resources & data sources

The provider drives the existing REST endpoints — /datasources, /review-plans, /admin/routing-policies, /admin/ai-configs, /admin/notification-channels, and the nested /datasources/{id}/{row-security,masking}-policies. No AccessFlow-specific endpoints were added for it. Idempotency comes from Terraform state (create → store UUID → read / update / delete by id), matching the bootstrap reconciler's authoritative-upsert intent.

ResourceNotes
accessflow_datasourceCreate / read / update / delete, plus import by UUID
accessflow_review_planNested approvers and notify_channels
accessflow_routing_policycondition is the typed tree as a JSON string
accessflow_row_security_policyNested under a datasource; import as datasource_id/policy_id
accessflow_masking_policyNested under a datasource; import as datasource_id/policy_id
accessflow_ai_configapi_key is write-only
accessflow_notification_channelconfig map; channel_type is immutable and forces replacement

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

Local development

From terraform-provider/: make build compiles it, make test runs the unit tests with no live stack, make testacc runs acceptance tests against a real backend (needs ACCESSFLOW_ENDPOINT and ACCESSFLOW_API_KEY), and make docs regenerates the provider docs via tfplugindocs. To try an unreleased build, point a CLI dev override for bablsoft/accessflow at the go install-ed binary.

CI Actions

The provision-datasource and run-query GitHub composite actions (referenced as bablsoft/accessflow/.github/actions/<name>@v1) and the include-able GitLab template wrap provisioning a datasource and submitting a governed query from a pipeline. run-query waits for a terminal status and sends an X-AccessFlow-CI header so context-aware routing policies recognise the CI origin.

.github/workflows/migrate.yml
- id: ds
  uses: bablsoft/accessflow/.github/actions/provision-datasource@v1
  with:
    endpoint: https://accessflow.example.com
    api-key: ${{ secrets.ACCESSFLOW_API_KEY }}
    name: prod-postgres
    db-type: POSTGRESQL
    host: postgres.prod.internal
    port: "5432"
    database-name: app
    username: af_reader
    password: ${{ secrets.PROD_DB_PASSWORD }}
    ssl-mode: REQUIRE

- uses: bablsoft/accessflow/.github/actions/run-query@v1
  with:
    endpoint: https://accessflow.example.com
    api-key: ${{ secrets.ACCESSFLOW_API_KEY }}
    datasource-id: ${{ steps.ds.outputs.id }}
    sql: "SELECT count(*) FROM orders"

provision-datasource is idempotent — it looks the datasource up by name, then creates or updates it — so re-running a pipeline is safe. run-query fails the step on any terminal status other than EXECUTED, so a rejected or timed-out query breaks the build rather than passing silently. For unattended execution, pair it with an AUTO_APPROVE routing policy scoped to the CI origin.

On GitLab, include: the ci-templates/gitlab/accessflow.gitlab-ci.yml template and then extends: .accessflow_provision_datasource or .accessflow_run_query, passing the same values as AF_* variables.

Deployment gate in CI

The deployment-governance wrappers let a pipeline ask AccessFlow for permission to release. Every provider follows the same four beats — submit a deployment request (idempotent on the CI run id, so a retried step never opens a second review), poll the fail-closed gate until it answers releasable, confirm execution, then report the outcome. All of them fail the job on a rejection, a timeout, or a gate that returns 404 — an unknown or invisible release is treated as not releasable, never as approved.

.github/workflows/deploy.yml
- id: gate
  uses: bablsoft/accessflow/.github/actions/deployment-gate@v1
  with:
    accessflow-url: https://accessflow.example.com
    api-key: ${{ secrets.ACCESSFLOW_API_KEY }}
    pipeline-id: 1f0c9d02-1c2a-4a19-9f0e-6b2f8f1a4c77
    environment: production
    version: ${{ github.ref_name }}
    commit-sha: ${{ github.sha }}
    wait-timeout: 30m

- run: ./deploy.sh

- uses: bablsoft/accessflow/.github/actions/deployment-outcome@v1
  if: always()
  with:
    accessflow-url: https://accessflow.example.com
    api-key: ${{ secrets.ACCESSFLOW_API_KEY }}
    request-id: ${{ steps.gate.outputs.request-id }}
    job-status: ${{ job.status }}

A composite action has no post-run hook, so the outcome step is separate and runs under if: always()job.status maps success to SUCCEEDED and failure to FAILED. Pass an explicit outcome: ROLLED_BACK when you roll back; on a review-required environment that opens a follow-up review the submitter can never close. The pipeline is identified by UUID, not by name: a trigger-only API key cannot list pipelines to resolve one.

On GitLab, include: the ci-templates/gitlab/accessflow-deployment.gitlab-ci.yml template and extends: .accessflow_deployment_gate / .accessflow_deployment_outcome. On Azure Pipelines, use the step template ci-templates/azure/accessflow-deployment.yml, which takes your own deploySteps list and wraps it. Any other CI system can follow the plain-curl sequence in ci-templates/examples/generic-curl-deployment.md.

The API key a pipeline uses is an ordinary AccessFlow key belonging to a dedicated service-account user — mint it as described under service-account API keys above, grant that user can_trigger on the pipeline (see deployment pipelines), and store the raw key as a CI secret. Because the key's owning user is the submitter, that account can never approve its own deployments. Triggering is governed by the grant, not by the DEPLOYMENT_PIPELINE_MANAGE permission, so a CI key needs no administrative rights.

Full provider reference, all resources, and the registry-publishing runbook (the provider is released to a dedicated terraform-provider-accessflow repo that opentofu.org and registry.terraform.io ingest) are in docs/16-iac.md.