# No project/region default here on purpose: each var.gcp_hosts entry carries its # own project + zone (a fleet could span projects), and with var.gcp_hosts empty # this provider is never invoked, so there's nothing to default. When you do # populate var.gcp_hosts, authenticate via Application Default Credentials # (`gcloud auth application-default login`) or GOOGLE_APPLICATION_CREDENTIALS -- # no credentials are configured in this repo. provider "google" {} locals { # Repo root (one level above this terraform/ dir). The module hashes the compose # files here so a compose change re-triggers the remote deploy, and this is the # tree the host's checkout mirrors over git. repo_root = abspath("${path.root}/..") # Reusable size presets — a host can reference one by name (hosts..size) # instead of hand-picking workers/app_cpus/db_cpus/db_memory separately. Mirrors # the target Proxmox sizing-tier model (architecture doc §6: nano/small/medium/ # large -> {cores, mem}) ahead of actually provisioning VMs — Proxmox itself is # deferred; today these tiers just size the container caps on the existing # SSH-managed VPS hosts. "large" matches the current 48 GB prod box's numbers. sizes = { nano = { workers = 1, app_cpus = 1, db_cpus = 1, db_memory = "1g" } small = { workers = 4, app_cpus = 4, db_cpus = 2, db_memory = "8g" } medium = { workers = 6, app_cpus = 6, db_cpus = 3, db_memory = "12g" } large = { workers = 8, app_cpus = 8, db_cpus = 4, db_memory = "16g" } } } # GCP-created hosts (empty by default — see variables.tf and modules/gcp-host). # Creates only the VM + minimal networking; every provisioning behavior comes from # the SAME thermograph-host module every SSH-managed host below uses, via # local.all_hosts merging its output IP in below. No live resources exist until # var.gcp_hosts is populated. module "gcp_vm" { source = "./modules/gcp-host" for_each = var.gcp_hosts name = each.key project = each.value.project zone = each.value.zone machine_type = each.value.machine_type ssh_user = each.value.ssh_user ssh_public_key_path = each.value.ssh_public_key_path } locals { # ONE MODULE INSTANCE PER (HOST, ENVIRONMENT) PAIR, not per host. # # var.hosts is keyed by machine (vps1, vps2) and each machine carries an # `environments` map, because vps2 runs prod AND beta. The module below # provisions an ENVIRONMENT — a checkout, a branch, image tags, a Caddy site, # container sizing — so it needs one instance per environment, with the two # instances on vps2 sharing that machine's SSH identity. # # Flattened to keys like "vps2-prod" and "vps2-beta". Keeping them distinct # module instances is what makes `app_dir` (required, no default in # variables.tf) do its job: two environments on one box get two explicitly # different checkouts, so neither apply can `git reset --hard` the other's # tree. ssh_environments = merge([ for hname, h in var.hosts : { for ename, e in h.environments : "${hname}-${ename}" => merge(e, { host = h.host ssh_user = h.ssh_user ssh_private_key_path = h.ssh_private_key_path }) } ]...) # Every environment this config manages, SSH-only (flattened above) plus # GCP-created (whose `host` is filled in from the VM Terraform just created) — # one unified map so a single `module.host` for_each below handles both without # duplicating any provisioning logic. GCP hosts stay one-entry-per-machine: # nothing creates two environments on a created VM today, and the variable # keeps its flat shape. They always use a named size tier (simpler than # exposing the four raw sizing fields on that variable too). all_hosts = merge( local.ssh_environments, { for name, h in var.gcp_hosts : name => merge(h, { host = module.gcp_vm[name].external_ip workers = null app_cpus = null db_cpus = null db_memory = null openmeteo = false om_data_dir = "/mnt/om-archive" }) } ) # Per-host resolved sizing: a named tier (host.size) wins when set; otherwise the # host's own workers/app_cpus/db_cpus/db_memory fields (each individually # defaulted in variables.tf) — so existing tfvars with explicit numbers are # unaffected, and a tier is purely an opt-in shortcut. host_sizing = { for name, h in local.all_hosts : name => h.size != null ? local.sizes[h.size] : { workers = h.workers app_cpus = h.app_cpus db_cpus = h.db_cpus db_memory = h.db_memory } } } # One module instance per host (SSH-managed or GCP-created — see local.all_hosts). # The module is entirely SSH-provisioner driven: it configures an already-running # VM (however it came to exist) and hands the app off to docker compose. module "host" { source = "./modules/thermograph-host" for_each = local.all_hosts # Per-host config name = each.key host = each.value.host ssh_user = each.value.ssh_user ssh_private_key_path = each.value.ssh_private_key_path role = each.value.role git_branch = each.value.git_branch backend_image_tag = each.value.backend_image_tag frontend_image_tag = each.value.frontend_image_tag domain = each.value.domain compose_files = each.value.compose_files app_dir = each.value.app_dir workers = local.host_sizing[each.key].workers app_cpus = local.host_sizing[each.key].app_cpus db_cpus = local.host_sizing[each.key].db_cpus db_memory = local.host_sizing[each.key].db_memory timescaledb_tag = each.value.timescaledb_tag openmeteo = each.value.openmeteo om_data_dir = each.value.om_data_dir # Shared infra config repo_root = local.repo_root repo_url = var.repo_url app_port = var.app_port # Shared object-storage config (only used where openmeteo = true) om_bucket_remote = var.om_bucket_remote om_rclone_conf = var.om_rclone_conf om_vfs_cache_max = var.om_vfs_cache_max }