Skip to main content

Databases as Code: Managing FoundryDB with Terraform

· 7 min read
FoundryDB Team
Engineering @ FoundryDB

Most teams manage their application infrastructure as code but still create databases by clicking through a dashboard. The database becomes the one piece of production infrastructure that has no audit trail, no PR review, and no reproducible setup. When someone asks "why is the production database on tier-4 instead of tier-6?" the answer is usually "someone changed it last year."

The FoundryDB Terraform provider closes that gap. You declare your database services, users, and firewall rules in HCL, version them in git, and apply changes through the same CI/CD pipelines you use for everything else.

Why Infrastructure as Code for Databases

Three problems disappear when you manage databases as code.

Reproducibility. Your staging environment should match production. With Terraform, you define the configuration once and parameterize the differences (service name, plan size, allowed CIDRs). Spinning up a new environment takes one terraform apply, not a checklist of manual steps.

Audit trail. Every change to your database infrastructure goes through a pull request. Reviewers can see exactly what will change before it happens. Git blame tells you who changed the firewall rules and when. This matters for compliance, but it also matters for debugging at 2 AM.

Safety. terraform plan shows you the exact diff before anything is modified. Resizing a plan? You will see the change. Accidentally removing a CIDR block that your application depends on? The plan output catches it. This preview step prevents the class of incidents that start with "I just wanted to update one setting."

Provider Setup

The FoundryDB provider is published on the Terraform Registry. Add it to your configuration and pass credentials through environment variables so they never touch source control.

terraform {
required_providers {
foundrydb = {
source = "registry.terraform.io/foundrydb/foundrydb"
version = "~> 1.0"
}
}
}

provider "foundrydb" {
endpoint = "https://api.foundrydb.com"
username = var.foundrydb_username
password = var.foundrydb_password
}

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

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

Set credentials as environment variables:

export TF_VAR_foundrydb_username="your-username"
export TF_VAR_foundrydb_password="your-password"

Run terraform init to download the provider. You are ready to declare resources.

Declaring a PostgreSQL Service

A foundrydb_service resource represents a managed database. Here is a production PostgreSQL instance with 100 GB of NVMe storage in Stockholm:

resource "foundrydb_service" "postgres" {
name = "prod-api-pg"
database_type = "postgresql"
version = "17"
plan_name = "tier-4"
zone = "se-sto1"

storage_size_gb = 100
storage_tier = "maxiops"

allowed_cidrs = [
"10.0.0.0/8",
var.office_cidr,
]

lifecycle {
prevent_destroy = true
}
}

The prevent_destroy lifecycle rule is critical for production databases. It prevents an accidental terraform destroy from deleting the service. Terraform will error out instead.

Provisioning is asynchronous. The provider polls the FoundryDB API until the service reaches running status before marking the resource as created. A tier-4 PostgreSQL service typically provisions in under 3 minutes.

Managing Users and Firewall Rules

Database users and firewall rules are first-class Terraform resources. Changes to either go through the same plan/apply workflow.

resource "foundrydb_database_user" "app" {
service_id = foundrydb_service.postgres.id
username = "app_user"
role = "admin"
}

output "app_password" {
value = foundrydb_database_user.app.password
sensitive = true
}

output "connection_string" {
value = "postgresql://${foundrydb_database_user.app.username}:${foundrydb_database_user.app.password}@${foundrydb_service.postgres.hostname}:5432/defaultdb?sslmode=verify-full"
sensitive = true
}

FoundryDB generates the password automatically. Mark outputs as sensitive so Terraform redacts them from logs. Retrieve them after apply:

terraform output -raw connection_string

Firewall rules live on the service resource as allowed_cidrs. Adding a new IP for a developer is a one-line diff in a pull request:

allowed_cidrs = [
"10.0.0.0/8",
var.office_cidr,
"203.0.113.42/32", # Alice, added 2026-04-06
]

Your team reviews the change. terraform plan confirms the update. terraform apply pushes it. The git log records exactly when access was granted.

Previewing Changes with terraform plan

Before any modification reaches your database, terraform plan shows you the exact diff. This is the safety net that makes infrastructure as code worth the effort.

