Files
rootnode-academy/docs/PHASE4_PRODUCTION_INFRASTRUCTURE.md
T
ced4568 bf1e1b5c76 docs(phase4): production infrastructure documentation + runbooks
Master overview (PHASE4_PRODUCTION_INFRASTRUCTURE.md) plus five detail docs
(PRODUCTION_DEPLOYMENT, PRODUCTION_NETWORK_SECURITY, BACKUP_DR,
OBSERVABILITY, CAPACITY_PLANNING), a master PRODUCTION_VALIDATION_CHECKLIST,
and 12 runbooks (first deployment, normal deploy, rollback, Postgres
restore, VM rebuild for both hosts, Docker TLS rotation, TLS renewal, owner
recovery, stuck Lab cleanup, failed migration, backup failure, disk
failure, Lab Host resource pressure).

PRODUCTION_NETWORK_SECURITY.md prominently documents the one genuine
architectural gap this phase surfaced: the Gateway's existing
env.internal_ip mechanism for reaching a Lab container assumes Gateway and
Lab share a Docker daemon, which breaks once Labs run on a genuinely
separate rn-lab-host-01 VM. The network-policy half is built (a firewall
allowance for Gateway->Lab-Host on a published port range); the
application half (Provisioner/Gateway address handling) is an explicit,
reviewed, NOT-YET-BUILT follow-up flagged there rather than silently
decided.

Updated LAB_ARCHITECTURE.md, ENGINEERING_SOP.md, README.md, and
platform/README.md with Phase 4 pointers, consistent with how every prior
phase updated these.

Status: PHASE 4 BUILD COMPLETE / PRODUCTION DEPLOYMENT PENDING HARDWARE.
2026-08-18 17:13:51 -05:00

