Skip to main content

Overview

The Formal Terraform Provider enables you to manage Connectors, Resources, Policies, Users, and all other Formal objects as infrastructure-as-code.

Installation

Add the provider to your Terraform configuration:
terraform {
  required_providers {
    formal = {
      source  = "formalco/formal"
      version = "~> 4.12.8"
    }
  }
}

provider "formal" {
  api_key = var.formal_api_key
}

Authentication

Create an API key in the Formal console:
  1. Navigate to API Keys
  2. Click Create API Key
  3. Name the key (e.g., “terraform-production”)
  4. Copy the key immediately
Set the key as an environment variable or Terraform variable:
export FORMAL_API_KEY="your-api-key-here"
Or in Terraform:
variable "formal_api_key" {
  description = "Formal API key"
  type        = string
  sensitive   = true
}

Examples

Full Production Stack

terraform {
  required_providers {
    formal = {
      source  = "formalco/formal"
      version = "~> 4.12.7"
    }
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.17.0"
    }
  }
}

# Variables
variable "name" {
  description = "Base name for resources"
  type        = string
  default     = "formal-production"
}

variable "datadog_account_id" {
  description = "Datadog account ID"
  type        = string
  default     = "datadog-account-id"
}

variable "datadog_api_key" {
  description = "Datadog API key"
  type        = string
  sensitive   = true
  default     = "datadog-api-key"
}

# Configure the Formal Provider
provider "formal" {
  api_key = "ENPxVb62qlm04BRU8Hkd7a5e31QzOW9X"
  retrieve_sensitive_values = false
}

# Configure AWS Provider
provider "aws" {
  region = "us-east-1"
}
# Create a Space
resource "formal_space" "production" {
  name = "production"
}

# Create a Resource
resource "formal_resource" "postgres" {
  name       = "production-postgres"
  technology = "postgres"
  hostname   = "prod-db.us-east-1.rds.amazonaws.com"
  port       = 5432
  space_id   = formal_space.production.id

  termination_protection = false
}

# Create a Connector
resource "formal_connector" "production" {
  name     = "production-connector"
  space_id = formal_space.production.id
}

# Create Connector Listener
resource "formal_connector_listener" "production_postgres" {
  connector_id = formal_connector.production.id
  name         = "production-postgres-listener"
  port         = 5432
}

# Link Connector Listener to Resource
resource "formal_connector_listener_link" "production_postgres" {
  connector_id          = formal_connector.production.id
  connector_listener_id = formal_connector_listener.production_postgres.id
}

# Add Listener Rule for Resource
resource "formal_connector_listener_rule" "postgres" {
  connector_listener_id = formal_connector_listener.production_postgres.id
  type                  = "resource"
  rule                  = formal_resource.postgres.id
}

# Create Users
resource "formal_user" "alice" {
  first_name = "Alice"
  last_name  = "Smith"
  email      = "alice@example.com"
  type       = "human"
}

resource "formal_user" "looker_app" {
  name = "Looker Application"
  type = "machine"
}

# Create Groups
resource "formal_group" "engineering" {
  name        = "Engineering Team"
  description = "Engineers with database access"
}

resource "formal_group_user_link" "alice_eng" {
  group_id = formal_group.engineering.id
  user_id  = formal_user.alice.id
}

# Create a Policy
resource "formal_policy" "mask_pii" {
  name         = "mask-pii-data"
  description  = "Mask PII fields for non-privileged users"
  status       = "active"
  owner        = "admin@example.com"
  notification = "none"

  module = <<-EOT
    package formal.v2

    import future.keywords.if
    import future.keywords.in

    post_request := {
      "action": "mask",
      "type": "nullify",
      "columns": pii_columns
    } if {
      not "pii_access" in input.user.groups

      pii_columns := [col |
        col := input.row[_]
        col["data_label"] in ["email", "ssn", "phone"]
      ]

      count(pii_columns) > 0
    }
  EOT
}

# AWS S3 Bucket for Logs
resource "aws_s3_bucket" "logs" {
  bucket        = "${var.name}-logs"
  force_destroy = true
}

# AWS Integration
resource "formal_integration_cloud" "aws" {
  name         = "aws-production"
  cloud_region = "us-east-1"

  aws {
    template_version          = "latest"
    enable_rds_autodiscovery  = true
    enable_eks_autodiscovery  = true
    allow_s3_access           = true
    s3_bucket_arn             = "${aws_s3_bucket.logs.arn}/*"
  }
}

# Log Integration
resource "formal_integration_log" "datadog" {
  name = "datadog-logs"

  datadog {
    account_id = var.datadog_account_id
    api_key    = var.datadog_api_key
    site       = "datadoghq.com"
  }
}

Resource Documentation

Full documentation for all resources: Core Objects: Integrations:

Example Repositories

Formal provides complete Terraform examples: Clone and customize for your needs:
git clone https://github.com/formalco/terraform-provider-formal.git
cd terraform-provider-formal/examples/deployments/aws/connector

Best Practices

Pin provider versions to avoid unexpected changes:
formal = {
  source  = "formalco/formal"
  version = "~> 4.12.8"  # Allow patches, not minor versions
}
Use remote state backends (S3, Terraform Cloud) for team collaboration:
terraform {
  backend "s3" {
    bucket = "my-terraform-state"
    key    = "formal/production/terraform.tfstate"
    region = "us-east-1"
  }
}
Parameterize configurations for reusability:
variable "environment" {
  type = string
}

resource "formal_connector" "main" {
  name = "${var.environment}-connector"
}
Use workspaces or separate state files for prod/staging/dev:
terraform workspace new production
terraform workspace select production
Protect production resources:
resource "formal_resource" "prod_db" {
  name                   = "production-postgres"
  termination_protection = true
  # ...
}

Importing Existing Resources

Bring existing Formal objects under Terraform management:
# Import a Resource
terraform import formal_resource.postgres <resource-id>

# Import a Connector
terraform import formal_connector.main <connector-id>

# Import a User
terraform import formal_user.alice <user-id>
Get object IDs from the Formal console or API.

Outputs

Export useful information:
output "connector_token" {
    description = "Connector API token for deployment"
    value       = formal_connector.production.api_key
    sensitive   = true
  }

  output "connector_listener_port" {
    description = "Configured listener port"
    value       = formal_connector_listener.production_postgres.port
  }

  output "resource_ids" {
    description = "Map of resource names to IDs"
    value = {
      (formal_space.production.name)                       = formal_space.production.id
      (formal_resource.postgres.name)                      = formal_resource.postgres.id
      (formal_connector.production.name)                   = formal_connector.production.id
      (formal_connector_listener.production_postgres.name) = formal_connector_listener.production_postgres.id
      (formal_group.engineering.name)                      = formal_group.engineering.id
    }
  }

Next Steps