feat(postgres) — PostGIS, posé par le playbook et non par une image custom
Kadans doit ranger le contour d'un quartier en vraie géométrie
(geometry(MultiPolygon,4326), ST_Contains, index GiST). L'extension
n'existait nulle part : mesuré sur pi2, `pg_available_extensions` ne
rendait AUCUNE ligne `postgis%`.
Arbitrage fondateur (2026-08-08) : on garde `postgres:16.3-alpine` et on
pose l'extension par Ansible, comme le playbook pose déjà les bases et le
rôle pgbouncer. Pas d'image custom.
⚠ POURQUOI LE RECALAGE DE CHEMINS N'EST PAS FACULTATIF — mesuré, arm64.
`apk add postgis` SEUL réussit, et `CREATE EXTENSION postgis` échoue quand
même :
ERROR: extension "postgis" is not available
DETAIL: Could not open extension control file
"/usr/local/share/postgresql/extension/postgis.control"
Le paquet Alpine vise la disposition d'Alpine (/usr/share/postgresql16,
/usr/lib/postgresql16) ; l'image officielle compile le serveur dans
/usr/local. Les fichiers sont là, le serveur regarde ailleurs. Après
recalage : PostGIS 3.4 USE_GEOS=1 USE_PROJ=1, et un polygone lyonnais qui
fait l'aller-retour ST_GeomFromText → ST_AsGeoJSON.
Ne pas « simplifier » en un `apk add` nu : la simulation dit OK,
l'installation dit OK, et l'extension reste inutilisable.
⚠ INSTALLATION PAR CONTENEUR, PAS PAR VOLUME. `apk add` écrit dans la
couche inscriptible : recréer le conteneur efface PostGIS pendant que les
données gardent leurs colonnes géométriques — toute requête spatiale casse
jusqu'au prochain passage du playbook. D'où l'ordre (déploiement compose
PUIS installation), l'idempotence, et surtout la tâche de vérification.
La vérification ne se contente pas d'un code de retour : elle exige que la
base rende USE_GEOS=1 ET un vrai Point GeoJSON avec son SRID. Un bouchon
qui répondrait une chaîne vide passerait un simple `rc == 0` et ne
prouverait rien — un playbook vert sur une extension absente ferait
atterrir le symptôme dans Kadans, des jours plus tard, déguisé en bug
applicatif.
Vérifié sur le conteneur RÉEL sans le modifier : `apk add --simulate`
résout postgis 3.4.2-r2, et `pg_config` y rend bien les deux chemins que
les variables supposent.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01J4UE4AmX5PAMN6c6Q6Fey9
This commit is contained in:
@@ -23,3 +23,44 @@ postgres:
|
||||
pgbouncer:
|
||||
auth_user: &pgbouncer_auth pgbouncer_auth
|
||||
auth_user_password: *pgbouncer_auth
|
||||
|
||||
# PostGIS — spatial types for the databases that ask for it.
|
||||
#
|
||||
# Kadans stores neighbourhood ("zone") outlines as real geometry, so it needs
|
||||
# geometry(MultiPolygon,4326), ST_Contains and a GiST index. The image stays
|
||||
# `postgres:16.3-alpine`: the extension is installed INTO the running container
|
||||
# by setup/postgres.yml, exactly like the pgbouncer role and the app databases
|
||||
# are created there. No custom image (founder's call, 2026-08-08).
|
||||
#
|
||||
# WARNING — this is a per-CONTAINER install, not a per-VOLUME one. `apk add`
|
||||
# writes to the container's writable layer, so recreating the container (image
|
||||
# change, `docker compose up --force-recreate`) REMOVES PostGIS while the data
|
||||
# keeps its geometry columns — every spatial query then fails until this
|
||||
# playbook runs again. The install task is therefore idempotent and runs after
|
||||
# every compose deploy, and it is the reason `postgis_verifier` exists below:
|
||||
# a silent absence would look like an application bug.
|
||||
postgis:
|
||||
# Only these databases get the extension. Adding one here is the whole change.
|
||||
databases:
|
||||
- kadans
|
||||
# The Alpine package. Pinned to a MAJOR line, not a patch: postgis 3.x
|
||||
# upgrades within a major are ABI-compatible with a given PostgreSQL major.
|
||||
paquet: postgis
|
||||
# ⚠ WHY THE COPY STEP EXISTS — measured on pi2 (arm64), 2026-08-08.
|
||||
# `apk add postgis` alone SUCCEEDS and `CREATE EXTENSION postgis` still fails:
|
||||
#
|
||||
# ERROR: extension "postgis" is not available
|
||||
# DETAIL: Could not open extension control file
|
||||
# "/usr/local/share/postgresql/extension/postgis.control"
|
||||
#
|
||||
# Alpine's package targets Alpine's own PostgreSQL layout
|
||||
# (/usr/share/postgresql16, /usr/lib/postgresql16), while the official
|
||||
# `postgres:16-alpine` image builds the server into /usr/local. The files are
|
||||
# on disk, the server looks elsewhere. Relocating them makes it work — proven
|
||||
# in a throwaway container: PostGIS 3.4 USE_GEOS=1 USE_PROJ=1, and a Lyon
|
||||
# polygon round-tripping through ST_GeomFromText/ST_AsGeoJSON.
|
||||
#
|
||||
# Do NOT "simplify" this to a bare `apk add`. An `apk add --simulate` reports
|
||||
# OK, the install reports OK, and the extension is still unusable.
|
||||
source_partagee: /usr/share/postgresql16/extension
|
||||
source_lib: /usr/lib/postgresql16
|
||||
@@ -25,6 +25,63 @@
|
||||
applications_databases:
|
||||
gitea: "{{ gitea_database }}"
|
||||
|
||||
# ── PostGIS ────────────────────────────────────────────────────────────
|
||||
# Installed INTO the running container rather than baked into a custom
|
||||
# image (founder's call, 2026-08-08). See group_vars/postgres/postgres.yml
|
||||
# for why the copy step is not optional, and for the durability caveat.
|
||||
#
|
||||
# Runs after the compose deploy above ON PURPOSE: if that task recreated
|
||||
# the container, the writable layer is fresh and PostGIS is gone with it.
|
||||
# This is what makes the pair (deploy, install) safe to replay.
|
||||
- name: Install PostGIS into the Postgres container
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
docker exec {{ postgres_container_name }} sh -c '
|
||||
set -eu
|
||||
apk add --no-cache {{ postgis.paquet }} >/dev/null
|
||||
cp -r {{ postgis.source_partagee }}/* "$(pg_config --sharedir)/extension/"
|
||||
cp -r {{ postgis.source_lib }}/*.so "$(pg_config --pkglibdir)/"
|
||||
'
|
||||
# `apk add` is idempotent and the copies overwrite identical files, so a
|
||||
# replay changes nothing observable. We do not pretend to detect that:
|
||||
# claiming `changed_when: false` outright would hide a REAL first install.
|
||||
register: postgis_installation
|
||||
changed_when: "'Installing' in postgis_installation.stdout"
|
||||
|
||||
- name: Enable PostGIS on the databases that need it
|
||||
ansible.builtin.shell: |
|
||||
docker exec {{ postgres_container_name }} \
|
||||
psql -U postgres -d {{ item }} -tAc 'CREATE EXTENSION IF NOT EXISTS postgis;'
|
||||
loop: "{{ postgis.databases }}"
|
||||
register: postgis_activation
|
||||
changed_when: "'CREATE EXTENSION' in postgis_activation.stdout"
|
||||
|
||||
# ⚠ THE HALF THAT MATTERS. Without it, a botched install leaves a green
|
||||
# playbook and an application that fails at its first spatial query — the
|
||||
# symptom would land in Kadans, days later, looking like an app bug.
|
||||
# We ask the database itself, and we FAIL on anything unexpected.
|
||||
- name: Verify PostGIS answers on every database
|
||||
ansible.builtin.shell: |
|
||||
docker exec {{ postgres_container_name }} psql -U postgres -d {{ item }} -tAc \
|
||||
"SELECT postgis_version() || ' | ' || ST_AsGeoJSON(ST_SetSRID(ST_Point(4.83, 45.76), 4326));"
|
||||
loop: "{{ postgis.databases }}"
|
||||
register: postgis_verifier
|
||||
changed_when: false
|
||||
# Not just "the command exited 0": a real geometry must come back with
|
||||
# the SRID applied. A stub that answered an empty string would pass a
|
||||
# bare rc check — and prove nothing.
|
||||
failed_when: >-
|
||||
postgis_verifier.rc != 0
|
||||
or 'USE_GEOS=1' not in postgis_verifier.stdout
|
||||
or '"type":"Point"' not in postgis_verifier.stdout
|
||||
|
||||
- name: Report the PostGIS version in use
|
||||
ansible.builtin.debug:
|
||||
msg: "PostGIS on {{ item.item }} → {{ item.stdout | trim }}"
|
||||
loop: "{{ postgis_verifier.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item }}"
|
||||
|
||||
- name: Create auth_user for pgbouncer (connection pool component)
|
||||
ansible.builtin.shell: |
|
||||
docker exec -it {{ postgres_container_name }} psql -U postgres -d {{ database }} -tc "{{ pg_instruction.replace('$','\$') }}"
|
||||
|
||||
Reference in New Issue
Block a user