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.