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:
1 parent
2aab1aec1e
commit
476a6d7cd0
85 files changed
+372
-2433
No files matched your search
+2
-39
@@ -1,28 +1,5 @@
|
||||
---
|
||||
# Yamtrack as a rootless Podman Quadlet unit, behind the shared Caddy unit
|
||||
# (caddy/playbook.yml), with its cache in the shared Valkey unit
|
||||
# (valkey/playbook.yml). The database stays SQLite, inside the app's own data
|
||||
# directory — see readme.md, "The database stays SQLite".
|
||||
#
|
||||
# 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 this play needs are duplicated at the top of vars/default.yml, so
|
||||
# it stands on its own.
|
||||
#
|
||||
# Run with: ansible-playbook -u tanshu yamtrack/playbook.yml
|
||||
# Requires caddy and valkey to have run first. The database is SQLite.
|
||||
- name: Deploy Yamtrack as a rootless Podman Quadlet
|
||||
hosts: emily
|
||||
gather_facts: true
|
||||
@@ -30,25 +7,11 @@
|
||||
- vars/default.yml
|
||||
|
||||
pre_tasks:
|
||||
# This must happen before the app unit is started, not after: the generated
|
||||
# unit references {{ network_name }}.network, and Quadlet fails *unit
|
||||
# generation* (so `systemctl --user start` reports "unit not found") when a
|
||||
# referenced .network file does not exist yet. The same include writes the
|
||||
# Caddy-side drop-in that attaches Caddy to this network, and the Valkey
|
||||
# attach below needs the network unit to exist as well.
|
||||
#
|
||||
# network_name is a play var, so the include can see it directly.
|
||||
# Quadlet needs the network unit before it generates the app unit.
|
||||
- name: Register Yamtrack's network with Caddy
|
||||
ansible.builtin.include_tasks: "{{ playbook_dir }}/../caddy/roles/register/tasks/main.yaml"
|
||||
|
||||
roles:
|
||||
# Attaches this app's network to the shared Valkey unit, which is what makes
|
||||
# REDIS_URL's host resolve, and owns the "Restart Valkey" handler that
|
||||
# valkey/roles/attach notifies. Runs first so the cache is reachable before
|
||||
# the app starts.
|
||||
- valkey
|
||||
# The app: data directory, the 0600 environment file, the Quadlet unit,
|
||||
# start + verify.
|
||||
- yamtrack
|
||||
# Appends this site's block to the shared Caddyfile and reloads Caddy.
|
||||
- caddy
|
||||
@@ -1,27 +1,15 @@
|
||||
---
|
||||
# Caddy is its own playbook (caddy/playbook.yml) and a rootless Quadlet unit
|
||||
# there. These handlers run as the connection user, so `systemctl --user` and
|
||||
# `podman exec` work without any XDG_RUNTIME_DIR juggling.
|
||||
|
||||
# Notified by this role when the Caddyfile changes. A reload keeps existing
|
||||
# connections; a restart would briefly drop every hosted site.
|
||||
#
|
||||
# Note the config path: /etc/caddy/Caddyfile is the mount inside the container,
|
||||
# not {{ caddyfile_path }}, which is the host side of it.
|
||||
# A reload keeps live connections; a restart would drop every hosted site.
|
||||
# The path is inside the container, not the host {{ caddyfile_path }}.
|
||||
- name: Reload Caddy configuration
|
||||
ansible.builtin.command: >-
|
||||
podman exec {{ caddy_name }} caddy reload --config /etc/caddy/Caddyfile
|
||||
changed_when: true
|
||||
listen: "Reload Caddy"
|
||||
|
||||
# Notified by caddy/roles/register when this service's network unit, or the
|
||||
# drop-in that attaches Caddy to it, is written. Quadlet has no live equivalent
|
||||
# of `docker network connect`, so the container has to be recreated — a brief
|
||||
# restart of every hosted site, not just this one. See caddy/readme.md,
|
||||
# "Trade-off worth knowing".
|
||||
#
|
||||
# Defined after the reload above so that when both are notified the cheap
|
||||
# config reload happens first.
|
||||
# Quadlet cannot attach a network live, so Caddy must be recreated.
|
||||
# Defined after the reload so the cheap reload runs first when both are notified.
|
||||
- name: Restart Caddy
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ caddy_name }}.service"
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
---
|
||||
# Append this site's block to the shared Caddyfile.
|
||||
#
|
||||
# The Caddyfile is a bind mount into the Caddy container, so Caddy sees the
|
||||
# change as soon as the file is written; the handler only asks Caddy to re-read
|
||||
# it. That is a reload, not a restart, so the other hosted sites are untouched.
|
||||
#
|
||||
# blockinfile is idempotent on its own via the marker, so there is no need to
|
||||
# slurp the file and compare first — which also matters because slurp *fails*
|
||||
# when the Caddyfile does not exist yet, on a host where the Caddy playbook has
|
||||
# not run. (The Docker-era version of this role did exactly that.)
|
||||
|
||||
- name: "Add the {{ host }} site block to the Caddyfile"
|
||||
ansible.builtin.blockinfile:
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
---
|
||||
# Valkey is its own playbook (valkey/playbook.yml) and a rootless Quadlet unit
|
||||
# there. This handler runs as the connection user, so `systemctl --user` works
|
||||
# without setting XDG_RUNTIME_DIR.
|
||||
#
|
||||
# Required by valkey/roles/attach, which notifies it whenever this app's drop-in
|
||||
# changes: a new drop-in changes the generated unit, so Valkey must be recreated
|
||||
# to join the network — Quadlet has no live equivalent of
|
||||
# `podman network connect`.
|
||||
#
|
||||
# The cost is a cold cache for every attached app, not lost data — but note that
|
||||
# attached means every cache client, not just Yamtrack. See valkey/readme.md,
|
||||
# "The trade-off".
|
||||
# A new drop-in changes the generated unit, so Valkey must be recreated to join the network.
|
||||
# That restarts every attached client, not just Yamtrack.
|
||||
- name: Restart Valkey
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ valkey_unit }}"
|
||||
|
||||
@@ -1,31 +1,10 @@
|
||||
---
|
||||
# Attach this app's network to the shared Valkey unit.
|
||||
#
|
||||
# Valkey knows nothing about its clients, exactly like Caddy and Postgres: a
|
||||
# service joins by dropping 50-yamtrack.conf and the shared
|
||||
# 10-network-aliases.conf next to valkey.container. The canonical way to do that
|
||||
# is the shared include below — it writes the same bytes every service writes,
|
||||
# asserts that yamtrack.network already exists, and notifies the "Restart Valkey"
|
||||
# handler defined in this role.
|
||||
#
|
||||
# `valkey` is the host REDIS_URL asks for — the container name, not the shared
|
||||
# `redis` alias, which is there for the clients whose config.php says `redis`.
|
||||
# The Docker-era container was given `redis://valkey:6379` too, so the URL is
|
||||
# unchanged.
|
||||
#
|
||||
# This runs in the roles phase, which is after the register include in
|
||||
# playbook.yml's pre_tasks has created yamtrack.network.
|
||||
|
||||
# Runs in the roles phase, after the register include in pre_tasks created the network.
|
||||
- name: Attach Yamtrack to Valkey
|
||||
ansible.builtin.include_tasks: "{{ playbook_dir }}/../valkey/roles/attach/tasks/main.yaml"
|
||||
|
||||
# On a first attach the drop-in is new, so Valkey has to be recreated to join
|
||||
# yamtrack_net — and that restart is a *handler*, which by default runs at the
|
||||
# end of the play, after the yamtrack role has already started the app. The app
|
||||
# would then come up before `valkey` resolves, fail its own checks, and fail the
|
||||
# play on a machine where nothing is actually wrong.
|
||||
#
|
||||
# Flushing applies the new drop-in before the app starts. On later runs the
|
||||
# drop-in is unchanged, nothing is pending, and this is a no-op.
|
||||
# The restart is a handler, so it would run after the yamtrack role starts the app.
|
||||
# Flushing applies the new drop-in before the app starts; later runs are a no-op.
|
||||
- name: Apply the Valkey network change before starting Yamtrack
|
||||
ansible.builtin.meta: flush_handlers
|
||||
@@ -1,16 +1,11 @@
|
||||
---
|
||||
# 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 changed environment file or unit file changes the generated unit, so
|
||||
# Yamtrack is recreated. The database is a bind mount and the rest of /yamtrack
|
||||
# is image content, so a recreate loses nothing — but it is a few seconds of
|
||||
# downtime for the one site.
|
||||
# A recreate keeps state in the bind mount but costs a few seconds of downtime.
|
||||
- name: Restart Yamtrack
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ unit_name }}"
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
---
|
||||
# Yamtrack as a rootless Podman Quadlet unit.
|
||||
#
|
||||
# Everything here runs as {{ podman_user }} — the connection user. No become, no
|
||||
# XDG_RUNTIME_DIR: the SSH connection gave us a real PAM session, so systemd's
|
||||
# user manager is reachable through the standard environment.
|
||||
|
||||
# /yamtrack/db is the image's state directory: db.sqlite3 plus its -wal/-shm
|
||||
# sidecars, and nothing else worth keeping — staticfiles/ is built into the
|
||||
# image and the code is read-only.
|
||||
# /yamtrack/db holds db.sqlite3 and its WAL sidecars; everything else is image content.
|
||||
- name: Ensure the Yamtrack data directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ data_dir }}"
|
||||
@@ -16,9 +9,7 @@
|
||||
group: "{{ podman_user }}"
|
||||
mode: '0755'
|
||||
|
||||
# The app's secrets and cache coordinates are consumed by systemd
|
||||
# (EnvironmentFile= in the Quadlet unit), not by Ansible, so this has to be
|
||||
# written before the unit starts. Mode 0600: it holds SECRET.
|
||||
# Written before the unit starts; 0600 keeps SECRET out of the world-readable unit.
|
||||
- name: Write the environment file
|
||||
ansible.builtin.template:
|
||||
src: yamtrack.env.j2
|
||||
@@ -48,9 +39,7 @@
|
||||
state: started
|
||||
daemon_reload: true
|
||||
rescue:
|
||||
# systemd reports "control process exited with error code" and nothing
|
||||
# else, which is useless for the usual failures (an unattached network, an
|
||||
# unreachable cache, a bad volume). Pull the real reason out.
|
||||
# systemd reports only a generic exit error, so collect the real reason.
|
||||
- name: Collect Yamtrack diagnostics
|
||||
ansible.builtin.command: "{{ item }}"
|
||||
loop:
|
||||
@@ -69,22 +58,15 @@
|
||||
ansible.builtin.fail:
|
||||
msg: "Yamtrack did not start — 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.
|
||||
#
|
||||
# `is-active` says the container is running; it does not say the app can serve.
|
||||
# The entrypoint runs migrations before supervisord starts nginx, gunicorn,
|
||||
# celery and beat, and `Restart=always` means even a crash-looping container is
|
||||
# reported active — so the checks below are the ones that matter.
|
||||
# Quadlet honours WantedBy=, so systemctl enable is neither needed nor possible on a generated unit.
|
||||
# is-active still reports active for a crash-looping container, so the checks below matter.
|
||||
- name: Verify Yamtrack is running
|
||||
ansible.builtin.command: "systemctl --user is-active {{ unit_name }}"
|
||||
register: yamtrack_active
|
||||
changed_when: false
|
||||
failed_when: yamtrack_active.stdout != 'active'
|
||||
|
||||
# `valkey` is the host the environment file actually asks for; it resolves from
|
||||
# ContainerName= on the shared unit, which valkey/roles/attach joined to this
|
||||
# app's network.
|
||||
# The app expects the valkey host; attach joined the shared unit to this app's network.
|
||||
- name: Confirm Yamtrack can resolve its cache
|
||||
ansible.builtin.command: >-
|
||||
podman exec {{ container_name }} getent hosts {{ valkey_container }}
|
||||
@@ -98,19 +80,9 @@
|
||||
{{ valkey_container }} resolves as
|
||||
{{ yamtrack_cache_lookup.stdout | default('(no answer)') }}
|
||||
|
||||
# `is-active` says the container is running; the image's own HEALTHCHECK is what
|
||||
# says the app works. It wgets http://127.0.0.1:8000/health/, and that view
|
||||
# checks all four things this deployment can get wrong — the cache, the database,
|
||||
# Redis and Celery — so a green result here is the app being genuinely ready.
|
||||
#
|
||||
# Running the image's check rather than one written here means depending on
|
||||
# nothing the play has to know: no curl or wget wrapper of our own, and no
|
||||
# hardcoded path that a later image tag could move. `podman exec <c> wget -qO-
|
||||
# http://127.0.0.1:{{ container_port }}/health/` prints the per-check breakdown
|
||||
# when this fails; see readme.md, "Verification".
|
||||
#
|
||||
# The start period (30s) plus the entrypoint's migrations are why this allows
|
||||
# three minutes rather than one.
|
||||
# The image's own HEALTHCHECK covers the cache, database, Redis and Celery, so a pass means ready.
|
||||
# Running the image's check means the play depends on no path or tool of its own.
|
||||
# The start period plus migrations is why this allows three minutes.
|
||||
- name: Wait for Yamtrack to pass its own health check
|
||||
ansible.builtin.command: "podman healthcheck run {{ container_name }}"
|
||||
register: yamtrack_health
|
||||
|
||||
+12
-71
@@ -1,109 +1,50 @@
|
||||
---
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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: Yamtrack listens on
|
||||
# 8000 inside the container and 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).
|
||||
# Derived from the connection user; requires gather_facts.
|
||||
podman_user: "{{ ansible_facts['user_id'] }}"
|
||||
podman_data_dir: "{{ ansible_facts['user_dir'] }}/podman-data"
|
||||
quadlet_dir: "{{ ansible_facts['user_dir'] }}/.config/containers/systemd"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Yamtrack
|
||||
# ---------------------------------------------------------------------------
|
||||
host: track.tanshu.com
|
||||
|
||||
container_name: yamtrack
|
||||
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. ghcr.io was already qualified; the tag is explicit
|
||||
# for the same reason. Nothing is pinned — see readme.md, "Notes and gotchas".
|
||||
# Fully qualified so rootless Podman never prompts for a short name; there is no TTY.
|
||||
image: ghcr.io/fuzzygrim/yamtrack:latest
|
||||
|
||||
# The Quadlet file is <network_name>.network. caddy/roles/register pins the
|
||||
# Podman network name to <network_name>_net (so: yamtrack.network, yamtrack_net,
|
||||
# which is the name the Docker-era deployment used).
|
||||
# The Quadlet file is <network_name>.network; register pins <network_name>_net.
|
||||
network_name: "{{ container_name }}"
|
||||
|
||||
host_directory: "{{ podman_data_dir }}/{{ container_name }}"
|
||||
|
||||
# The image's bind mount: /yamtrack/db holds db.sqlite3 and its WAL sidecars.
|
||||
# Nothing else on /yamtrack is state — staticfiles/ is baked into the image and
|
||||
# the code is read-only — so this is the only directory worth keeping.
|
||||
# /yamtrack/db holds db.sqlite3 and its WAL sidecars; nothing else on /yamtrack is state.
|
||||
data_dir: "{{ host_directory }}/data"
|
||||
|
||||
# The port inside the container that Caddy proxies to: the image's own nginx
|
||||
# listens there and forwards to gunicorn. Reproduces the Docker-era Caddyfile
|
||||
# entry (`reverse_proxy yamtrack:8000`) exactly.
|
||||
# The image's nginx listens on 8000 and forwards to gunicorn.
|
||||
container_port: 8000
|
||||
|
||||
# SECRET, TZ and REDIS_URL live here rather than as Environment= lines in the
|
||||
# unit file: that file is world-readable in ~/.config. Read by systemd through
|
||||
# EnvironmentFile=, not by Ansible, so it has to be written before the unit
|
||||
# starts.
|
||||
# The unit is world-readable, so secrets go in this 0600 file.
|
||||
env_file: "{{ host_directory }}/{{ container_name }}.env"
|
||||
|
||||
# The uid this unit maps onto podman_user, and the PUID/PGID the image's
|
||||
# entrypoint is given. It is 0 — container root — and that is deliberate:
|
||||
#
|
||||
# 1. `--userns=keep-id` does two things: it maps one container uid onto the
|
||||
# connecting user, and it runs the container *as that uid*. The entrypoint's
|
||||
# first act is `groupmod`/`usermod`, which rewrite /etc/group and
|
||||
# /etc/passwd — files owned by container-root in the image. With
|
||||
# `keep-id:uid=1000` those run as `abc` and die on "cannot lock /etc/group"
|
||||
# (the unit exits 10) before supervisord is ever reached. Root is required.
|
||||
# 2. Mapping container root onto podman_user is what keeps
|
||||
# ~/podman-data/yamtrack owned by podman_user rather than by a subuid. It is
|
||||
# the same choice caddy, bitwarden and tandoor make.
|
||||
#
|
||||
# It is also rendered as PUID/PGID in the environment file: the entrypoint's own
|
||||
# default is 1000, which under this mapping is a subuid on the host, so its
|
||||
# `chown -R abc:abc db` would strand the database on an unmapped uid. See
|
||||
# readme.md, "Who runs as whom".
|
||||
# It is 0 because the entrypoint rewrites /etc/group and /etc/passwd as container root.
|
||||
# keep-id:uid=1000 would run that as abc and exit before supervisord starts.
|
||||
# Mapping root onto podman_user keeps the data owned by podman_user, not a subuid.
|
||||
app_uid: 0
|
||||
app_gid: 0
|
||||
|
||||
yamtrack_tz: "Asia/Kolkata"
|
||||
|
||||
# The Django SECRET_KEY, as the image calls it (`SECRET`). Carried over verbatim
|
||||
# from the Docker-era playbook so existing sessions and password-reset tokens
|
||||
# stay valid — rotating it logs every user out. Committed in plaintext as
|
||||
# before; flagged as a follow-up, not changed here.
|
||||
# Carried over so existing sessions and password-reset tokens stay valid.
|
||||
yamtrack_secret: "c14c5970eb728fbf8b2325fd3004069d"
|
||||
|
||||
# What the old container passed as URLS, which Yamtrack turns into its CSRF
|
||||
# trusted origins. ALLOWED_HOSTS is deliberately not set: its default is `*`,
|
||||
# which is what the Docker-era container relied on.
|
||||
# ALLOWED_HOSTS is deliberately unset; its default is *.
|
||||
urls: "https://{{ host }}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Valkey (the shared cache / Celery broker)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Yamtrack hard-requires Redis: its cache backend is django_redis with no
|
||||
# fallback, the Celery worker and beat need a broker, and several metadata
|
||||
# providers talk to Redis directly. It is a *separate* playbook, deployed before
|
||||
# this one: yamtrack/roles/valkey only attaches its network to that shared unit.
|
||||
#
|
||||
# The Docker-era container was given `redis://valkey:6379`, and valkey is what
|
||||
# the container is called, so the URL is unchanged.
|
||||
# The container is called valkey, so the Redis URL is unchanged.
|
||||
valkey_container: valkey
|
||||
valkey_unit: "{{ valkey_container }}.service"
|
||||
redis_url: "redis://{{ valkey_container }}:6379"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Caddy
|
||||
# ---------------------------------------------------------------------------
|
||||
# The shared Caddy unit and its Caddyfile, both owned by caddy/playbook.yml.
|
||||
caddy_name: caddy
|
||||
caddyfile_path: "{{ podman_data_dir }}/{{ caddy_name }}/conf/Caddyfile"
|
||||
|
||||
|
||||
Reference in new issue
Block a user