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,53 @@
|
||||
---
|
||||
# The variable contract. Playbooks set these; nothing else should. Every entry is
|
||||
# documented in AGENTS.md — keep the two in step.
|
||||
|
||||
# Required. Names the stack, and by default its payload and destination too.
|
||||
stack_name: ""
|
||||
|
||||
# Payload directory in this repository, and where it lands on the host. Override
|
||||
# stack_src to share one payload between hosts or between two instances of the
|
||||
# same application; override stack_dest when the remote directory cannot be
|
||||
# named after the stack.
|
||||
stack_src: "{{ repo_root }}/server/{{ inventory_hostname }}/{{ stack_name }}"
|
||||
stack_dest: "{{ stack_root | default('/srv/stacks') }}/{{ stack_name }}"
|
||||
|
||||
# Extra directories to create before the stack starts. Each entry needs a `path`;
|
||||
# `mode`, `owner`, `group` and `recurse` are optional and applied only when
|
||||
# given, so a directory that already exists on the host is never re-chmodded or
|
||||
# re-chowned.
|
||||
stack_dirs: []
|
||||
|
||||
# Extra environment for the compose invocation. PWD is always set to stack_dest;
|
||||
# anything here is merged on top.
|
||||
stack_env: {}
|
||||
|
||||
# External Docker networks the compose file expects to already exist.
|
||||
stack_networks: []
|
||||
|
||||
# Compose files, relative to stack_dest, in the order `docker compose -f` wants.
|
||||
stack_files:
|
||||
- docker-compose.yml
|
||||
|
||||
# Never ship secrets to the host through the payload. Extend this list; do not
|
||||
# replace it without understanding what you are letting through.
|
||||
stack_exclude:
|
||||
- .env
|
||||
|
||||
# Whether the sync removes host files that are no longer in the payload. Off by
|
||||
# default: a stack's directory usually also holds data the payload knows nothing
|
||||
# about (bind mounts, certificates, logs).
|
||||
stack_prune: false
|
||||
|
||||
# present, absent, or restarted.
|
||||
stack_state: present
|
||||
|
||||
# Image pull policy for this invocation. Defaults to the repo-wide `pull` flag so
|
||||
# `-e pull=always` keeps working, but because it is a role variable a playbook
|
||||
# can override it for a single call — for example to sync with the old images,
|
||||
# run a database upgrade, and only then bring the stack up on the new ones.
|
||||
stack_pull: "{{ pull | default('policy') }}"
|
||||
|
||||
# Compose build policy. `policy` builds only when the image is missing; stacks
|
||||
# that build from a Dockerfile in their payload want `always` on an update run.
|
||||
stack_build: policy
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
galaxy_info:
|
||||
role_name: compose_stack
|
||||
namespace: example
|
||||
author: The contributors
|
||||
description: Sync a Docker Compose stack payload to a host and bring the stack up.
|
||||
license: MIT
|
||||
min_ansible_version: "2.15"
|
||||
platforms:
|
||||
- name: EL
|
||||
versions:
|
||||
- "9"
|
||||
- name: Debian
|
||||
versions:
|
||||
- bookworm
|
||||
- name: Ubuntu
|
||||
versions:
|
||||
- jammy
|
||||
- noble
|
||||
galaxy_tags:
|
||||
- docker
|
||||
- compose
|
||||
- deployment
|
||||
|
||||
dependencies: []
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
# Two entry points, so a playbook that must do maintenance between the payload
|
||||
# landing and the stack starting can interleave its own tasks:
|
||||
#
|
||||
# - ansible.builtin.include_role:
|
||||
# name: compose_stack
|
||||
# tasks_from: sync
|
||||
# - name: Run the database migration
|
||||
# ...
|
||||
# - ansible.builtin.include_role:
|
||||
# name: compose_stack
|
||||
# tasks_from: up
|
||||
#
|
||||
# Using the role normally (roles: - role: compose_stack) runs both, in order.
|
||||
# playbooks/webapp.yml is the worked example.
|
||||
- name: Sync the payload to the host
|
||||
ansible.builtin.include_tasks: sync.yml
|
||||
|
||||
- name: Start the stack
|
||||
ansible.builtin.include_tasks: up.yml
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
- name: Verify the caller named a stack
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- stack_name | length > 0
|
||||
fail_msg: compose_stack requires stack_name
|
||||
quiet: true
|
||||
|
||||
- name: Look for the payload on the control node
|
||||
ansible.builtin.stat:
|
||||
path: "{{ stack_src }}"
|
||||
register: compose_stack_payload
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
changed_when: false
|
||||
|
||||
# A missing payload otherwise surfaces as an empty sync that reports success and
|
||||
# leaves the host running whatever was there before.
|
||||
- name: Verify the payload directory exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- compose_stack_payload.stat.isdir | default(false)
|
||||
fail_msg: "no payload directory at {{ stack_src }}"
|
||||
quiet: true
|
||||
|
||||
- name: Create the stack directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ stack_dest }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Create the extra directories the stack needs
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: directory
|
||||
# All four default to omit: create the directory if it is absent, and leave
|
||||
# whatever is already on the host alone. Bind mounts are routinely created
|
||||
# root-owned by Docker on first start, and re-chmodding those breaks them.
|
||||
mode: "{{ item.mode | default(omit) }}"
|
||||
owner: "{{ item.owner | default(omit) }}"
|
||||
group: "{{ item.group | default(omit) }}"
|
||||
# For a bind mount whose existing tree is owned by the wrong uid. Set it with
|
||||
# `owner`/`group` and no `mode`: recursing a mode over live data rewrites the
|
||||
# permissions of everything the container has already written there.
|
||||
recurse: "{{ item.recurse | default(omit) }}"
|
||||
# Escalate only to set an owner or group. Creating these as root would defeat
|
||||
# the point of pre-creating them at all — callers do it precisely so the bind
|
||||
# mount is *not* root-owned when Docker first starts the stack. Unescalated
|
||||
# they land as the deploy user, which is the user Compose runs as.
|
||||
become: "{{ item.owner is defined or item.group is defined }}"
|
||||
loop: "{{ stack_dirs }}"
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
|
||||
# Deliberately the docker CLI rather than community.docker.docker_network: that
|
||||
# module needs the Docker SDK for Python installed on every host, and creating a
|
||||
# network is the only thing this repository would need it for.
|
||||
- name: Create the external Docker networks
|
||||
ansible.builtin.command:
|
||||
cmd: docker network create {{ item }}
|
||||
register: compose_stack_network
|
||||
changed_when: compose_stack_network.rc == 0
|
||||
failed_when:
|
||||
- compose_stack_network.rc != 0
|
||||
- '"already exists" not in compose_stack_network.stderr'
|
||||
loop: "{{ stack_networks }}"
|
||||
|
||||
- name: Sync the payload to the host
|
||||
ansible.posix.synchronize:
|
||||
src: "{{ stack_src }}/"
|
||||
dest: "{{ stack_dest }}/"
|
||||
delete: "{{ stack_prune }}"
|
||||
rsync_opts: "{{ stack_exclude | map('regex_replace', '^', '--exclude=') | list }}"
|
||||
register: compose_stack_sync
|
||||
|
||||
# Bind-mounted configuration changes without the compose file changing, so
|
||||
# docker_compose_v2 will not recreate the container and the new file has no
|
||||
# effect. Playbooks key a reload task off this fact — see
|
||||
# playbooks/reverse-proxy.yml.
|
||||
- name: Record whether the payload changed
|
||||
ansible.builtin.set_fact:
|
||||
compose_stack_synced: "{{ compose_stack_sync.changed }}"
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
- name: Bring the stack up
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ stack_dest }}"
|
||||
files: "{{ stack_files }}"
|
||||
state: "{{ stack_state }}"
|
||||
pull: "{{ stack_pull }}"
|
||||
build: "{{ stack_build }}"
|
||||
remove_orphans: true
|
||||
# A compose file that interpolates ${PWD} — a common way to hand a host path to
|
||||
# a sibling container — resolves it against the *invoking shell*, not the
|
||||
# project directory. The module sets the process working directory but not PWD,
|
||||
# so ${PWD} would silently become the SSH login directory. Set it explicitly.
|
||||
environment: "{{ {'PWD': stack_dest} | combine(stack_env) }}"
|
||||
Reference in New Issue
Block a user