Skip to main content

Terraform & IaC

The official FoundryDB Terraform provider lets you declare database services, users, and firewall rules as code. Changes are tracked in state, reviewed in pull requests, and applied through standard terraform plan / terraform apply workflows.

Provider source: registry.terraform.io/foundrydb/foundrydb GitHub: github.com/foundrydb/terraform-provider-foundrydb


Provider setup

Requirements

  • Terraform 1.3 or later
  • A FoundryDB API key

Configure the provider

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
}

Store credentials in environment variables so they are never committed to source control:

export TF_VAR_foundrydb_username="admin"
export TF_VAR_foundrydb_password="your_password"

Or pass them via a .tfvars file that is listed in .gitignore:

# terraform.tfvars  (do not commit)
foundrydb_username = "admin"
foundrydb_password = "your_password"

Resources

PostgreSQL service

resource "foundrydb_service" "postgres" {
name = "prod-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_ip,
]
}

Provisioning runs asynchronously. The provider polls until status reaches running before marking the resource as created.

Database user

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

The password is generated by FoundryDB and exposed as a sensitive output (see below).

Firewall rules

Firewall rules are managed on the service resource via the allowed_cidrs attribute. To add or remove CIDRs, update the list and re-apply:

resource "foundrydb_service" "postgres" {
# ...
allowed_cidrs = [
"10.0.0.0/8", # private network
"203.0.113.42/32", # developer IP
]
}

Outputs

output "postgres_host" {
value = foundrydb_service.postgres.hostname
}

output "postgres_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
}

Retrieve sensitive outputs after apply:

terraform output -raw postgres_connection_string

State management

Import an existing service

If you have a service that was created outside of Terraform, import it by ID:

terraform import foundrydb_service.postgres <service-id>

After import, generate matching HCL from the current state:

terraform show -json | jq '.values.root_module.resources[] | select(.type=="foundrydb_service")'

Drift detection

Run terraform plan at any time to detect configuration drift. The provider reads live service attributes from the API and flags differences. Common sources of drift include manually updated firewall rules or plan changes made through the dashboard.

To enforce that only Terraform can modify a service, set resource lifecycle policies:

resource "foundrydb_service" "postgres" {
# ...

lifecycle {
prevent_destroy = true
}
}

GitHub Actions

The foundrydb-github-actions action (github.com/foundrydb/foundrydb-github-actions) wraps the provider with convenience steps for CI/CD. The example below provisions an ephemeral staging database when a pull request opens and tears it down when the PR closes.

# .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"

- name: Terraform init
run: terraform init

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

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

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"

- name: Terraform init
run: terraform init

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

The corresponding infra/preview/main.tf uses var.pr_number to give each preview environment a unique name:

variable "pr_number" {}
variable "foundrydb_username" {}
variable "foundrydb_password" {}

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"
allowed_cidrs = ["0.0.0.0/0"]
}

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

Store FOUNDRYDB_USERNAME and FOUNDRYDB_PASSWORD as encrypted secrets in your repository settings. Never pass credentials through environment variables that are logged or through terraform.tfvars files committed to the repository.


Example: PostgreSQL and Valkey full stack

A common pattern is to provision an application database alongside a Valkey instance for caching, and pass both connection strings to your application as environment variables or secrets.

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
}

# --- PostgreSQL (primary application database) ---

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

storage_size_gb = 100
storage_tier = "maxiops"

allowed_cidrs = var.allowed_cidrs
}

resource "foundrydb_database_user" "pg_app" {
service_id = foundrydb_service.pg.id
username = "app_user"
role = "admin"
}

# --- Valkey (cache layer) ---

resource "foundrydb_service" "valkey" {
name = "myapp-valkey"
database_type = "valkey"
version = "8"
plan_name = "tier-2"
zone = "se-sto1"

storage_size_gb = 20
storage_tier = "maxiops"

allowed_cidrs = var.allowed_cidrs
}

resource "foundrydb_database_user" "valkey_app" {
service_id = foundrydb_service.valkey.id
username = "cache_user"
role = "admin"
}

# --- Outputs ---

output "postgres_host" {
value = foundrydb_service.pg.hostname
}

output "postgres_url" {
description = "PostgreSQL connection string (TLS required)"
value = "postgresql://${foundrydb_database_user.pg_app.username}:${foundrydb_database_user.pg_app.password}@${foundrydb_service.pg.hostname}:5432/defaultdb?sslmode=verify-full"
sensitive = true
}

output "valkey_host" {
value = foundrydb_service.valkey.hostname
}

output "valkey_url" {
description = "Valkey connection string (TLS on port 6380)"
value = "rediss://${foundrydb_database_user.valkey_app.username}:${foundrydb_database_user.valkey_app.password}@${foundrydb_service.valkey.hostname}:6380"
sensitive = true
}

Variables file

# variables.tf
variable "foundrydb_username" {
type = string
sensitive = true
}

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

variable "allowed_cidrs" {
type = list(string)
default = []
}

Apply

terraform init
terraform plan
terraform apply

Once apply completes, retrieve the connection strings:

terraform output -raw postgres_url
terraform output -raw valkey_url

Both services are provisioned in the same UpCloud zone, so cross-service latency is minimal.