Cut the comment bloat from the Podman playbooks

Comment blocks now stay within three lines and explain only the non-obvious. Long histories, include examples, and commented-out code are gone. No task, key, or value changed; the only data change is comment text inside the generated unit files.
This commit is contained in:
tanshu committed 2026-10-18 22:10:57 +05:30
1 parent 2aab1aec1e
commit 476a6d7cd0
85 files changed
+372 -2433

No files matched your search

+1 -23
View File
@@ -1,25 +1,5 @@
---
# Deploys Valkey as a rootless Podman Quadlet unit.
#
# Connects as the container user and runs everything as that user: the Quadlet
# units belong to it, so the connection's PAM session is what makes
# `systemctl --user` work without any environment juggling.
#
# gather_facts is required: podman_user and the home directory come from the
# user_id / user_dir facts.
#
# The rootless-Podman prerequisites (installing podman, enabling lingering, the
# low-port sysctl, creating the Quadlet directory) are deliberately NOT applied
# here. They are the shared task file at caddy/roles/podman/tasks/main.yaml, and
# a host that has already run caddy/playbook.yml has them:
#
# - name: Rootless Podman prerequisites
# ansible.builtin.include_tasks: "{{ playbook_dir }}/../caddy/roles/podman/tasks/main.yaml"
#
# The variables those tasks need are duplicated at the top of vars/default.yml,
# so this play stands on its own.
#
# Run with: ansible-playbook -u tanshu valkey/playbook.yml
# Run this before the app playbooks. Each service attaches itself.
- name: Deploy Valkey as a rootless Podman Quadlet
hosts: emily
gather_facts: true
@@ -27,6 +7,4 @@
- vars/default.yml
roles:
# The base Valkey unit. It does NOT know which app networks it serves.
# Each service attaches itself via valkey/roles/attach.
- valkey
+6 -46
View File
@@ -1,45 +1,12 @@
---
# Attach a service's network to Valkey.
#
# Valkey is the *base* unit and knows nothing about its clients, the same way
# Caddy knows nothing about its upstreams. A service that needs a cache adds
# itself by dropping files next to valkey.container, and Quadlet merges them in.
#
# Include this from a service playbook:
#
# - name: Attach this service to Valkey
# ansible.builtin.include_tasks: "{{ playbook_dir }}/../valkey/roles/attach/tasks/main.yaml"
# vars:
# network_name: "{{ network_name }}"
#
# The including play must:
#
# 1. have created {{ network_name }}.network already — caddy/roles/register
# does that, and every service here that needs a cache is also behind
# Caddy, so its register include comes first;
# 2. run the `podman` role (or include its task file and define the same four
# vars), for podman_user and quadlet_dir;
# 3. define a "Restart Valkey" handler — see
# roles/valkey/handlers/main.yaml for the exact snippet; and
# 4. flush handlers before starting the app, on a first attach. This include
# notifies that restart, so by default it runs at the end of the play —
# after the app has already started and failed to reach `redis`. A
# `meta: flush_handlers` immediately after this include is enough; see
# nextcloud/roles/valkey for the working example.
#
# It runs as the connection user, which must be the rootless podman_user.
#
# A service may instead write these two files from its own role; the bytes have
# to match this file exactly, or the two will overwrite each other on every run.
- name: "Check that the {{ network_name }} network unit exists"
ansible.builtin.stat:
path: "{{ quadlet_dir }}/{{ network_name }}.network"
register: valkey_attach_network
# A drop-in naming a .network file that does not exist makes Quadlet fail to
# generate the whole valkey unit, with a message that does not mention the
# drop-in at all. Fail here instead, where the cause is obvious.
# A drop-in naming a missing network breaks unit generation with an unrelated
# error, so fail here instead.
- name: "Fail if {{ network_name }} has no network unit"
ansible.builtin.assert:
that: valkey_attach_network.stat.exists
@@ -64,21 +31,14 @@
group: "{{ podman_user }}"
mode: '0644'
content: |
# Added by the {{ network_name }} playbook. Do not edit by hand.
#
# Deleting this file removes the network from {{ valkey_container | default('valkey') }} on
# the next daemon-reload; nothing else changes.
# Added by {{ network_name }}. Deleting it detaches the network from
# {{ valkey_container | default('valkey') }} on the next daemon-reload.
[Container]
Network={{ network_name }}.network
notify: Restart Valkey
# NetworkAlias= applies to every network the container joins, which is exactly
# right for a hub: this repo's cache clients reach Valkey as `redis` (nextcloud,
# which writes REDIS_HOST=redis into its config, plus outline and overleaf),
# while the rest use the container name. It is a fixed value in one shared file,
# not a per-service option, so the same bytes are written no matter how many
# services attach — two services writing their own aliases would make Quadlet
# pass --network-alias twice.
# NetworkAlias applies to every network, so one shared file holds the fixed
# alias. Two services writing their own would pass --network-alias twice.
- name: "Give {{ valkey_container | default('valkey') }} the shared 'redis' alias"
ansible.builtin.copy:
dest: >-
+2 -27
View File
@@ -1,12 +1,12 @@
---
# All handlers run as the connection user ({{ podman_user }}), so systemctl
# --user works without setting XDG_RUNTIME_DIR.
- name: Reload systemd user units
ansible.builtin.command: systemctl --user daemon-reload
changed_when: true
listen: Reload systemd user units
# A new drop-in needs a recreate, which keeps data through the final RDB but
# takes Nextcloud sessions and Yamtrack's Celery queue offline for seconds.
- name: Restart Valkey
ansible.builtin.systemd_service:
name: "{{ unit_name }}"
@@ -14,28 +14,3 @@
state: restarted
daemon_reload: true
listen: Restart Valkey
# Reference implementation for service playbooks. A service that attaches its
# network with valkey/roles/attach must notify "Restart Valkey", and defines its
# own copy of this handler, e.g.:
#
# - name: Restart Valkey
# ansible.builtin.systemd_service:
# name: valkey.service
# scope: user
# state: restarted
# daemon_reload: true
# listen: Restart Valkey
#
# A new drop-in changes the generated unit, so Valkey has to be recreated to join
# the network — Quadlet has no live equivalent of `podman network connect`.
#
# The cost is lighter than Postgres's in one way and heavier in another. Lighter:
# no app is left without its data. RDB persistence is on by default, the recreate
# is a graceful stop, and valkey-server writes a final `dump.rdb` on the way down
# that the new container loads back from the bind mount, so keys survive. Heavier
# to reason about: two clients do not treat this as a pure cache — Nextcloud keeps
# its PHP sessions and `memcache.locking` here, and Yamtrack uses it as its Celery
# broker — so the seconds the container is down are seconds in which logins and
# queued tasks are unavailable. Only a hard kill or a power loss costs changes
# since the last save point. See readme.md, "The trade-off".
+8 -29
View File
@@ -1,19 +1,7 @@
---
# Start Valkey
#
# Everything here runs as {{ podman_user }} — the connection user. No become, no
# XDG_RUNTIME_DIR: SSH gave us a real PAM session, so systemd's user manager is
# reachable via the standard environment.
# Created here rather than left to the container's entrypoint. That entrypoint
# runs as container-root, which under keep-id is a subuid on the host, so a
# directory it created would be owned by e.g. 165536 and the host user could not
# `rm -rf` it without `podman unshare`. Creating it up front keeps everything in
# the data root owned by {{ podman_user }}.
#
# Mode 0755 rather than Postgres's 0700: Valkey's entrypoint runs `find` in this
# directory as root before dropping to uid {{ app_uid }}, and nothing sensitive
# lives here — it is a cache.
# The entrypoint runs as a subuid under keep-id, so create the data directory
# here to keep it owned by podman_user rather than 165536.
- name: Ensure the Valkey data directory exists
ansible.builtin.file:
path: "{{ host_directory }}"
@@ -41,11 +29,8 @@
- Reload systemd user units
- Restart Valkey
# The wait is part of the block on purpose. `Restart=always` means systemd
# reports the unit active even while the container crash-loops — a wrong keep-id
# shows up as an entrypoint permission error over and over — so is-active alone
# would call a broken cache healthy, and every app that uses it would fail later
# with a DNS or connection error that points nowhere near the cause.
# `Restart=always` reports the unit active while the container crash-loops, so
# wait for a real PING instead of trusting is-active.
- name: Start Valkey and wait for it to answer
block:
- name: Start Valkey
@@ -55,8 +40,7 @@
state: started
daemon_reload: true
# valkey-cli is the server's own client and is guaranteed present, the same
# way pg_isready is in the Postgres image.
# valkey-cli ships in the image, so it is always available for the check.
- name: Wait for Valkey to answer PING
ansible.builtin.command: "podman exec {{ container_name }} valkey-cli ping"
register: valkey_ping
@@ -65,9 +49,7 @@
delay: 5
changed_when: false
rescue:
# systemd reports "control process exited with error code" and nothing else,
# which is useless for the usual failures (a bad uid mapping, an unwritable
# data directory, a missing image). Pull the real reason out.
# systemd only reports a generic exit code, so collect the real reason.
- name: Collect Valkey diagnostics
ansible.builtin.command: "{{ item }}"
loop:
@@ -85,8 +67,7 @@
ansible.builtin.fail:
msg: "Valkey did not come up — see the diagnostics above."
# Quadlet honours [Install] WantedBy= when it generates the unit, so
# `systemctl --user enable` is neither needed nor possible on a generated unit.
# Quadlet honours [Install] WantedBy=, so generated units cannot be enabled.
- name: Verify the Valkey unit is active
ansible.builtin.command: "systemctl --user is-active {{ unit_name }}"
register: valkey_active
@@ -116,9 +97,7 @@
{{ valkey_dropins.files | map(attribute='path') | map('basename') | join(', ')
| default('(nothing yet)', true) }}.
# Say this out loud rather than let it be discovered by a failing app: with no
# drop-in, Valkey has no Network= at all, so it sits on the default Podman
# network and no app network can resolve it.
# With no drop-in there is no Network=, so it sits on the default Podman network.
- name: Note that no service has attached yet
ansible.builtin.debug:
msg: >-
+9 -38
View File
@@ -1,58 +1,29 @@
---
# ---------------------------------------------------------------------------
# Rootless Podman / Quadlet
# ---------------------------------------------------------------------------
# Duplicated on purpose from caddy/vars/default.yml: a play cannot include that
# file wholesale, because it also sets `host_directory` — to the Caddy data
# directory — and every playbook defines that for itself. Keep these three in
# sync with caddy/vars/default.yml. (Its fourth, podman_unprivileged_port_start,
# belongs to the podman tasks, which this play does not run: Valkey publishes
# nothing host-side.)
#
# podman_user is derived from the connection: this play carries no become, so
# the Quadlet units belong to whoever Ansible connects as. Requires gather_facts
# (set in playbook.yml).
# A play cannot include caddy/vars/default.yml, which also sets host_directory,
# so keep these in sync with it. Set gather_facts: true for the facts below.
podman_user: "{{ ansible_facts['user_id'] }}"
podman_data_dir: "{{ ansible_facts['user_dir'] }}/podman-data"
quadlet_dir: "{{ ansible_facts['user_dir'] }}/.config/containers/systemd"
# ---------------------------------------------------------------------------
# Valkey
# ---------------------------------------------------------------------------
container_name: valkey
unit_name: "{{ container_name }}.service"
# Fully qualified on purpose: rootless Podman resolves short names against
# unqualified-search-registries, and there is no TTY to answer a prompt when the
# systemd unit starts. The Docker-era playbook said `valkey/valkey:alpine`.
# Fully qualified, because short names prompt for a registry and the unit has
# no TTY.
image: docker.io/valkey/valkey:alpine
host_directory: "{{ podman_data_dir }}/{{ container_name }}"
# The image's WORKDIR and VOLUME: the RDB snapshot and, if it is ever enabled,
# the AOF live here. The contents are disposable — nothing needs to be copied
# off monoco — but the mount is not: RDB persistence is on by default, and the
# graceful stop of a rebuild writes a final snapshot here that the next
# container loads. Keep it unless you also set `save ""`. See readme.md, "The
# trade-off".
# The image volume holds the RDB snapshot. A graceful stop writes a final one
# here, so keep the mount unless you also set `save ""`.
data_mount: /data
# The uid and gid of the image's `valkey` account. The uid is 999 — the one the
# anonymous Docker volume was owned by (docs/podman-compat-audit.md §3.2) — but
# the gid is *1000*, not 999: Alpine already has a gid 999, and the image's
# Dockerfile says so outright ("alpine already has a gid 999, so we'll use the
# next id") before dropping to the account with `setpriv --regid=valkey`.
# Mapping both ids onto podman_user is what keeps {{ host_directory }} — owner
# and group alike — with the connection user, instead of stranding the group on
# a subgid the moment valkey-server writes a snapshot.
# The image account is uid 999 and gid 1000, not 999: Alpine already has a gid
# 999. Mapping both keeps host_directory owned by podman_user.
app_uid: 999
app_gid: 1000
# No PublishPort, on purpose: the Docker-era playbook published 127.0.0.1:6379,
# and nothing on the host needs it. Clients reach Valkey over the app networks
# that attach themselves; {{ container_port }} is the port inside the container.
# No PublishPort: clients reach Valkey over the app networks, not the host.
container_port: 6379
# Quadlet drop-in directory for the Valkey unit. Services attach themselves
# here; see the "How services attach themselves" section of readme.md.
valkey_dropin_dir: "{{ quadlet_dir }}/{{ container_name }}.container.d"