Skip to content

Terraform

All DigitalOcean infrastructure lives in infra/terraform/. Workloads do not — those are Kubernetes manifests applied separately.

Files

FileContains
provider.tfProvider versions, DigitalOcean/Kubernetes/Helm providers, backend config
variables.tfEvery input variable with defaults
main.tfVPC, DOKS, database, Spaces, CDN, registry, firewalls, K8s secrets, project
ingress.tfnginx-ingress Helm release
cert-manager.tfcert-manager Helm release and ClusterIssuers
dns.tfDomain and DNS records
livekit.tfLiveKit droplet and firewall
outputs.tfConnection details and helper commands
MakefileConvenience targets
validate-setup.shPre-flight checks

Getting started

bash
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 cluster

make help lists every target. make all chains init → validate → plan → apply → kubeconfig.

Key variables

VariableDefaultNotes
project_nameplaypalzPrefixes resource names
environmentproduction
regionnyc3
kubernetes_version1.34.5-do.6Pinned
node_pool_sizes-4vcpu-8gb
node_pool_count3
node_pool_min_nodes / max_nodes2 / 10Autoscaling bounds
enable_auto_upgradetruePatch upgrades during the maintenance window
enable_surge_upgradetrueAdds a node before draining one
db_engine / db_versionpg / 15
db_sizedb-s-2vcpu-4gb
db_node_count1Raise to 2+ for HA
spaces_bucket_nameplaypalz-media
enable_spaces_cdntrue
vpc_ip_range10.10.0.0/16
domain_nameplaypalz.gg
manage_dnsfalseWhether Terraform owns DNS records
enable_wildcard_dnsfalse
livekit_droplet_sizes-4vcpu-8gb

What it provisions

Networking and compute

  • VPC 10.10.0.0/16 isolating 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:

SecretContents
database-credentialsDATABASE_URL, host, port, user, password, database name
spaces-credentialsAccess key, secret, region, endpoint, URL, bucket

Everything else is a sealed secret — see Secrets Management.

Useful Makefile targets

bash
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 versions

Avoid 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 apply tries 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-state is the only safety net, and it is manual.

Migrate to a remote backend. DigitalOcean Spaces works as an S3-compatible backend:

hcl
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

  1. Edit the .tf file.
  2. make plan and read it — confirm nothing is being replaced that you did not intend.
  3. make apply.
  4. 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

bash
make destroy

Irreversible: 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.

Internal documentation — PlayPalz platform