diff --git a/terraform/README.md b/terraform/README.md index ba48f0c..77a4df5 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -17,14 +17,16 @@ This config manages **two VPS hosts** today: | key | role | VPS | branch | domain | notes | |--------|--------|-------------------------|-----------|-------------------|------------------------------------| -| `prod` | prod | NEW 48 GB / 12-core box (`169.58.46.181`) | `release` (of the APP repo — see `app_image_tag`) | `thermograph.org` | Caddy TLS; sized up (8/8/4/16g) | +| `prod` | prod | NEW 48 GB / 12-core box (`169.58.46.181`) | `release` (of the APP repos — see `backend_image_tag` / `frontend_image_tag`) | `thermograph.org` | Caddy TLS; sized up (8/8/4/16g) | | `beta` | beta | old box `75.119.132.91` | `main` (of the APP repo) | `beta.thermograph.org` | Caddy TLS; also hosts Forgejo | Each host's own checkout on disk (`app_dir`, `git_branch`) is **this infra repo**, not the app repo — the "branch" column above is which app-repo tag a -host is meant to track conceptually; the actual pinned version is -`var.hosts[*].app_image_tag` (e.g. `"sha-<12 hex>"`), since the host has no app -checkout to derive a tag from. The LAN dev server is **out of scope here** — it +host is meant to track conceptually; the actual pinned versions are +`var.hosts[*].backend_image_tag` / `frontend_image_tag` (e.g. `"sha-<12 hex>"` each — +the app is two separately-published images, `emi/thermograph-backend/app` and +`emi/thermograph-frontend/app`), since the host has no app checkout to derive a tag +from. The LAN dev server is **out of scope here** — it builds from source via the app repo's `deploy/deploy-dev.sh` (a self-hosted Forgejo Actions runner), not Terraform. @@ -41,7 +43,8 @@ Per host, over SSH provisioners, Terraform: `/etc/thermograph.env` from the SOPS+age vault — **Terraform itself never sees or carries an app secret** (see "Secrets" below); - for a host **with** a domain, installs a rendered Caddyfile and reloads Caddy; -- brings the stack up on the explicit `app_image_tag`: `docker login` to the registry, +- brings the stack up on the explicit per-service tags (exports `BACKEND_IMAGE_TAG` / + `FRONTEND_IMAGE_TAG` for the compose file): `docker login` to the registry, `docker compose <-f each compose file> pull backend frontend`, then `... up -d --remove-orphans`, running docker as root with both env files sourced; - health-checks `http://127.0.0.1:8137/`. @@ -134,7 +137,8 @@ pinned across machines. `terraform apply` runs `remote-exec` **on the server**: it resets the checkout to the branch, renders topology config + secrets, and runs `docker compose pull && up -d` -(pulling the pinned `app_image_tag` and recreating containers — a brief app restart). +(pulling the pinned `backend_image_tag` / `frontend_image_tag` and recreating +containers — a brief app restart). Against the live production host this is a real deploy. Review the plan, apply in a maintenance window, and prefer `-target` to touch one host at a time. diff --git a/terraform/main.tf b/terraform/main.tf index b8c8f98..b5a2171 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -92,7 +92,8 @@ module "host" { ssh_private_key_path = each.value.ssh_private_key_path role = each.value.role git_branch = each.value.git_branch - app_image_tag = each.value.app_image_tag + 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 diff --git a/terraform/modules/thermograph-host/main.tf b/terraform/modules/thermograph-host/main.tf index 7eaae42..6d616df 100644 --- a/terraform/modules/thermograph-host/main.tf +++ b/terraform/modules/thermograph-host/main.tf @@ -68,14 +68,15 @@ resource "null_resource" "host" { # Re-provision when the rendered topology env, the compose files, the branch, # sizing, the app image tag, or the Caddyfile change. triggers = { - topology_env_sha = sha256(local.topology_env_content) - compose_sha = local.compose_files_sha - compose_flags = local.compose_flags - caddy_sha = sha256(local.caddy_content) - branch = var.git_branch - app_image_tag = var.app_image_tag - app_dir = var.app_dir - sizing = "${var.workers}/${var.app_cpus}/${var.db_cpus}/${var.db_memory}/${var.timescaledb_tag}" + topology_env_sha = sha256(local.topology_env_content) + compose_sha = local.compose_files_sha + compose_flags = local.compose_flags + caddy_sha = sha256(local.caddy_content) + branch = var.git_branch + backend_image_tag = var.backend_image_tag + frontend_image_tag = var.frontend_image_tag + app_dir = var.app_dir + sizing = "${var.workers}/${var.app_cpus}/${var.db_cpus}/${var.db_memory}/${var.timescaledb_tag}" # Re-provision when the archive self-hosting config changes. The rclone.conf is # hashed (nonsensitive on a one-way digest) so a credential rotation redeploys. openmeteo = "${var.openmeteo}/${var.om_data_dir}/${var.om_bucket_remote}/${var.om_vfs_cache_max}" @@ -235,10 +236,13 @@ resource "null_resource" "host" { # 3. Install /etc/thermograph-topology.env, render /etc/thermograph.env from the # SOPS+age vault (deploy/render-secrets.sh, now part of this same checkout), - # then bring the stack up on the EXPLICIT app image tag — there's no app - # checkout on this host to derive a tag from (see var.app_image_tag). docker - # runs as root (sources both env files in the same shell) so it never depends - # on the docker group membership taking effect in this session. + # then bring the stack up on the EXPLICIT per-service app image tags — there's no + # app checkout on this host to derive a tag from (see var.backend_image_tag / + # frontend_image_tag). The compose file reads BACKEND_IMAGE_TAG / FRONTEND_IMAGE_TAG + # (for emi/thermograph-backend/app and emi/thermograph-frontend/app), so we export + # those, not the old single IMAGE_PATH/IMAGE_TAG. docker runs as root (sources both + # env files in the same shell) so it never depends on the docker group membership + # taking effect in this session. provisioner "remote-exec" { inline = [ <<-EOT @@ -252,7 +256,7 @@ resource "null_resource" "host" { . deploy/render-secrets.sh render_thermograph_secrets ${var.app_dir} set -a; . /etc/thermograph-topology.env; . /etc/thermograph.env; set +a - export REGISTRY_HOST="git.thermograph.org" IMAGE_PATH="emi/thermograph/app" IMAGE_TAG="${var.app_image_tag}" + export REGISTRY_HOST="git.thermograph.org" BACKEND_IMAGE_TAG="${var.backend_image_tag}" FRONTEND_IMAGE_TAG="${var.frontend_image_tag}" echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" --username emi --password-stdin docker compose ${local.compose_flags} pull backend frontend docker compose ${local.compose_flags} up -d --remove-orphans diff --git a/terraform/modules/thermograph-host/variables.tf b/terraform/modules/thermograph-host/variables.tf index 86742cc..c41f85b 100644 --- a/terraform/modules/thermograph-host/variables.tf +++ b/terraform/modules/thermograph-host/variables.tf @@ -26,12 +26,17 @@ variable "role" { } variable "git_branch" { - description = "This INFRA repo's branch the host checkout is reset to (independent of which app image is deployed — see app_image_tag)." + description = "This INFRA repo's branch the host checkout is reset to (independent of which app images are deployed — see backend_image_tag / frontend_image_tag)." type = string } -variable "app_image_tag" { - description = "App image tag to pull, e.g. \"sha-<12 hex>\" (build-push.yml's tag for the app-repo commit) or a semver tag. The host has no app-repo checkout to derive this from, so it's always explicit." +variable "backend_image_tag" { + description = "Backend image tag to pull (emi/thermograph-backend/app), e.g. \"sha-<12 hex>\" (build-push.yml's tag for the backend-repo commit) or a semver tag. The host has no app-repo checkout to derive this from, so it's always explicit." + type = string +} + +variable "frontend_image_tag" { + description = "Frontend image tag to pull (emi/thermograph-frontend/app), e.g. \"sha-<12 hex>\" (build-push.yml's tag for the frontend-repo commit) or a semver tag. Always explicit, same as backend_image_tag." type = string } diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example index cdf3673..a2ea7ed 100644 --- a/terraform/terraform.tfvars.example +++ b/terraform/terraform.tfvars.example @@ -20,8 +20,9 @@ hosts = { ssh_user = "agent" ssh_private_key_path = "~/.ssh/thermograph_agent_ed25519" role = "prod" - git_branch = "main" # this INFRA repo's branch -- see app_image_tag for the app version - app_image_tag = "REPLACE_WITH_APP_IMAGE_TAG" # e.g. "sha-abcdef012345" -- from build-push.yml on the app repo + git_branch = "main" # this INFRA repo's branch -- see *_image_tag for the app versions + backend_image_tag = "REPLACE_WITH_BACKEND_IMAGE_TAG" # e.g. "sha-abcdef012345" -- from thermograph-backend build-push.yml + frontend_image_tag = "REPLACE_WITH_FRONTEND_IMAGE_TAG" # e.g. "sha-012345abcdef" -- from thermograph-frontend build-push.yml domain = "thermograph.org" # Caddy TLS in front, app on loopback compose_files = ["docker-compose.yml"] app_dir = "/opt/thermograph" @@ -43,7 +44,8 @@ hosts = { ssh_private_key_path = "~/.ssh/thermograph_agent_ed25519" role = "beta" git_branch = "main" - app_image_tag = "REPLACE_WITH_APP_IMAGE_TAG" + backend_image_tag = "REPLACE_WITH_BACKEND_IMAGE_TAG" + frontend_image_tag = "REPLACE_WITH_FRONTEND_IMAGE_TAG" # No public domain by default: no Caddy/TLS, firewall opens the app port. NOTE: # with compose_files = ["docker-compose.yml"] the app binds 127.0.0.1 only, so # until you either set a domain (e.g. "beta.thermograph.org", which fronts it with @@ -68,7 +70,8 @@ hosts = { # ssh_private_key_path = "~/.ssh/thermograph_agent_ed25519" # role = "uat" # git_branch = "main" - # app_image_tag = "REPLACE_WITH_APP_IMAGE_TAG" + # backend_image_tag = "REPLACE_WITH_BACKEND_IMAGE_TAG" + # frontend_image_tag = "REPLACE_WITH_FRONTEND_IMAGE_TAG" # domain = "" # compose_files = ["docker-compose.yml"] # app_dir = "/opt/thermograph" @@ -89,7 +92,8 @@ hosts = { # ssh_private_key_path = "~/.ssh/thermograph_agent_ed25519" # role = "uat" # git_branch = "main" -# app_image_tag = "REPLACE_WITH_APP_IMAGE_TAG" +# backend_image_tag = "REPLACE_WITH_BACKEND_IMAGE_TAG" +# frontend_image_tag = "REPLACE_WITH_FRONTEND_IMAGE_TAG" # size = "nano" # } # } diff --git a/terraform/variables.tf b/terraform/variables.tf index 6959eb3..8386d75 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -24,14 +24,16 @@ variable "hosts" { ssh_private_key_path = string # path to the private key for that user role = string # "prod" | "beta" (informational + outputs) git_branch = string # this INFRA repo's branch the checkout is reset to - # Which app image to run, e.g. "sha-<12 hex>" (matches build-push.yml's tag for - # the app-repo commit build-push.yml built) or a semver tag on a release push. - # Required, no default: since the host's checkout is this infra repo, not the app - # repo, there is no "current commit" to derive a tag from — it must be explicit. - # Bump this (via a normal tfvars edit + apply) whenever the app repo ships a - # commit you want this host running; the infra repo's own git_branch is - # independent of that and rarely needs to change. - app_image_tag = string + # Which app images to run, e.g. "sha-<12 hex>" (each matching build-push.yml's tag + # for the commit that app repo built) or a semver tag on a release push. The app is + # TWO separately-published images now — emi/thermograph-backend/app and + # emi/thermograph-frontend/app — pinned independently. Required, no default: the + # host's checkout is this infra repo, not the app repos, so there's no "current + # commit" to derive a tag from; both must be explicit. Bump these (via a normal + # tfvars edit + apply) whenever an app repo ships a commit you want this host + # running; the infra repo's own git_branch is independent and rarely needs to change. + backend_image_tag = string + frontend_image_tag = string domain = optional(string, "") # public domain; "" => no Caddy/TLS compose_files = optional(list(string), ["docker-compose.yml"]) app_dir = optional(string, "/opt/thermograph") # checkout path on the host @@ -73,7 +75,8 @@ variable "gcp_hosts" { ssh_private_key_path = string # path to the matching PRIVATE key (for the module's provisioner) role = string git_branch = string - app_image_tag = string + backend_image_tag = string + frontend_image_tag = string domain = optional(string, "") compose_files = optional(list(string), ["docker-compose.yml"]) app_dir = optional(string, "/opt/thermograph") @@ -84,7 +87,7 @@ variable "gcp_hosts" { } variable "repo_url" { - description = "Git remote to clone from when a host has no checkout yet. Points at THIS repo (thermograph-infra) now, not the app repo -- the app's own source is never checked out on a host; only its published registry images are pulled (see var.hosts[*].app_image_tag). thermograph-infra is a private repo, so this typically needs embedded read credentials, e.g. a Forgejo deploy token: \"https://:@git.thermograph.org/emi/thermograph-infra.git\"." + description = "Git remote to clone from when a host has no checkout yet. Points at THIS repo (thermograph-infra) now, not the app repo -- the app's own source is never checked out on a host; only its published registry images are pulled (see var.hosts[*].backend_image_tag / frontend_image_tag). thermograph-infra is a private repo, so this typically needs embedded read credentials, e.g. a Forgejo deploy token: \"https://:@git.thermograph.org/emi/thermograph-infra.git\"." type = string default = "https://git.thermograph.org/emi/thermograph-infra.git" sensitive = true