thermograph/terraform/modules/gcp-host/main.tf
Emi Griffith 0e3196fa05 Decouple Terraform from the app repo; add a GCP host scaffold
Content-change pass following the extraction from the app monorepo (this repo
now stands alone, sourced via git filter-repo to preserve history):

- terraform/variables.tf, secrets.tf, modules/thermograph-host: remove every
  app-secret Terraform variable (postgres_password, auth_secret, VAPID keys,
  registry_token, Discord/SMTP creds, ...) and the random_password/random_id
  generators. The SOPS+age vault (deploy/secrets/*.yaml) is now the sole
  source of app secrets, rendered at deploy time by deploy/render-secrets.sh;
  Terraform renders only a non-secret /etc/thermograph-topology.env (sizing,
  routing) via the renamed thermograph-topology.env.tftpl template.
- hosts gains a required app_image_tag field: the host's own checkout is now
  this infra repo, not the app repo, so there is no "current commit" to
  derive an image tag from — every host pins one explicitly. repo_url now
  points at this repo (private; typically needs an embedded read token).
- deploy.sh: IMAGE_TAG is now required from the environment instead of
  derived via `git rev-parse HEAD` of the (now infra-repo) checkout, which
  would have silently resolved to the wrong or a nonexistent tag.
- New terraform/modules/gcp-host: creates a GCE VM + minimal VPC/firewall
  only, then feeds its IP into the same thermograph-host module every
  SSH-managed host already uses — one provisioning path regardless of how a
  host came to exist. var.gcp_hosts defaults to {}, so no google_* resource
  is planned and the provider is never invoked without it (verified: plan
  and validate succeed with no GCP credentials configured).
- terraform/README.md, ACCESS.md (renamed from INFRA.md), README.md: updated
  for the new secrets model, the GCP scaffold, and this repo's own identity.

Verified: terraform fmt/validate/init clean; plan succeeds against realistic
dummy hosts (prod+beta shape) and against a populated gcp_hosts entry (plans
6 resources with no live credentials, confirming the composition wires
correctly end to end).
2026-07-21 21:46:05 -07:00

77 lines
2.9 KiB
HCL

# Creates a GCE VM (+ the minimal networking it needs) for the thermograph-host
# module to then provision over SSH — this module contributes NOTHING to
# provisioning logic (no Docker install, no compose, no secrets rendering); it
# only stands the box up and hands its IP back. See ../../main.tf's module.gcp_vm
# / module.host composition. Scaffold only: nothing here is applied until a
# caller populates var.gcp_hosts (root variables.tf) with a real entry.
# A dedicated VPC rather than the project's "default" network, so this doesn't
# depend on (or clutter) whatever else may already exist in the project.
resource "google_compute_network" "this" {
project = var.project
name = "thermograph-${var.name}"
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "this" {
project = var.project
name = "thermograph-${var.name}"
network = google_compute_network.this.id
region = join("-", slice(split("-", var.zone), 0, 2))
ip_cidr_range = "10.20.0.0/24"
}
# 22/80/443 only -- matches the SSH-managed hosts' own ufw rules (thermograph-host
# module's setup step). The app port itself is opened by that same module's ufw
# step when the host has no domain, same as any other host it provisions.
resource "google_compute_firewall" "allow_ingress" {
project = var.project
name = "thermograph-${var.name}-allow-ingress"
network = google_compute_network.this.id
source_ranges = ["0.0.0.0/0"]
allow {
protocol = "tcp"
ports = ["22", "80", "443"]
}
}
resource "google_compute_instance" "this" {
project = var.project
name = "thermograph-${var.name}"
zone = var.zone
machine_type = var.machine_type
boot_disk {
initialize_params {
# Debian: matches the apt-based setup script thermograph-host's
# provisioner runs (Docker's get.docker.com installer, ufw, Caddy's apt repo).
image = "debian-cloud/debian-12"
size = 30
}
}
network_interface {
subnetwork = google_compute_subnetwork.this.id
access_config {} # ephemeral public IP
}
metadata = {
ssh-keys = "${var.ssh_user}:${file(pathexpand(var.ssh_public_key_path))}"
}
}
# A brief pause before the caller's thermograph-host module opens an SSH
# connection: a fresh GCE instance's sshd is not always immediately reachable
# the instant the API reports the instance RUNNING (cloud-init/sshd startup is a
# known race here, distinct from the null_resource provisioner's own timeout).
resource "time_sleep" "wait_for_ssh" {
create_duration = "30s"
depends_on = [google_compute_instance.this]
}
output "external_ip" {
description = "Public IP of the created instance, once it (and a short settle delay) exist."
value = google_compute_instance.this.network_interface[0].access_config[0].nat_ip
depends_on = [time_sleep.wait_for_ssh]
}