Files
factory/postgres/iac/main.tf
Gabriel Radureau c00c4cdd5c feat(multi-env): Phase B — make factory machinery env-capable (no activation)
ADR-0002 Phase B. Makes postgres/iac, argocd, and the conventions docs
multi-environment-capable WITHOUT activating any sandbox yet — every app
stays prod-only, so this change is behaviour-neutral:
  - postgres/iac `tofu plan` is a no-op (proven: the elision flatten keys
    are bare app names, db=<app>, role=<app>_role — identical addresses)
  - the argocd apps.yaml render is byte-identical (181→181 lines, diff
    empty) since no app declares `envs`

postgres/iac:
- variables.tf: `applications` becomes set(object({name, envs=optional(["prod"])}))
- main.tf: a `local.app_instances` flatten of applications × envs keyed by the
  elided instance id (env=prod → "<app>"); per-app resources iterate it and
  reference each.key / each.value.{database,role}. For prod-only apps every
  resource address + attribute is unchanged. (main.tf also got a full
  `tofu fmt` pass — the pgbouncer function block reindents 4→2 spaces, which
  is cosmetic; the correctness gate is the CI tofu plan, not the text diff.)
- terraform.tfvars: string entries → { name = "..." } objects.

argocd/templates/apps.yaml:
- after the prod Application, a `range $app_attr.envs` loop renders one extra
  Application per non-prod env: name/namespace `<app>-<env>`, shared repoURL,
  helm.valueFiles [values.yaml, values-<env>.yaml], per-env syncPolicy override.
  Renders nothing while no app sets `envs` → prod render unchanged.

docs:
- doc/runbooks/new-web-app/conventions.md (FR, authoritative): new section
  "Plusieurs environnements pour une même app" — elision rule, suffix rule,
  snake-case owner-role exception, erp/erp-sandbox table, ADR-0002 link.
- vibe/guidebooks/lab-ecosystem/naming-conventions.md (EN mirror): the env
  coordinate section + a "Two sandbox models" section reconciling the
  separate-cluster (ADR-0001, names repeat) vs in-cluster sibling (ADR-0002,
  <env> suffix) strategies; Last Updated bumped; ADR-0002 cross-links.

Activation (erp gets envs=["prod","sandbox"] in postgres tfvars + argocd
values + erp/iac) is Phase D, gated by its own plan review.

Refs ADR-0002 (factory#15). Phase A = tools#2 (merged). Phase C = erp#11 (merged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-28 16:28:28 +02:00

116 lines
3.5 KiB
HCL
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.
locals {
# Flatten applications × envs into per-instance objects, keyed by the elided
# instance id (ADR-0002 elision rule): env=prod → "<app>", else "<app>-<env>".
# The Postgres owner role stays snake-case: "<app>_role" (prod) / "<app>_<env>_role".
# For a prod-only app the key equals "<app>", database equals "<app>", and role
# equals "<app>_role" — identical to the previous set(string) for_each, so every
# resource address and attribute is unchanged (a no-op plan).
app_instances = merge([
for app in var.applications : {
for env in app.envs :
(env == "prod" ? app.name : "${app.name}-${env}") => {
database = env == "prod" ? app.name : "${app.name}-${env}"
role = env == "prod" ? "${app.name}_role" : "${app.name}_${env}_role"
}
}
]...)
}
resource "random_password" "credentials_editor" {
length = 24
override_special = "-:!+<>"
}
resource "postgresql_role" "credentials_editor" {
name = "credentials_editor"
login = true
password = random_password.credentials_editor.result
create_role = true
lifecycle {
ignore_changes = [
roles,
]
}
}
resource "vault_kv_secret" "postgres_admin_credentials" {
path = "kvv1/postgres/credentials_editor/credentials"
data_json = jsonencode({
username = postgresql_role.credentials_editor.name
password = postgresql_role.credentials_editor.password
})
}
resource "postgresql_role" "app_role" {
for_each = local.app_instances
name = each.value.role
login = false
}
resource "postgresql_grant_role" "credentials_editor_app_role" {
for_each = local.app_instances
role = postgresql_role.credentials_editor.name
grant_role = postgresql_role.app_role[each.key].name
with_admin_option = true
}
resource "postgresql_database" "app_db" {
for_each = local.app_instances
name = each.value.database
owner = postgresql_role.app_role[each.key].name
template = "template0"
alter_object_ownership = true
}
resource "postgresql_function" "pgbouncer_user_lookup" {
for_each = local.app_instances
name = "user_lookup"
database = postgresql_database.app_db[each.key].name
arg {
mode = "IN"
name = "i_username"
type = "text"
}
arg {
mode = "OUT"
name = "uname"
type = "text"
}
arg {
mode = "OUT"
name = "phash"
type = "text"
}
returns = "record"
language = "plpgsql"
body = <<-EOF
BEGIN
SELECT usename, passwd FROM pg_catalog.pg_shadow
WHERE usename = i_username INTO uname, phash;
RETURN;
END;
EOF
parallel = "SAFE"
security_definer = true
}
resource "postgresql_grant" "pgbouncer_user_lookup_public_revoke" {
for_each = local.app_instances
database = postgresql_function.pgbouncer_user_lookup[each.key].database
role = "public"
schema = "public"
object_type = "function"
objects = [
postgresql_function.pgbouncer_user_lookup[each.key].name,
]
privileges = []
}
resource "postgresql_grant" "pgbouncer_user_lookup" {
depends_on = [postgresql_grant.pgbouncer_user_lookup_public_revoke] # can't do both in parallel
for_each = local.app_instances
database = postgresql_function.pgbouncer_user_lookup[each.key].database
role = "pgbouncer_auth"
schema = "public"
object_type = "function"
objects = [
postgresql_function.pgbouncer_user_lookup[each.key].name,
]
privileges = ["EXECUTE"]
}