185 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 4 — Production Infrastructure Package
**Status: PHASE 4 BUILD COMPLETE. PRODUCTION DEPLOYMENT PENDING HARDWARE.**
The dedicated Hetzner server this is designed for hasn't been purchased yet. Everything in this
document and the `infra/` tree it describes is buildable, testable (against local
Docker/Postgres/disposable certs), and reviewable **now** — none of it requires the real
hardware to exist. What genuinely can't happen until the hardware exists is called out
explicitly throughout, never silently assumed to already work.
This document is the map. Detail docs: `docs/PRODUCTION_DEPLOYMENT.md` (step-by-step),
`docs/PRODUCTION_NETWORK_SECURITY.md` (network/firewall/TLS design), `docs/BACKUP_DR.md`
(backup/restore/disaster recovery), `docs/OBSERVABILITY.md` (metrics/logs/alerts),
`docs/CAPACITY_PLANNING.md` (sizing model), and the `docs/RUNBOOK_*.md` files (14 topics,
command-oriented).
## 1. What was built
```
infra/
├── proxmox/ manual bare-metal install docs + cloud-init template definition
├── terraform/ VM creation (rn-platform-01, rn-lab-host-01) — real `tofu validate` pass
├── ansible/ all host configuration — real `ansible-playbook --syntax-check` pass
│ ├── roles/ common, firewall, docker-tls, platform, lab-host, monitoring-agent, backup
│ ├── group_vars/ production.yml, staging.yml, vault template
│ └── site.yml top-level playbook, correct role ordering
├── docker/ production Compose files, Caddyfile, Postgres tuning — real `docker
│ compose config` + `caddy validate` pass
├── firewall/ iptables-restore rulesets for all 3 hosts — structural lint pass
├── monitoring/ Prometheus/Alertmanager/Loki/Promtail/Grafana config — real `promtool`
│ + `amtool` validation pass
├── backup/ Postgres backup/restore/verify scripts — REAL end-to-end test against
│ disposable local Postgres, including the actual production migrations
├── scripts/ deploy/rollback/smoke-test/CI/loadtest/capacity-model tooling
├── ci/ Gitea Actions workflow (build/test/stage automatic, prod manual)
└── environments/ dev/staging/production config templates, no secrets committed
```
18 new docs (this file + 5 detail docs + 12 runbooks), 4 updated (`LAB_ARCHITECTURE.md`,
`ENGINEERING_SOP.md`, `README.md`, `platform/README.md`).
## 2. Architecture (unchanged from the design, now codified)
```
Hetzner Dedicated Server
└── Proxmox VE
├── rn-platform-01 (frontend, Platform API, Gateway, Provisioner, Postgres,
│ Prometheus/Alertmanager/Grafana/Loki, backup cron, Caddy)
└── rn-lab-host-01 (Docker Engine w/ TLS, egress proxy, disposable student Labs,
node-exporter/cadvisor/promtail)
```
The Platform VM / Lab Host VM security boundary from `docs/LAB_ARCHITECTURE.md` is the one
thing this phase was explicitly told never to weaken — it's codified, not just described, in
`infra/firewall/` and `infra/ansible/roles/docker-tls/`.
## 3. VM specifications
| | rn-platform-01 | rn-lab-host-01 |
|---|---|---|
| Baseline (default in `infra/terraform/variables.tf`) | 4 cores, 16 GB RAM, 100 GB disk | 24 cores, 96 GB RAM, 500 GB disk |
| Recommended (once real host is ~16c/32t/128GB) | same — Postgres/API/Gateway/Provisioner don't need much | majority of remaining host resources |
| Max-growth | scale RAM with Postgres `shared_buffers` if instructor/cohort count grows a lot | scale with measured concurrent-Lab capacity (`docs/CAPACITY_PLANNING.md`) |
| Configurable via | `terraform.tfvars`' `platform_vm`/`lab_host_vm` objects | same |
NUMA/CPU pinning: not configured — not useful at this scale (a handful of VMs on one physical
host, not a dozen contending for cache locality). Revisit only if real load testing shows a
reason to.
## 4. Storage decision framework
Not a final answer — `infra/environments/production/provisioner-config/README.md` and this
section exist so the final ZFS layout is a deployment-time decision made against real disks,
not guessed now. Candidate layouts (`infra/terraform/variables.tf`'s `vm_disk_datastore` names
whichever one gets chosen):
- **4× NVMe**: mirrored vdevs (RAID10-equivalent) — two mirror pairs, one for
Postgres/platform data (I/O-sensitive, small), one for Lab Host disposable data
(throughput-sensitive, large, never needs redundancy since nothing on it is precious).
- **2× SSD/NVMe**: one mirrored vdev, logical dataset separation (ZFS datasets, not separate
pools) between platform (`{{ rootnode_data_root }}/postgres`, `/backups`) and Lab Host data.
- **Mixed SSD/HDD**: only if the purchased server's actual disks require it — HDD for
disposable Lab scratch space is acceptable (it's disposable), never for Postgres.
**Never single-disk for Postgres data**, in any layout — a mirror is the floor, not a nice-to-have.
Pre-deployment disk inspection checklist (model, size, SMART, power-on hours, wear/endurance,
sector errors, temperature, firmware): `docs/RUNBOOK_DISK_FAILURE.md`'s inspection section —
run once, against the real drives, before committing to a layout.
## 5-8. Network, firewall, Docker TLS, public ingress
Full detail: `docs/PRODUCTION_NETWORK_SECURITY.md`. Summary: two private VM networks, Docker TLS
(mutual, already application-supported since Phase 1 — this phase only needed the certificate
generation/deployment automation, not new app code), a single public origin (Caddy, automatic
HTTPS) proxying only to the Gateway, and one genuinely new architectural item this phase
surfaced and addressed at the network-policy layer: **Gateway-to-Lab reachability across two
real VMs** — see that doc's dedicated section for what's built vs. what still needs a small,
reviewed Provisioner/Gateway code change before real go-live.
## 9-10. Production auth/cookie config, secrets
`infra/environments/production/*.env.template` set `COOKIE_SECURE=1`/`TRUST_PROXY=1` explicitly
(dev's unsafe defaults are never inherited). Secrets: Ansible Vault (already bundled with
Ansible — no new tool to install/trust for this project's secret surface), template at
`infra/ansible/group_vars/production.vault.yml.template`. Nothing is committed —
`infra/.gitignore` covers `*.env`, TLS private key material, vault plaintext, Terraform state.
## 11-13. Postgres, backup, DR
Full detail: `docs/BACKUP_DR.md`. Summary: conservative tuning baseline (not hand-tuned before
real RAM is known), daily encrypted backups with an off-host adapter point (Storage Box/S3/any
SSH target), and a **real, tested** backup→encrypt→restore→verify cycle
(`infra/backup/test-backup-restore.sh`) — run against disposable local Postgres with the actual
production migrations applied, not a mock.
## 14-17. Observability, logging, alerting, uptime
Full detail: `docs/OBSERVABILITY.md`. Prometheus + Grafana + Loki/Promtail (the user's stated
Prometheus/Grafana preference, honored) + Alertmanager, all self-hosted, no paid dependency.
Deliberately does not duplicate the application's own instructor learning analytics
(Phase 3B) — operational observability and learning analytics stay separate concerns with
separate audiences, per the explicit instruction. Application-level metrics (active
students/Labs, provisioning failures, Gateway error rate) are a documented, **not-yet-built**
follow-up — see that doc's honest accounting of what's real today (host/container/Postgres
metrics, HTTP healthz probes) vs. what needs app instrumentation later.
## 18-19. CI/CD, image strategy
`infra/ci/gitea-workflows/build-and-test.yml` — automatic test/build/tag/staging-deploy,
**manual** production promotion (`infra/scripts/deploy-production.sh`), matching the stated
preference. SHA-tagged images always, semantic tags on deliberate releases, never `:latest` in
staging/production. Full detail: `infra/docker/IMAGE_STRATEGY.md`.
## 20-21. Ansible, Terraform
`infra/ansible/site.yml` — idempotent roles, doesn't assume the VMs already exist (Terraform
creates them first). Real `ansible-playbook --syntax-check` pass. `infra/terraform/` — real
`tofu validate` pass against the actual `bpg/proxmox` provider schema. Neither installs Proxmox
itself or builds the base VM template — both stay deliberate manual, documented, one-time steps
(`infra/proxmox/README.md`), per the explicit "don't over-automate the fragile one-time step"
instruction.
## 22-23. Environment config, staging
`infra/environments/{dev,staging,production}/` — explicit separation, production never
inherits dev defaults. Staging is designed to run in the homelab or a resource-constrained
slice of the real server later — no second dedicated server required.
## 24-25. Load testing, capacity model
`infra/scripts/loadtest/` (real Prepare-Labs-driven boot test + `docker stats` sampling) and
`infra/scripts/capacity-model.js` (a real, working worksheet — tested against example inputs,
produces CPU-vs-memory-bound recommended/warning/hard-admission-control thresholds). No
concurrency number is hard-coded as a marketing figure; the example config in
`infra/environments/production/capacity.yml.example` is explicitly flagged as placeholder data.
## 26-27. Validation checklist, runbooks
`docs/PRODUCTION_VALIDATION_CHECKLIST.md` — one master checklist spanning hardware through
academy-workflow verification. 12 runbooks (`docs/RUNBOOK_*.md`) — first deployment, normal
deploy, rollback, Postgres restore, VM rebuild (both hosts), Docker TLS rotation, TLS renewal,
owner recovery, stuck Lab cleanup, failed migration, backup failure, disk failure, Lab Host
resource pressure.
## What this phase deliberately did NOT touch
Learning system design, curriculum content, RBAC model, session architecture — all Phase 3A/3B,
unchanged. No parent portal, billing, held scenarios, or production OIDC provider integration.
RootNode production infrastructure is dedicated to RootNode Academy — never folded into
existing Synthos production servers, and canonical source stays Gitea, never GitHub `kubekosh`.
## Known gap surfaced during this phase (not silently papered over)
Building the real production Compose/network config surfaced that the Gateway's existing
mechanism for reaching a Lab container (`env.internal_ip`, a Docker-bridge-local address) only
works when Gateway and Lab share a Docker daemon — true in every dev/single-host setup so far,
**not** true once Labs run on a genuinely separate `rn-lab-host-01`. The network-policy half is
built (a firewall allowance for Gateway→Lab-Host on a published port range,
`infra/firewall/rn-lab-host-01.rules`); the application half (the Provisioner recording a
reachable external address for each Lab, and the Gateway using it) is an explicit, reviewed,
NOT-YET-BUILT follow-up — see `docs/PRODUCTION_NETWORK_SECURITY.md` for the two candidate
designs and why neither was silently chosen here. This must land before real two-VM go-live;
flagging it clearly was judged better than guessing at an architecture change mid-infra-phase.