feat(postgres) — PostGIS, posé par le playbook et non par une image custom #52

Merged
arcodange merged 1 commits from arcodange/postgis-pour-kadans into main 2026-08-08 11:59:41 +02:00
2 changed files with 99 additions and 1 deletions
@@ -22,4 +22,45 @@ postgres:
pgbouncer:
auth_user: &pgbouncer_auth pgbouncer_auth
auth_user_password: *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('$','\$') }}"