Point at the playbook's new home
The playbook now lives in ansible-playbooks/menu-admin/ with the other playbooks; a second copy here would be edited by mistake.
This commit is contained in:
1 parent
1f06e32854
commit
f89bd63685
9 files changed
+4
-293
No files matched your search
@@ -40,8 +40,12 @@ config/ Per-Site config, one file per Site
|
||||
test/ node:test suites
|
||||
docs/plan.md The agreed plan
|
||||
docs/adr/ Decisions worth remembering
|
||||
deploy.sh Tests, build, push
|
||||
```
|
||||
|
||||
The Ansible playbook lives in `ansible-playbooks/menu-admin/`, beside the other
|
||||
playbooks, because that is where it is run from.
|
||||
|
||||
## Working on it
|
||||
|
||||
Node 24 or newer runs the TypeScript sources directly.
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
# Deploys the menu app for one Site. Copy this directory to
|
||||
# ansible-playbooks/menu-admin/ and run it from there:
|
||||
#
|
||||
# ansible-playbook -l monoco -e site=hopsngrains -e tag=<tag> playbook.yml
|
||||
#
|
||||
# The first run targets a temporary vhost (menu-new...) so the new site can be
|
||||
# checked against the live one. Cutover is the same playbook with the real vhost
|
||||
# and the two retire flags set; see readme.md.
|
||||
- hosts: all
|
||||
become: true
|
||||
vars_files:
|
||||
- vars/default.yml
|
||||
- "vars/{{ site }}.yml"
|
||||
|
||||
roles:
|
||||
- network
|
||||
- menu-app
|
||||
- caddy
|
||||
@@ -1,81 +0,0 @@
|
||||
# menu-admin playbook
|
||||
|
||||
Deploys the menu app for one Site, beside the existing nginx container at first
|
||||
and in place of it after cutover. Copy this directory to
|
||||
`ansible-playbooks/menu-admin/` and run it from there, so `playbook_dir` and the
|
||||
inventory match the other playbooks.
|
||||
|
||||
This playbook owns the `menu-app` container and the Site's snippet in the shared
|
||||
Caddyfile. It does not touch the app's source, the image, or the other Sites'
|
||||
containers.
|
||||
|
||||
## What lives where
|
||||
|
||||
| Thing | Where |
|
||||
| --- | --- |
|
||||
| Sections, brand, theme | `config/<site>.site.json` in the app repo, baked into the image |
|
||||
| Menu content, Drafts, Originals | `/var/lib/menu-app.<site>/data` on the host |
|
||||
| Admin password hash, session secret | `/var/lib/menu-app.<site>/site.env` (untracked, mode 600) |
|
||||
| Which image tag runs | `-e tag=<tag>` |
|
||||
|
||||
Sections are deliberately not editable on the server: changing them is a commit
|
||||
to the app repo, a new image, and a redeploy (ADR 0005).
|
||||
|
||||
## Deploying
|
||||
|
||||
```sh
|
||||
# 1. Build and push from the app repo
|
||||
./deploy.sh 2026-09-22 # -> registry.tanshu.com/menu-admin:2026-09-22
|
||||
|
||||
# 2. First run: a temporary hostname, so the live site keeps serving
|
||||
ansible-playbook -l monoco -e site=hopsngrains -e tag=2026-09-22 playbook.yml
|
||||
|
||||
# 3. The admin needs a password before it can be used
|
||||
ssh monoco scripts/provision-site-env.sh /var/lib/menu-app.hopsngrains.com
|
||||
```
|
||||
|
||||
The first run creates `/var/lib/menu-app.hopsngrains.com`, starts the container
|
||||
on the `menu-app.hopsngrains.com_net` network, adds a snippet for
|
||||
`menu-new.hopsngrains.com` to the shared Caddyfile, and reloads Caddy. The
|
||||
existing `menu.hopsngrains.com` vhost still points at the old nginx container.
|
||||
|
||||
`site.env` is deliberately not created by Ansible: the password is typed by a
|
||||
human on the server. Until it exists the admin is disabled and the playbook says
|
||||
so — the public menu still serves.
|
||||
|
||||
## Checking the parallel run
|
||||
|
||||
From a phone on restaurant wifi, not from the server:
|
||||
|
||||
- `https://menu-new.hopsngrains.com/` lists the Sections that have a current menu.
|
||||
- A Section page scrolls, the pour line tracks, tapping a page opens it full width.
|
||||
- The admin signs in, uploads a real photo, reorders, previews, publishes.
|
||||
- After publishing, the phone shows the new menu and the old set appears under
|
||||
"Older menus".
|
||||
|
||||
## Cutover
|
||||
|
||||
```sh
|
||||
ansible-playbook -l monoco -e site=hopsngrains -e tag=2026-09-22 \
|
||||
-e vhost=menu.hopsngrains.com \
|
||||
-e retire_nginx_snippet=true \
|
||||
-e retire_nginx_container=true \
|
||||
playbook.yml
|
||||
```
|
||||
|
||||
This repoints the real vhost at the app, removes the snippet the old playbook
|
||||
wrote for that hostname, and stops the old container. The old container and
|
||||
`/var/lib/menu.hopsngrains.com` are kept, not deleted: deleting 319 MB of the
|
||||
only copy of the old menus is a human decision, made a week later, once the new
|
||||
site has been trusted.
|
||||
|
||||
## Rolling back
|
||||
|
||||
```sh
|
||||
docker start nginx-menu.hopsngrains.com
|
||||
ansible-playbook -l monoco -e site=hopsngrains -e tag=<previous> \
|
||||
-e vhost=menu.hopsngrains.com -e retire_nginx_snippet=false playbook.yml
|
||||
```
|
||||
|
||||
Then delete the app's snippet from `/var/lib/caddy/conf/Caddyfile` by hand, or
|
||||
set `vhost` back to the temporary name, and reload Caddy.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
- name: Reload Caddy
|
||||
# Caddy validates before it applies, so a mistake here leaves the running
|
||||
# config untouched and fails the play loudly.
|
||||
command: "docker exec -w /etc/caddy {{ caddy_container }} caddy reload"
|
||||
listen: Reload Caddy
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
- name: Check the Caddyfile exists
|
||||
stat:
|
||||
path: "{{ caddyfile_path }}"
|
||||
register: menu_caddyfile
|
||||
|
||||
- name: Fail when Caddy is not where we expect it
|
||||
fail:
|
||||
msg: >-
|
||||
{{ caddyfile_path }} is missing. Deploy the caddy playbook (which owns the
|
||||
Caddy container and its config) before this one.
|
||||
when: not menu_caddyfile.stat.exists
|
||||
|
||||
# blockinfile replaces whatever sits between its markers, so changing `vhost`
|
||||
# updates the snippet in place. The sibling nginx-menu role only appended a
|
||||
# block when its exact text was absent, which cannot change a proxy target.
|
||||
- name: Point Caddy at the app
|
||||
blockinfile:
|
||||
path: "{{ caddyfile_path }}"
|
||||
marker: "# {mark} ANSIBLE MANAGED: menu app ({{ site }})"
|
||||
block: "{{ caddy_snippet }}"
|
||||
create: no
|
||||
notify: Reload Caddy
|
||||
|
||||
- name: Remove the snippet the nginx-menu playbook wrote for this vhost
|
||||
# A different marker, written by a different role. Leaving it in place would
|
||||
# leave Caddy with two blocks for the same hostname.
|
||||
blockinfile:
|
||||
path: "{{ caddyfile_path }}"
|
||||
marker: "# {mark} Ansible managed Caddy snippet for {{ host }}"
|
||||
state: absent
|
||||
when: retire_nginx_snippet | bool
|
||||
notify: Reload Caddy
|
||||
|
||||
- name: Stop the old nginx container
|
||||
docker_container:
|
||||
name: "{{ legacy_container }}"
|
||||
state: stopped
|
||||
when: retire_nginx_container | bool
|
||||
@@ -1,91 +0,0 @@
|
||||
---
|
||||
- name: Pull the app image
|
||||
docker_image:
|
||||
name: "{{ image_name }}"
|
||||
tag: "{{ image_tag }}"
|
||||
source: pull
|
||||
force_source: yes
|
||||
|
||||
- name: Ensure the app root exists
|
||||
file:
|
||||
path: "{{ app_root }}"
|
||||
state: directory
|
||||
owner: "{{ app_uid }}"
|
||||
group: "{{ app_gid }}"
|
||||
mode: "0750"
|
||||
|
||||
- name: Ensure the data directory exists
|
||||
file:
|
||||
path: "{{ app_root }}/data"
|
||||
state: directory
|
||||
owner: "{{ app_uid }}"
|
||||
group: "{{ app_gid }}"
|
||||
mode: "0750"
|
||||
|
||||
- name: Look for the untracked site.env
|
||||
stat:
|
||||
path: "{{ app_root }}/site.env"
|
||||
register: menu_site_env
|
||||
|
||||
- name: Warn when the admin has no password yet
|
||||
debug:
|
||||
msg: >-
|
||||
{{ app_root }}/site.env is missing, so the admin is disabled: the public
|
||||
menu still serves. Run scripts/provision-site-env.sh {{ app_root }} on
|
||||
this host, then re-run this playbook.
|
||||
when: not menu_site_env.stat.exists
|
||||
|
||||
- name: Run the menu app
|
||||
docker_container:
|
||||
name: "{{ docker_container }}"
|
||||
image: "{{ image_name }}:{{ image_tag }}"
|
||||
state: started
|
||||
restart_policy: unless-stopped
|
||||
user: "{{ app_uid }}:{{ app_gid }}"
|
||||
env:
|
||||
MENU_ROOT: /data
|
||||
MENU_SITE_CONFIG: "/app/config/{{ site }}.site.json"
|
||||
SECURE_COOKIES: "true"
|
||||
LOG_LEVEL: info
|
||||
PORT: "3000"
|
||||
volumes:
|
||||
- "{{ app_root }}:/data"
|
||||
networks:
|
||||
- name: "{{ docker_network }}"
|
||||
|
||||
- name: Wait for the app to answer
|
||||
uri:
|
||||
url: "http://{{ docker_container }}:3000/healthz"
|
||||
status_code: 200
|
||||
register: menu_health
|
||||
retries: 10
|
||||
delay: 2
|
||||
until: menu_health.status == 200
|
||||
changed_when: false
|
||||
|
||||
- name: Check the data directory for integrity problems
|
||||
# Reads the same directory the container writes, as the same user, so a
|
||||
# permissions mistake shows up here rather than at 8pm service.
|
||||
command:
|
||||
argv:
|
||||
- docker
|
||||
- run
|
||||
- --rm
|
||||
- --user
|
||||
- "{{ app_uid }}:{{ app_gid }}"
|
||||
- -e
|
||||
- MENU_ROOT=/data
|
||||
- -e
|
||||
- "MENU_SITE_CONFIG=/app/config/{{ site }}.site.json"
|
||||
- --entrypoint
|
||||
- node
|
||||
- "{{ image_name }}:{{ image_tag }}"
|
||||
- src/cli/verify.ts
|
||||
register: menu_verify
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Report integrity problems
|
||||
debug:
|
||||
msg: "{{ menu_verify.stdout_lines | default([]) }}"
|
||||
when: menu_verify.rc != 0
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
- name: "Ensure {{ docker_network }} exists and Caddy is on it"
|
||||
docker_network:
|
||||
name: "{{ docker_network }}"
|
||||
state: present
|
||||
connected:
|
||||
- "{{ caddy_container }}"
|
||||
appends: yes
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
# Defaults shared by every Site. Override per Site in vars/<site>.yml, and pass
|
||||
# -e site=<site> on the command line.
|
||||
|
||||
# Which Site config inside the image to run, and therefore which vhost it is
|
||||
# for. The file lives in the app repo at config/<site>.site.json.
|
||||
site: hopsngrains
|
||||
|
||||
image_name: registry.tanshu.com/menu-admin
|
||||
image_tag: "{{ tag | default('latest') }}"
|
||||
|
||||
# The hostname Caddy serves this Site on. The parallel run uses a temporary
|
||||
# name; cutover points the real one at the app.
|
||||
vhost: "{{ host }}"
|
||||
|
||||
# Cutover switches. Both stay off until the parallel run has been checked.
|
||||
retire_nginx_snippet: false
|
||||
retire_nginx_container: false
|
||||
|
||||
app_uid: 1000
|
||||
app_gid: 1000
|
||||
|
||||
caddy_container: caddy
|
||||
caddyfile_path: /var/lib/caddy/conf/Caddyfile
|
||||
|
||||
caddy_snippet: |
|
||||
{{ vhost }} {
|
||||
reverse_proxy {{ docker_container }}:3000
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
# Hops n Grains. The Section list and branding live in the app repo at
|
||||
# config/hopsngrains.site.json, which is baked into the image, so this file
|
||||
# only says where to run it.
|
||||
site: hopsngrains
|
||||
|
||||
host: menu.hopsngrains.com
|
||||
|
||||
docker_container: menu-app.hopsngrains.com
|
||||
docker_network: "{{ docker_container }}_net"
|
||||
|
||||
# First deployment: a temporary hostname beside the existing site.
|
||||
vhost: menu-new.hopsngrains.com
|
||||
|
||||
# Everything the app writes — data/ and the untracked site.env.
|
||||
app_root: /var/lib/menu-app.hopsngrains.com
|
||||
|
||||
# The old nginx container serving /var/lib/menu.hopsngrains.com. Left alone
|
||||
# until cutover.
|
||||
legacy_container: nginx-menu.hopsngrains.com
|
||||
Reference in new issue
Block a user