Add the compose_stack role, inventory and playbooks
One role that syncs a payload and brings the stack up, configured entirely through stack_* variables, plus six playbooks that each demonstrate one part of that contract. Every playbook runs standalone and is tagged with its stack name, so site.yml --tags <stack> works. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7cdf9b854b
commit
7b52e9af9e
@@ -0,0 +1,111 @@
|
||||
---
|
||||
# The stack that needs more than "sync, then up".
|
||||
#
|
||||
# Demonstrates the whole heavy end of the contract: an assertion that a host-side
|
||||
# .env exists, a payload built from a Dockerfile, a second compose file holding
|
||||
# maintenance-only services, and work that has to happen *between* the payload
|
||||
# landing and the stack starting.
|
||||
#
|
||||
# The shape to copy is the two entry points. `roles: - role: compose_stack` runs
|
||||
# sync and then up, which is what almost every stack wants. When something has to
|
||||
# happen in between, include the role twice with tasks_from — do not invent a
|
||||
# second mechanism, and do not write a separate *-update.yml playbook.
|
||||
- name: Deploy webapp
|
||||
hosts: app
|
||||
gather_facts: false
|
||||
tags: [webapp]
|
||||
|
||||
vars:
|
||||
webapp_dir: "{{ stack_root }}/webapp"
|
||||
# Both compose files, in the order `docker compose -f` wants. Only the
|
||||
# maintenance runs below use this; see the note on stack_files.
|
||||
webapp_maintenance_compose: >-
|
||||
-f docker-compose.yml -f docker-compose.migrate.yml
|
||||
# The single flag that separates a deploy from an update. Read it through
|
||||
# `default` at every use site so the playbook still runs when `pull` is
|
||||
# undefined.
|
||||
webapp_updating: "{{ (pull | default('policy')) == 'always' }}"
|
||||
|
||||
# The compose_stack contract, declared at play level so that both the sync
|
||||
# phase and the up phase below see the same values. Inside `roles:` they
|
||||
# would be scoped to the role invocation and the second include would not
|
||||
# see them.
|
||||
stack_name: webapp
|
||||
stack_dest: "{{ webapp_dir }}"
|
||||
stack_networks:
|
||||
- proxy-net
|
||||
# Deliberately only docker-compose.yml. docker-compose.migrate.yml adds
|
||||
# one-shot `run --rm` targets; bringing the stack up with both files would
|
||||
# start them as long-running services.
|
||||
stack_files:
|
||||
- docker-compose.yml
|
||||
# Rebuild the image only on an update run. Rebuilding on every deploy would
|
||||
# report a change every time and quietly break idempotency.
|
||||
stack_build: "{{ 'always' if webapp_updating else 'policy' }}"
|
||||
stack_dirs:
|
||||
# Bind mounts Docker would otherwise create root-owned. ./pgdata is
|
||||
# deliberately absent: Postgres refuses to start unless its data directory
|
||||
# is 0700 or 0750, so it must be left to the image.
|
||||
- path: "{{ stack_root }}/webapp/uploads"
|
||||
- path: "{{ stack_root }}/webapp/backups"
|
||||
|
||||
pre_tasks:
|
||||
# Every variable in this stack's compose files comes from .env with no
|
||||
# defaults, and the role never ships .env to a host. Fail loudly and say what
|
||||
# to do, rather than starting a stack with empty credentials.
|
||||
- name: Look for the webapp .env on the host
|
||||
ansible.builtin.stat:
|
||||
path: "{{ webapp_dir }}/.env"
|
||||
register: webapp_env
|
||||
changed_when: false
|
||||
|
||||
- name: Verify the webapp .env is present on the host
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- webapp_env.stat.exists
|
||||
fail_msg: >-
|
||||
{{ webapp_dir }}/.env is missing on {{ inventory_hostname }}. webapp
|
||||
reads every credential from it and it is never synced from this
|
||||
repository; copy .env.example on the host and fill it in, or restore
|
||||
it from your secret store.
|
||||
quiet: true
|
||||
|
||||
tasks:
|
||||
# Phase 1: deliver the payload, including the current Dockerfile and
|
||||
# migration script, without starting anything.
|
||||
- name: Sync the webapp payload
|
||||
ansible.builtin.include_role:
|
||||
name: compose_stack
|
||||
tasks_from: sync
|
||||
|
||||
# Phase 2: maintenance, against the payload that was just delivered and only
|
||||
# on an update run. An ordinary deploy must never back up or migrate
|
||||
# anything — see "Deploy versus update" in the README.
|
||||
- name: Build the webapp maintenance images
|
||||
ansible.builtin.command:
|
||||
cmd: docker compose {{ webapp_maintenance_compose }} build
|
||||
chdir: "{{ webapp_dir }}"
|
||||
changed_when: true
|
||||
when: webapp_updating
|
||||
|
||||
- name: Back up the webapp database before migrating
|
||||
ansible.builtin.command:
|
||||
cmd: docker compose {{ webapp_maintenance_compose }} run --rm backup
|
||||
chdir: "{{ webapp_dir }}"
|
||||
changed_when: true
|
||||
when: webapp_updating
|
||||
|
||||
- name: Migrate the webapp database
|
||||
# app/migrate.sh is idempotent by design, so repeating an update run after
|
||||
# a failure elsewhere is safe.
|
||||
ansible.builtin.command:
|
||||
cmd: docker compose {{ webapp_maintenance_compose }} run --rm migrate
|
||||
chdir: "{{ webapp_dir }}"
|
||||
changed_when: true
|
||||
when: webapp_updating
|
||||
|
||||
# Phase 3: start the stack, on the new images if any were pulled or built.
|
||||
- name: Start webapp
|
||||
ansible.builtin.include_role:
|
||||
name: compose_stack
|
||||
tasks_from: up
|
||||
Reference in New Issue
Block a user