feat(ansible): idempotent role-based host config for both production VMs
common/firewall/docker-tls/platform/lab-host/monitoring-agent/backup roles, orchestrated by site.yml with the ordering that matters (firewall/TLS land before the services that depend on them). Ansible Vault for secrets, no new tool introduced for this project's modest secret surface. Explicit dev/staging/production environment separation, no secrets committed (.gitignore covers vault plaintext, TLS keys, Terraform state, *.env). Validated: ansible-playbook --syntax-check clean.
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# Environment configuration
|
||||
|
||||
Three environments, explicitly separated so production never silently inherits a dev default:
|
||||
|
||||
| | dev | staging | production |
|
||||
|---|---|---|---|
|
||||
| Where it runs | any dev machine, `platform/docker-compose.dev.yml` | homelab or a resource-limited slice of the dedicated server once it exists | rn-platform-01 / rn-lab-host-01 on Proxmox |
|
||||
| `COOKIE_SECURE` | unset (plain HTTP) | `1` if served over HTTPS, else unset | `1` — required |
|
||||
| `TRUST_PROXY` | unset | `1` if fronted by a proxy | `1` — required, real reverse proxy in front |
|
||||
| `ADMIN`/owner bootstrap | `platform/api/bootstrap-owner.js` manually | same, against staging DB | same, against production DB, run once by a human, credential handled per `docs/PRODUCTION_DEPLOYMENT.md` §10 |
|
||||
| Secrets | `platform/.env` (gitignored, throwaway) | `infra/environments/staging/*.env` (gitignored, real but low-stakes) | `infra/environments/production/*.env` (gitignored, real, backed up per `docs/BACKUP_DR.md`) |
|
||||
| Lab image | built locally, `:test` tag | tagged build from CI, `:staging-<sha>` | tagged release, `:<sha>` or `:vX.Y.Z`, never `:latest` |
|
||||
|
||||
Each environment directory holds `*.env.template` files (tracked) — copy to `*.env` (gitignored
|
||||
via `infra/.gitignore`) and fill in real values. **Never commit a filled `.env` file.** See
|
||||
`docs/PRODUCTION_DEPLOYMENT.md` §10 for how production secrets are actually generated, stored,
|
||||
and rotated (not just "don't commit them").
|
||||
|
||||
`dev/` is intentionally thin — it just points back at the existing
|
||||
`platform/docker-compose.dev.yml` workflow (Phase 1–3B, unchanged); there was nothing new to
|
||||
build there for Phase 4.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Dev environment
|
||||
|
||||
Nothing new here for Phase 4 — dev still means `platform/docker-compose.dev.yml` +
|
||||
`platform/.env` (gitignored), exactly as built in Phases 1–3B. See `platform/README.md`.
|
||||
|
||||
This directory exists only so `infra/environments/` visibly has all three tiers, not because
|
||||
dev needed new infrastructure code.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Capacity planning inputs (Phase 4 §25). Copy to capacity.yml and fill in REAL measured values
|
||||
# once the actual Hetzner host and a real load test (infra/scripts/loadtest/) exist. The
|
||||
# numbers below are illustrative placeholders for a ~16c/32t/128GB host — do not treat them as
|
||||
# a real recommendation; run infra/scripts/capacity-model.js against real ones once measured.
|
||||
_placeholder: true
|
||||
|
||||
host_cpu_cores: 32 # threads, if the host does hyperthreading — matches how Docker/cgroups count them
|
||||
host_mem_mb: 131072 # 128 GB
|
||||
|
||||
platform_reserved_cpu_cores: 4 # rn-platform-01's VM allocation, reserved off the Lab Host's own pool
|
||||
platform_reserved_mem_mb: 16384
|
||||
|
||||
proxmox_overhead_pct: 5 # hypervisor + host OS overhead — a conservative starting estimate
|
||||
|
||||
measured_lab_cpu_cores: 2 # from infra/scripts/loadtest/sample-resources.sh peak CPU per Lab
|
||||
measured_lab_mem_mb: 2048 # from the same — peak memory per Lab
|
||||
|
||||
safety_margin_pct: 20 # headroom kept unallocated even at the "recommended" threshold
|
||||
@@ -0,0 +1,8 @@
|
||||
# Production environment template for rn-platform-01's `gateway` service.
|
||||
# `api`/`provisioner` are Docker Compose service names, not localhost — production runs these
|
||||
# on a named bridge network (rn-platform-internal), not network_mode: host like dev.
|
||||
|
||||
PLATFORM_API_URL=http://api:4300
|
||||
PROVISIONER_URL=http://provisioner:4100
|
||||
PROVISIONER_SHARED_SECRET=__REPLACE_same_value_as_platform.env_and_provisioner.env__
|
||||
GATEWAY_PORT=4200
|
||||
@@ -0,0 +1,38 @@
|
||||
# Production environment template for rn-platform-01's `api` service.
|
||||
# Copy to platform.env OUTSIDE git (see infra/backup/README.md "secrets" and
|
||||
# docs/PRODUCTION_DEPLOYMENT.md §10), fill in real values, never commit the filled file.
|
||||
# infra/.gitignore already excludes *.env (only *.env.template is tracked).
|
||||
|
||||
# ── Postgres ────────────────────────────────────────────────────────────────────────────
|
||||
# `postgres`/`provisioner` are Docker Compose service names, not localhost — production runs
|
||||
# these on a named bridge network (rn-platform-internal), not network_mode: host like dev.
|
||||
PLATFORM_DB_URL=postgres://rootnode_platform:__REPLACE_WITH_STRONG_PASSWORD__@postgres:5432/rootnode_platform
|
||||
|
||||
# ── Provisioner ─────────────────────────────────────────────────────────────────────────
|
||||
PROVISIONER_URL=http://provisioner:4100
|
||||
PROVISIONER_SHARED_SECRET=__REPLACE_openssl_rand_hex_32__
|
||||
|
||||
# ── Listen ──────────────────────────────────────────────────────────────────────────────
|
||||
PLATFORM_API_PORT=4300
|
||||
|
||||
# ── Sessions (production values — do NOT inherit dev defaults) ────────────────────────────
|
||||
STUDENT_SESSION_TTL_HOURS=8
|
||||
STUDENT_SESSION_IDLE_HOURS=2
|
||||
INSTRUCTOR_SESSION_TTL_HOURS=12
|
||||
INSTRUCTOR_SESSION_IDLE_HOURS=2
|
||||
|
||||
# ── Cookies / proxy — MUST be set for production, unlike dev ──────────────────────────────
|
||||
# Secure requires the origin to actually be HTTPS end-to-end (see docs/PRODUCTION_NETWORK_SECURITY.md §8).
|
||||
COOKIE_SECURE=1
|
||||
# Only set to 1 once a trusted reverse proxy (Cloudflare/nginx/Caddy, see infra/docker/reverse-proxy/)
|
||||
# is actually in front of the Gateway — otherwise req.ip becomes spoofable via X-Forwarded-For.
|
||||
TRUST_PROXY=1
|
||||
|
||||
# ── Frontend (baked into the platform/api image at build time; listed here for reference) ─
|
||||
# FRONTEND_DIST_DIR=/frontend-dist
|
||||
|
||||
# ── Future OIDC (Phase 3A adapter — not wired yet, see docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md §16) ─
|
||||
# OIDC_ISSUER_URL=
|
||||
# OIDC_CLIENT_ID=
|
||||
# OIDC_CLIENT_SECRET=__do_not_commit__
|
||||
# OIDC_REDIRECT_URL=https://academy.example.com/api/auth/oidc/callback
|
||||
@@ -0,0 +1,14 @@
|
||||
# Production environment template for rn-platform-01's `postgres` service.
|
||||
# Postgres itself binds only to localhost/the platform-internal Docker network — never exposed
|
||||
# beyond rn-platform-01, see docs/PRODUCTION_NETWORK_SECURITY.md §6.
|
||||
|
||||
POSTGRES_USER=rootnode_platform
|
||||
POSTGRES_PASSWORD=__REPLACE_WITH_STRONG_PASSWORD__
|
||||
POSTGRES_DB=rootnode_platform
|
||||
|
||||
# Tuning baseline — conservative, meant to be revisited once rn-platform-01's real allocated
|
||||
# RAM is known (see docs/production-postgres section in docs/PHASE4_PRODUCTION_INFRASTRUCTURE.md
|
||||
# §11). Applied via infra/docker/postgres/postgresql.conf.d/ — do not hand-edit the container.
|
||||
# POSTGRES_SHARED_BUFFERS=1GB
|
||||
# POSTGRES_EFFECTIVE_CACHE_SIZE=3GB
|
||||
# POSTGRES_MAX_CONNECTIONS=100
|
||||
@@ -0,0 +1,15 @@
|
||||
# Production Provisioner config overlay
|
||||
|
||||
Deployed by the `platform` Ansible role to `/etc/rootnode/provisioner-config/` on
|
||||
rn-platform-01 (`PROVISIONER_CONFIG_DIR`), replacing the dev-only files shipped in
|
||||
`platform/provisioner/config/`.
|
||||
|
||||
- `lab-hosts.yaml` — points at the real rn-lab-host-01 VM over TLS instead of the dev Unix
|
||||
socket. TLS paths reference material generated by `infra/scripts/docker-tls/` and deployed by
|
||||
the `docker-tls` Ansible role — **never checked into this repo** (see
|
||||
`infra/docker/tls/.gitignore`).
|
||||
- `resource-policy.yaml` — deliberately still the Phase 1 dev defaults, unchanged, with a
|
||||
loud comment explaining why. Per the explicit Phase 4 instruction: do not hard-code final
|
||||
per-Lab CPU/RAM numbers before the real host is benchmarked (`docs/CAPACITY_PLANNING.md`).
|
||||
Whoever runs the first production load test (`infra/scripts/loadtest/`) updates this file
|
||||
with real measured numbers, not this deployment.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Production lab_hosts seed — points at the real rn-lab-host-01 VM instead of Phase 1's dev
|
||||
# Unix socket. Same mechanism the Provisioner has had since Phase 1
|
||||
# (platform/provisioner/config/lab-hosts.seed.yaml), just pointed at a real remote endpoint —
|
||||
# no application code changes required for this, see docs/PRODUCTION_NETWORK_SECURITY.md §7.
|
||||
#
|
||||
# {{ lab_host_ip }} is an Ansible template variable — see infra/ansible/group_vars/production.yml.
|
||||
# Rendered by the `platform` role; the raw file here keeps the placeholder for readability.
|
||||
|
||||
lab_hosts:
|
||||
- lab_host_id: labhost-01
|
||||
display_name: "RootNode Lab Host VM 01 (production)"
|
||||
endpoint: "tcp://{{ lab_host_ip }}:2376"
|
||||
tls_ca_path: /etc/rootnode/docker-tls/ca.pem
|
||||
tls_cert_path: /etc/rootnode/docker-tls/provisioner-client-cert.pem
|
||||
tls_key_path: /etc/rootnode/docker-tls/provisioner-client-key.pem
|
||||
enabled: true
|
||||
@@ -0,0 +1,25 @@
|
||||
# PRODUCTION OVERLAY — placeholder, deliberately unchanged from the Phase 1 dev defaults.
|
||||
# Do not hand-tune these before the real host exists (docs/CAPACITY_PLANNING.md, Phase 4 §25).
|
||||
# After the first real load test on rn-lab-host-01 (infra/scripts/loadtest/), replace this
|
||||
# comment block and the numbers below with actual measured values, then redeploy via the
|
||||
# `platform` Ansible role (idempotent — re-running just re-renders this file).
|
||||
#
|
||||
# Phase 1 resource policy — config-driven, not hard-coded (decision #4).
|
||||
# Target host class for planning is ~16 cores/32 threads, 128GB RAM, SSD/NVMe.
|
||||
|
||||
default:
|
||||
cpu_limit: 2.0 # vCPUs
|
||||
mem_limit_mb: 4096
|
||||
pids_limit: 512
|
||||
storage_limit_mb: 8192 # only enforced if the host's Docker storage driver supports
|
||||
# --storage-opt size= (see known-limitations in the SOP)
|
||||
|
||||
idle_threshold_minutes: 30 # past this: log a warning event only
|
||||
abandoned_threshold_minutes: 120 # past this: reaper destroys for real
|
||||
|
||||
reaper_poll_interval_seconds: 60
|
||||
|
||||
health_check:
|
||||
path: /api/health
|
||||
timeout_ms: 240000 # k3s cold start in a nested container can be slow — matches the
|
||||
# ~60-attempt wait already in scripts/entrypoint.sh
|
||||
@@ -0,0 +1,20 @@
|
||||
# Production environment template for rn-platform-01's `provisioner` service.
|
||||
# See platform.env.template for the secrets-handling convention.
|
||||
#
|
||||
# Docker-connectivity config (lab_hosts) and resource limits are NOT env vars — they're YAML
|
||||
# files the Provisioner already loads from PROVISIONER_CONFIG_DIR (Phase 1 design, unchanged).
|
||||
# Production overlays for both live at infra/environments/production/provisioner-config/ — see
|
||||
# that directory's README before touching resource-policy.yaml numbers.
|
||||
|
||||
PLATFORM_DB_URL=postgres://rootnode_platform:__REPLACE_WITH_STRONG_PASSWORD__@localhost:5432/rootnode_platform
|
||||
PROVISIONER_SHARED_SECRET=__REPLACE_same_value_as_platform.env__
|
||||
PROVISIONER_PORT=4100
|
||||
PROVISIONER_CONFIG_DIR=/etc/rootnode/provisioner-config
|
||||
|
||||
# Real Lab image, tagged (never :latest in production — see infra/docker/IMAGE_STRATEGY.md)
|
||||
LAB_IMAGE_REF=registry.example.com/rootnode/lab:__GIT_SHA_OR_TAG__
|
||||
|
||||
# ── Egress proxy on rn-lab-host-01 ─────────────────────────────────────────────────────────
|
||||
EGRESS_PROXY_PORT=3128
|
||||
DNS_RESOLVER=127.0.0.11
|
||||
NO_PROXY_CIDRS=10.42.0.0/16,10.43.0.0/16
|
||||
@@ -0,0 +1,16 @@
|
||||
# Staging environment
|
||||
|
||||
Same shape as production (`docs/PRODUCTION_DEPLOYMENT.md` applies to both — pass `-e env=staging`
|
||||
to the Ansible playbooks), lower stakes: separate database name/port, staging-tagged images, and
|
||||
`COOKIE_SECURE` only if staging actually sits behind HTTPS. See `docs/CI_CD` section in
|
||||
`docs/PHASE4_PRODUCTION_INFRASTRUCTURE.md` §18 for how a Gitea Actions build lands here
|
||||
automatically before a human promotes it to production.
|
||||
|
||||
Staging is designed to run either in the homelab (a couple of low-resource VMs/containers) or,
|
||||
once the dedicated server exists, as a resource-constrained slice of it — see
|
||||
`docs/PHASE4_PRODUCTION_INFRASTRUCTURE.md` §23 for the sizing note. It does not require a
|
||||
second dedicated server.
|
||||
|
||||
Staging acceptance = the same four things a person actually needs before promoting a build:
|
||||
app deploys, migrations run clean, one or two disposable Labs boot through the Gateway, and an
|
||||
assessment can be started and completed. See `infra/scripts/staging-smoke-test.sh`.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Production environment template for rn-platform-01's `gateway` service.
|
||||
|
||||
PLATFORM_API_URL=http://localhost:4300
|
||||
PROVISIONER_URL=http://localhost:4100
|
||||
PROVISIONER_SHARED_SECRET=__REPLACE_same_value_as_platform.env_and_provisioner.env__
|
||||
GATEWAY_PORT=4200
|
||||
@@ -0,0 +1,36 @@
|
||||
# Production environment template for rn-platform-01's `api` service.
|
||||
# Copy to platform.env OUTSIDE git (see infra/backup/README.md "secrets" and
|
||||
# docs/PRODUCTION_DEPLOYMENT.md §10), fill in real values, never commit the filled file.
|
||||
# infra/.gitignore already excludes *.env (only *.env.template is tracked).
|
||||
|
||||
# ── Postgres ────────────────────────────────────────────────────────────────────────────
|
||||
PLATFORM_DB_URL=postgres://rootnode_platform_staging:__REPLACE_WITH_STRONG_PASSWORD__@localhost:5433/rootnode_platform_staging
|
||||
|
||||
# ── Provisioner ─────────────────────────────────────────────────────────────────────────
|
||||
PROVISIONER_URL=http://localhost:4100
|
||||
PROVISIONER_SHARED_SECRET=__REPLACE_openssl_rand_hex_32__
|
||||
|
||||
# ── Listen ──────────────────────────────────────────────────────────────────────────────
|
||||
PLATFORM_API_PORT=4300
|
||||
|
||||
# ── Sessions (production values — do NOT inherit dev defaults) ────────────────────────────
|
||||
STUDENT_SESSION_TTL_HOURS=8
|
||||
STUDENT_SESSION_IDLE_HOURS=2
|
||||
INSTRUCTOR_SESSION_TTL_HOURS=12
|
||||
INSTRUCTOR_SESSION_IDLE_HOURS=2
|
||||
|
||||
# ── Cookies / proxy — MUST be set for production, unlike dev ──────────────────────────────
|
||||
# Secure requires the origin to actually be HTTPS end-to-end (see docs/PRODUCTION_NETWORK_SECURITY.md §8).
|
||||
COOKIE_SECURE=1 # unset if staging is not served over HTTPS
|
||||
# Only set to 1 once a trusted reverse proxy (Cloudflare/nginx/Caddy, see infra/docker/reverse-proxy/)
|
||||
# is actually in front of the Gateway — otherwise req.ip becomes spoofable via X-Forwarded-For.
|
||||
TRUST_PROXY=1
|
||||
|
||||
# ── Frontend (baked into the platform/api image at build time; listed here for reference) ─
|
||||
# FRONTEND_DIST_DIR=/frontend-dist
|
||||
|
||||
# ── Future OIDC (Phase 3A adapter — not wired yet, see docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md §16) ─
|
||||
# OIDC_ISSUER_URL=
|
||||
# OIDC_CLIENT_ID=
|
||||
# OIDC_CLIENT_SECRET=__do_not_commit__
|
||||
# OIDC_REDIRECT_URL=https://academy.example.com/api/auth/oidc/callback
|
||||
@@ -0,0 +1,14 @@
|
||||
# Production environment template for rn-platform-01's `postgres` service.
|
||||
# Postgres itself binds only to localhost/the platform-internal Docker network — never exposed
|
||||
# beyond rn-platform-01, see docs/PRODUCTION_NETWORK_SECURITY.md §6.
|
||||
|
||||
POSTGRES_USER=rootnode_platform
|
||||
POSTGRES_PASSWORD=__REPLACE_WITH_STRONG_PASSWORD__
|
||||
POSTGRES_DB=rootnode_platform
|
||||
|
||||
# Tuning baseline — conservative, meant to be revisited once rn-platform-01's real allocated
|
||||
# RAM is known (see docs/production-postgres section in docs/PHASE4_PRODUCTION_INFRASTRUCTURE.md
|
||||
# §11). Applied via infra/docker/postgres/postgresql.conf.d/ — do not hand-edit the container.
|
||||
# POSTGRES_SHARED_BUFFERS=1GB
|
||||
# POSTGRES_EFFECTIVE_CACHE_SIZE=3GB
|
||||
# POSTGRES_MAX_CONNECTIONS=100
|
||||
@@ -0,0 +1,16 @@
|
||||
# Production lab_hosts seed — points at the real rn-lab-host-01 VM instead of Phase 1's dev
|
||||
# Unix socket. Same mechanism the Provisioner has had since Phase 1
|
||||
# (platform/provisioner/config/lab-hosts.seed.yaml), just pointed at a real remote endpoint —
|
||||
# no application code changes required for this, see docs/PRODUCTION_NETWORK_SECURITY.md §7.
|
||||
#
|
||||
# {{ lab_host_ip }} is an Ansible template variable — see infra/ansible/group_vars/production.yml.
|
||||
# Rendered by the `platform` role; the raw file here keeps the placeholder for readability.
|
||||
|
||||
lab_hosts:
|
||||
- lab_host_id: labhost-01
|
||||
display_name: "RootNode Lab Host VM 01 (production)"
|
||||
endpoint: "tcp://{{ lab_host_ip }}:2376"
|
||||
tls_ca_path: /etc/rootnode/docker-tls/ca.pem
|
||||
tls_cert_path: /etc/rootnode/docker-tls/provisioner-client-cert.pem
|
||||
tls_key_path: /etc/rootnode/docker-tls/provisioner-client-key.pem
|
||||
enabled: true
|
||||
@@ -0,0 +1,25 @@
|
||||
# PRODUCTION OVERLAY — placeholder, deliberately unchanged from the Phase 1 dev defaults.
|
||||
# Do not hand-tune these before the real host exists (docs/CAPACITY_PLANNING.md, Phase 4 §25).
|
||||
# After the first real load test on rn-lab-host-01 (infra/scripts/loadtest/), replace this
|
||||
# comment block and the numbers below with actual measured values, then redeploy via the
|
||||
# `platform` Ansible role (idempotent — re-running just re-renders this file).
|
||||
#
|
||||
# Phase 1 resource policy — config-driven, not hard-coded (decision #4).
|
||||
# Target host class for planning is ~16 cores/32 threads, 128GB RAM, SSD/NVMe.
|
||||
|
||||
default:
|
||||
cpu_limit: 2.0 # vCPUs
|
||||
mem_limit_mb: 4096
|
||||
pids_limit: 512
|
||||
storage_limit_mb: 8192 # only enforced if the host's Docker storage driver supports
|
||||
# --storage-opt size= (see known-limitations in the SOP)
|
||||
|
||||
idle_threshold_minutes: 30 # past this: log a warning event only
|
||||
abandoned_threshold_minutes: 120 # past this: reaper destroys for real
|
||||
|
||||
reaper_poll_interval_seconds: 60
|
||||
|
||||
health_check:
|
||||
path: /api/health
|
||||
timeout_ms: 240000 # k3s cold start in a nested container can be slow — matches the
|
||||
# ~60-attempt wait already in scripts/entrypoint.sh
|
||||
@@ -0,0 +1,20 @@
|
||||
# Production environment template for rn-platform-01's `provisioner` service.
|
||||
# See platform.env.template for the secrets-handling convention.
|
||||
#
|
||||
# Docker-connectivity config (lab_hosts) and resource limits are NOT env vars — they're YAML
|
||||
# files the Provisioner already loads from PROVISIONER_CONFIG_DIR (Phase 1 design, unchanged).
|
||||
# Production overlays for both live at infra/environments/production/provisioner-config/ — see
|
||||
# that directory's README before touching resource-policy.yaml numbers.
|
||||
|
||||
PLATFORM_DB_URL=postgres://rootnode_platform:__REPLACE_WITH_STRONG_PASSWORD__@localhost:5433/rootnode_platform
|
||||
PROVISIONER_SHARED_SECRET=__REPLACE_same_value_as_platform.env__
|
||||
PROVISIONER_PORT=4100
|
||||
PROVISIONER_CONFIG_DIR=/etc/rootnode/provisioner-config
|
||||
|
||||
# Real Lab image, tagged (never :latest in production — see infra/docker/IMAGE_STRATEGY.md)
|
||||
LAB_IMAGE_REF=registry.example.com/rootnode/lab:staging-__GIT_SHA__
|
||||
|
||||
# ── Egress proxy on rn-lab-host-01 ─────────────────────────────────────────────────────────
|
||||
EGRESS_PROXY_PORT=3128
|
||||
DNS_RESOLVER=127.0.0.11
|
||||
NO_PROXY_CIDRS=10.42.0.0/16,10.43.0.0/16
|
||||
Reference in New Issue
Block a user