locals { # Flatten applications × envs into per-instance objects, keyed by the elided # instance id (ADR-0002 elision rule): env=prod → "", else "-". # The Postgres owner role stays snake-case: "_role" (prod) / "__role". # For a prod-only app the key equals "", database equals "", and role # equals "_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"] }