thermograph/infra/terraform/modules/gcp-host/main.tf

78 lines
2.9 KiB
Terraform
Raw Permalink Normal View History

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-22 04:46:05 +00:00
# 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]
}