$ terraform plan

foundrydb_service.postgres: Refreshing state...

~ resource "foundrydb_service" "postgres" {
~ plan_name = "tier-4" -> "tier-6"
~ allowed_cidrs = [
"10.0.0.0/8",
+ "203.0.113.42/32",
]
}

Plan: 0 to add, 1 to change, 0 to destroy.

You see exactly what will change: the compute plan is being upgraded, and a new CIDR is being added. No changes will be applied until you run terraform apply. In a CI/CD pipeline, you can post this plan output as a PR comment for review before merging.

CI/CD: Staging Databases on Pull Request

One of the most practical patterns is provisioning an ephemeral database for each pull request. Your integration tests run against a real FoundryDB instance that matches production configuration. When the PR closes, the database is destroyed automatically.

# .github/workflows/preview-db.yml
name: Preview database

on:
pull_request:
types: [opened, synchronize, reopened, closed]

jobs:
provision:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
permissions:
pull-requests: write
defaults:
run:
working-directory: infra/preview

steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.8"

- run: terraform init
- run: terraform apply -auto-approve
env:
TF_VAR_foundrydb_username: ${{ secrets.FOUNDRYDB_USERNAME }}
TF_VAR_foundrydb_password: ${{ secrets.FOUNDRYDB_PASSWORD }}
TF_VAR_pr_number: ${{ github.event.pull_request.number }}

- name: Post connection info to PR
env:
GH_TOKEN: ${{ github.token }}
run: |
HOST=$(terraform output -raw postgres_host)
gh pr comment ${{ github.event.pull_request.number }} \
--body "Preview database ready: \`$HOST\`"

teardown:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
defaults:
run:
working-directory: infra/preview
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.8"
- run: terraform init
- run: terraform destroy -auto-approve
env:
TF_VAR_foundrydb_username: ${{ secrets.FOUNDRYDB_USERNAME }}
TF_VAR_foundrydb_password: ${{ secrets.FOUNDRYDB_PASSWORD }}
TF_VAR_pr_number: ${{ github.event.pull_request.number }}

The preview Terraform config uses var.pr_number to name each service uniquely:

resource "foundrydb_service" "preview_pg" {
name = "pr-${var.pr_number}-pg"
database_type = "postgresql"
version = "17"
plan_name = "tier-2"
zone = "se-sto1"
storage_size_gb = 50
storage_tier = "maxiops"
}

Store FOUNDRYDB_USERNAME and FOUNDRYDB_PASSWORD as encrypted secrets in your repository settings.

Importing Existing Services

If you already have FoundryDB services that were created through the dashboard or CLI, you can bring them under Terraform management without recreating them.

# Get your service ID from the dashboard or CLI
fdb services list --output json | jq '.[].id'

# Import into your Terraform state
terraform import foundrydb_service.postgres svc_x7k2m9p4

After import, write the matching HCL resource block. Run terraform plan to verify there is no drift between your code and the live service. If the plan shows zero changes, your configuration matches reality.

Best Practices

Separate state per environment. Use different Terraform workspaces or directories for production, staging, and development. This prevents an accidental apply in the wrong workspace from modifying production.

infra/
production/
main.tf
terraform.tfvars
staging/
main.tf
terraform.tfvars
preview/
main.tf

Use prevent_destroy on production databases. This is a one-line addition that prevents catastrophic deletions. Remove it only when you intentionally want to decommission a service.

Store state remotely. Use Terraform Cloud, S3, or GCS for state storage. Local state files are easy to lose and impossible to share across a team.

Pin the provider version. Use version = "~> 1.0" to allow patch updates but prevent breaking changes from major version bumps.

Never commit credentials. Use environment variables (TF_VAR_*), a secrets manager, or your CI/CD platform's encrypted secrets. Add *.tfvars and .terraform/ to .gitignore.

Getting Started

Install the FoundryDB Terraform provider and declare your first service in under five minutes. The full resource reference is in the Terraform documentation. If you prefer a different IaC approach, the same operations are available through the CLI, TypeScript SDK, and REST API.