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]>
112 lines
4.6 KiB
YAML
112 lines
4.6 KiB
YAML
---
|
|
# 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
|