Terraform
All DigitalOcean infrastructure lives in infra/terraform/. Workloads do not — those are Kubernetes manifests applied separately.
Files
| File | Contains |
|---|---|
provider.tf | Provider versions, DigitalOcean/Kubernetes/Helm providers, backend config |
variables.tf | Every input variable with defaults |
main.tf | VPC, DOKS, database, Spaces, CDN, registry, firewalls, K8s secrets, project |
ingress.tf | nginx-ingress Helm release |
cert-manager.tf | cert-manager Helm release and ClusterIssuers |
dns.tf | Domain and DNS records |
livekit.tf | LiveKit droplet and firewall |
outputs.tf | Connection details and helper commands |
Makefile | Convenience targets |
validate-setup.sh | Pre-flight checks |
Getting started
cd infra/terraform
cp terraform.tfvars.example terraform.tfvars
# fill in do_token, spaces_access_id, spaces_secret_key, letsencrypt_email,
# livekit_ssh_key_fingerprint
./validate-setup.sh # checks tools, auth, config, and estimates cost
make init
make plan
make apply
make kubeconfig # configure kubectl against the new clustermake help lists every target. make all chains init → validate → plan → apply → kubeconfig.
Key variables
| Variable | Default | Notes |
|---|---|---|
project_name | playpalz | Prefixes resource names |
environment | production | |
region | nyc3 | |
kubernetes_version | 1.34.5-do.6 | Pinned |
node_pool_size | s-4vcpu-8gb | |
node_pool_count | 3 | |
node_pool_min_nodes / max_nodes | 2 / 10 | Autoscaling bounds |
enable_auto_upgrade | true | Patch upgrades during the maintenance window |
enable_surge_upgrade | true | Adds a node before draining one |
db_engine / db_version | pg / 15 | |
db_size | db-s-2vcpu-4gb | |
db_node_count | 1 | Raise to 2+ for HA |
spaces_bucket_name | playpalz-media | |
enable_spaces_cdn | true | |
vpc_ip_range | 10.10.0.0/16 | |
domain_name | playpalz.gg | |
manage_dns | false | Whether Terraform owns DNS records |
enable_wildcard_dns | false | |
livekit_droplet_size | s-4vcpu-8gb |
What it provisions
Networking and compute
- VPC
10.10.0.0/16isolating all resources - DOKS cluster with an autoscaling node pool
- LiveKit droplet with its own firewall, outside the cluster because WebRTC needs a wide UDP range and host networking
Data
- Managed PostgreSQL 15, plus a dedicated database and application user
- Database firewall restricting access to the Kubernetes cluster — the database is not reachable from the internet
- Spaces bucket with a CORS configuration and an optional CDN
Redis is deliberately not here — it runs in-cluster from infra/k8s/redis-deployment.yaml.
Platform services
- nginx-ingress via Helm, with explicit resource requests and limits
- cert-manager via Helm, with resources set for the controller, webhook, and cainjector
Credentials
Terraform writes two Kubernetes secrets directly, so the manifests can reference them without anyone copying values by hand:
| Secret | Contents |
|---|---|
database-credentials | DATABASE_URL, host, port, user, password, database name |
spaces-credentials | Access key, secret, region, endpoint, URL, bucket |
Everything else is a sealed secret — see Secrets Management.
Useful Makefile targets
make plan # generate and show an execution plan
make apply # apply, with confirmation
make output # all outputs
make kubeconfig # configure kubectl
make registry-login # doctl registry login
make check-cluster # cluster status
make check-db # database status
make cost # cost estimate
make backup-state # copy the state file
make check-versions # available Kubernetes versionsAvoid make apply-auto and make quick-deploy — both skip confirmation, and the Makefile's own help text flags them as dangerous.
State management
State is local only. infra/terraform/.gitignore correctly excludes *.tfstate, *.tfvars, and .terraform/, and the remote backend in provider.tf is commented out — so terraform.tfstate exists on whichever machine last ran apply and nowhere else.
Local state is a single point of failure
- One laptop owns production. Lose that file and Terraform no longer knows what exists; the next
applytries to recreate everything. - No locking. Two people applying concurrently corrupt state.
- Secrets in plaintext. State contains database passwords, Spaces keys, and generated registry credentials. It is not in git, but it is sitting unencrypted on a developer machine.
make backup-stateis the only safety net, and it is manual.
Migrate to a remote backend. DigitalOcean Spaces works as an S3-compatible backend:
terraform {
backend "s3" {
endpoint = "https://nyc3.digitaloceanspaces.com"
bucket = "playpalz-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1" # required but ignored by Spaces
skip_credentials_validation = true
skip_metadata_api_check = true
}
}Migrating gives you locking, encryption at rest, and a state file that does not live on one person's machine.
tfplan is committed
infra/terraform/tfplan is tracked in git. A saved plan file embeds resource attribute values, including sensitive ones, so it should not be. Add tfplan to the ignore list (the pattern there only covers *.tfplan) and remove it from the repository.
Making changes
- Edit the
.tffile. make planand read it — confirm nothing is being replaced that you did not intend.make apply.- Update this page if you changed the shape of the infrastructure.
Some changes destroy and recreate
Watch the plan for -/+ destroy and then create replacement. Changing the VPC IP range, the database engine or version, or the cluster region will replace the resource. For the database that means data loss unless you restore from a snapshot.
Destroying
make destroyIrreversible: the cluster, the database and its data, and the Spaces bucket and its contents all go. Take a database snapshot and confirm you have the media backed up first. There is essentially no legitimate reason to run this against production.
