3 Commits
Author SHA1 Message Date
westfarn 658152088e Treat omitted host_apps.enabled as true (#36)
Sync runner checkout / sync (push) Successful in 6s
Closes #35.

## Summary
- `#33` used Jinja `rejectattr('enabled', 'equalto', false)` to skip stopped apps. That filter **requires** the key, so every `host_apps` row that omits `enabled` (the documented default) crashed deploy on all webservers before git/compose.
- Filter with `item.enabled | default(true)` instead, in `app-deploy` and `web-static`. `print_forge` (`enabled: false`) still skipped. `url_shortening_service` and every other omitted-key row deploy again.

## Test plan
- [ ] `ansible-playbook playbooks/deploy-apps.yml --syntax-check`
- [ ] Redeploy `url_shortening_service` beta: `deploy.sh --app url_shortening_service --env beta --ref <sha>`
- [ ] Confirm `print_forge` is still not started (no compose up on 8007/8019)

Reviewed-on: #36
2026-09-16 08:21:29 -07:00
westfarn 726ad971cb Add stop.sh (compose down) and disable print_forge (#34)
Sync runner checkout / sync (push) Successful in 6s
## Summary
- Closes [#33](#33).
- Add `scripts/stop.sh` → `playbooks/stop-apps.yml` → `docker compose down` for one `--app` + `--env` (no `-v`; checkouts/secrets stay).
- `host_apps.enabled: false` skips deploy (including CI `--app`) so a stopped app cannot come back. Rows stay for ports and `stop.sh`.
- Disable `print_forge` beta + prod on all app hosts. After merge (or from this branch): `./scripts/stop.sh --app print_forge --env prod` and `--env beta`.

## Test plan
- [ ] `./scripts/stop.sh --help` shows required `--app` / `--env`
- [ ] `ansible-playbook playbooks/stop-apps.yml --syntax-check -e app=print_forge -e app_env=prod`
- [ ] `./scripts/stop.sh --app print_forge --env prod` compose-downs `print_forge_prod` on all webservers
- [ ] `./scripts/stop.sh --app print_forge --env beta` compose-downs `print_forge_beta` (including adama worker)
- [ ] `./scripts/deploy.sh --app print_forge --env prod --check` skips disabled rows (does not start containers)
- [ ] NPM: `print-forge-preview.aimloperations.com` will 502 until the proxy host is disabled (out of scope)

Reviewed-on: #34
2026-09-16 03:11:23 -07:00
westfarn 94fa05a952 Merge pull request 'Register GIS LiveKit SFU for webserver docker deploy' (#32) from livekit-sfu into master
Sync runner checkout / sync (push) Successful in 7s
2026-09-15 12:47:47 -07:00
15 changed files with 329 additions and 29 deletions
+39 -8
View File
@@ -30,6 +30,7 @@ flowchart TB
|----------|------|----------|---------------| |----------|------|----------|---------------|
| **Provision** | New VM, OS change, firewall, Docker install | `site.yml` | ai-server-4080 — run manually | | **Provision** | New VM, OS change, firewall, Docker install | `site.yml` | ai-server-4080 — run manually |
| **Deploy** | Green unit tests on `master` | `deploy-apps.yml` | Gitea Act runner on ai-server-4080 | | **Deploy** | Green unit tests on `master` | `deploy-apps.yml` | Gitea Act runner on ai-server-4080 |
| **Stop** | Take a compose app down | `stop-apps.yml` (`scripts/stop.sh`) | ai-server-4080 — run manually |
Both pipelines share the same inventory (`inventory/hosts.yml`). Both pipelines share the same inventory (`inventory/hosts.yml`).
@@ -65,7 +66,8 @@ server-infra/
│ └── ai-server-4080.yml # control node / act runner / SearxNG / observability │ └── ai-server-4080.yml # control node / act runner / SearxNG / observability
├── playbooks/ ├── playbooks/
│ ├── site.yml # Phase 1: provision │ ├── site.yml # Phase 1: provision
── deploy-apps.yml # Phase 2: CI deploy ── deploy-apps.yml # Phase 2: CI deploy
│ └── stop-apps.yml # compose down one app+env
├── roles/ ├── roles/
│ ├── common/ # Base packages │ ├── common/ # Base packages
│ ├── ufw/ # Firewall │ ├── ufw/ # Firewall
@@ -80,7 +82,8 @@ server-infra/
│ └── web-static/ # nginx container serving /var/www builds │ └── web-static/ # nginx container serving /var/www builds
└── scripts/ └── scripts/
├── provision.sh # Wrapper with --limit support ├── provision.sh # Wrapper with --limit support
── deploy.sh # Wrapper for deploy playbook ── deploy.sh # Wrapper for deploy playbook
└── stop.sh # Wrapper for stop playbook (compose down)
``` ```
## Prerequisites (One-Time Bootstrap) ## Prerequisites (One-Time Bootstrap)
@@ -174,6 +177,18 @@ New hosts need the one-time passwordless sudo bootstrap in
./scripts/deploy.sh apollo ./scripts/deploy.sh apollo
``` ```
### Stop a compose app
```bash
./scripts/stop.sh --app print_forge --env prod
./scripts/stop.sh --app print_forge --env beta
./scripts/stop.sh adama --app chat_backend --env beta --check
```
`stop.sh` is `docker compose down` for one `--app` + `--env` (no `-v`). Checkouts and
secrets stay. Next `deploy.sh` of that app+env starts it again unless `host_apps`
sets `enabled: false`. Node-static apps have no compose project; stop.sh skips them.
Under the hood, scripts pass `--limit <hostname>` to `ansible-playbook`. Under the hood, scripts pass `--limit <hostname>` to `ansible-playbook`.
## Phase 1: Provision (`site.yml`) ## Phase 1: Provision (`site.yml`)
@@ -218,7 +233,7 @@ After Docker install, re-SSH so the `docker` group membership takes effect.
| `monica_site` | django (docker) | all webservers | beta + prod | active/active behind NPM; no bundled Postgres (like `scha`); dj-queue **worker singleton on adama** only (`compose --profile worker`); Ollama social drafting via `10.0.0.128:11434` | | `monica_site` | django (docker) | all webservers | beta + prod | active/active behind NPM; no bundled Postgres (like `scha`); dj-queue **worker singleton on adama** only (`compose --profile worker`); Ollama social drafting via `10.0.0.128:11434` |
| `url_shortening_service` | django (docker) | all webservers | beta + prod | active/active behind NPM; no bundled Postgres; **no worker**. Two public hosts, same container: short domain (`GET /`, `GET /<code>` 302) and API host (`/api/links/`, Bearer required). | | `url_shortening_service` | django (docker) | all webservers | beta + prod | active/active behind NPM; no bundled Postgres; **no worker**. Two public hosts, same container: short domain (`GET /`, `GET /<code>` 302) and API host (`/api/links/`, Bearer required). |
| `college_craft` | django (docker) | all webservers | beta + prod | active/active behind NPM; no bundled Postgres (like `scha` / `monica_site`); dj-queue **worker singleton on adama** only (`compose --profile worker`); Ollama social drafting via `10.0.0.128:11434`; Nominatim `http://10.0.0.128:8089`; prod `SITE_UNDER_CONSTRUCTION=true` until launch | | `college_craft` | django (docker) | all webservers | beta + prod | active/active behind NPM; no bundled Postgres (like `scha` / `monica_site`); dj-queue **worker singleton on adama** only (`compose --profile worker`); Ollama social drafting via `10.0.0.128:11434`; Nominatim `http://10.0.0.128:8089`; prod `SITE_UNDER_CONSTRUCTION=true` until launch |
| `print_forge` | django (docker) | all webservers | beta + prod | active/active behind NPM; no bundled Postgres (like `scha` / `monica_site` / `college_craft`); dj-queue **worker singleton on adama** only (`compose --profile worker`); Nominatim `http://10.0.0.128:8089`; prod `SITE_UNDER_CONSTRUCTION=true` until launch; prod NPM waits until launch | | `print_forge` | django (docker) | all webservers | beta + prod | **stopped** (`enabled: false`, [#33](https://git.aimloperations.com/ai_ml_operations/server-infra/issues/33)). Ports reserved. Re-enable + `deploy.sh` to bring back. Worker was adama-only. |
| `abc_be` | django (docker) | all webservers | **beta only** | AI Benefits Coach API; no bundled Postgres; Ollama `http://10.0.0.128:11434`; shares DB `abc_be_beta` with `abc_worker` | | `abc_be` | django (docker) | all webservers | **beta only** | AI Benefits Coach API; no bundled Postgres; Ollama `http://10.0.0.128:11434`; shares DB `abc_be_beta` with `abc_worker` |
| `abc_worker` | django-type compose (FastAPI) | all webservers | **beta only** | same Postgres as `abc_be`; `migrate_cmd: true`; WS on host port 8018 | | `abc_worker` | django-type compose (FastAPI) | all webservers | **beta only** | same Postgres as `abc_be`; `migrate_cmd: true`; WS on host port 8018 |
| `abc_fe` | node-static (Vite) | all webservers | **beta only** | built to `/var/www/<env>.abc.aimloperations/html`; no prod `host_apps` row | | `abc_fe` | node-static (Vite) | all webservers | **beta only** | built to `/var/www/<env>.abc.aimloperations/html`; no prod `host_apps` row |
@@ -234,6 +249,9 @@ a DB.
- `host_apps` (`host_vars/<host>.yml`) — which app+env+port runs on that host. - `host_apps` (`host_vars/<host>.yml`) — which app+env+port runs on that host.
Optional `compose_profiles: [worker]` activates docker compose profiles on that Optional `compose_profiles: [worker]` activates docker compose profiles on that
host only (used for `monica_site` / `college_craft` / `print_forge` dj-queue singleton on adama). host only (used for `monica_site` / `college_craft` / `print_forge` dj-queue singleton on adama).
Optional `enabled: false` skips deploy (CI included) but keeps the row for ports
and `stop.sh`. Omit the key (or `true`) to deploy as before. Do not use Jinja
`rejectattr('enabled')` — missing key is an error, not "enabled".
- Django app = one compose project per env: project name `<app>_<env>`, host port from `host_apps`. - Django app = one compose project per env: project name `<app>_<env>`, host port from `host_apps`.
Ports match across app hosts so NPM can balance `adama:PORT` + `roslin:PORT` + `starbuck:PORT` + `apollo:PORT`. Ports match across app hosts so NPM can balance `adama:PORT` + `roslin:PORT` + `starbuck:PORT` + `apollo:PORT`.
@@ -252,7 +270,7 @@ future beta replica.
| monica_site | 8014 | 8004 | all webservers | | monica_site | 8014 | 8004 | all webservers |
| url_shortening_service | 8015 | 8005 | all webservers | | url_shortening_service | 8015 | 8005 | all webservers |
| college_craft | 8016 | 8006 | all webservers | | college_craft | 8016 | 8006 | all webservers |
| print_forge | **8019** | **8007** | all webservers | | print_forge | **8019** (*stopped*) | **8007** (*stopped*) | all webservers (`enabled: false`) |
| abc_be | **8017** | 8009 (*not deployed*) | all webservers | | abc_be | **8017** | 8009 (*not deployed*) | all webservers |
| abc_worker | **8018** | 8008 (*not deployed*) | all webservers | | abc_worker | **8018** | 8008 (*not deployed*) | all webservers |
| dta_webapp (nginx) | 8081 | 8080 | all webservers | | dta_webapp (nginx) | 8081 | 8080 | all webservers |
@@ -285,7 +303,16 @@ chat_backend secrets must use `SEARXNG_BASE_URL=http://10.0.0.128:8088`.
--app company_site --env prod --ref "${{ gitea.sha }}" --app company_site --env prod --ref "${{ gitea.sha }}"
``` ```
3. `deploy-apps.yml` runs against `webservers`; each host deploys only the 3. `deploy-apps.yml` runs against `webservers`; each host deploys only the
matching app+env from its `host_apps`. matching **enabled** app+env from its `host_apps` (`enabled: false` is skipped).
To stop a compose app without deleting inventory:
```bash
./scripts/stop.sh --app print_forge --env prod
```
Set `enabled: false` on that `host_apps` row so the next CI deploy cannot start it
again. `stop.sh` still matches disabled rows.
### `app-deploy` role behavior ### `app-deploy` role behavior
@@ -329,7 +356,7 @@ point each domain at the backend(s):
| url_shortening_service (short) | `aiml.pw` and/or `cidinn.li` (pick when DNS is ready); `short-beta.aimloperations.com` (beta). Proxy `/` + `/[a-z0-9]{4,8}` only — 404 `/api/`, `/admin/`, `/debug/` | `adama:8005` / `8015` + same on roslin / starbuck / apollo / ai-server-4080 | | url_shortening_service (short) | `aiml.pw` and/or `cidinn.li` (pick when DNS is ready); `short-beta.aimloperations.com` (beta). Proxy `/` + `/[a-z0-9]{4,8}` only — 404 `/api/`, `/admin/`, `/debug/` | `adama:8005` / `8015` + same on roslin / starbuck / apollo / ai-server-4080 |
| url_shortening_service (API) | `shortener.aimloperations.com`; `shortener-beta.aimloperations.com` (beta). Proxy `/api/` only — 404 `/admin/` | same ports as short host (one container) | | url_shortening_service (API) | `shortener.aimloperations.com`; `shortener-beta.aimloperations.com` (beta). Proxy `/api/` only — 404 `/admin/` | same ports as short host (one container) |
| college_craft | `collegecraft.com` (+ www); `college-craft-preview.aimloperations.com` (beta) | `adama:8006` / `8016` + same on roslin / starbuck / apollo / ai-server-4080 | | college_craft | `collegecraft.com` (+ www); `college-craft-preview.aimloperations.com` (beta) | `adama:8006` / `8016` + same on roslin / starbuck / apollo / ai-server-4080 |
| print_forge | `printforgeprints.com` (+ www) — **do not NPM-route prod until launch**; `print-forge-preview.aimloperations.com` (beta) | `adama:8007` / `8019` + same on roslin / starbuck / apollo / ai-server-4080 | | print_forge | `printforgeprints.com` (+ www); `print-forge-preview.aimloperations.com` (beta) — **app stopped** (`enabled: false`). Disable NPM hosts or expect 502. | `:8007` / `:8019` reserved |
| abc_fe | `beta.abc.aimloperations.com` (beta only) | `adama:8085` + same on roslin / starbuck / apollo / ai-server-4080 | | abc_fe | `beta.abc.aimloperations.com` (beta only) | `adama:8085` + same on roslin / starbuck / apollo / ai-server-4080 |
| abc_be | `beta.abc.be.aimloperations.com` (beta only) | `adama:8017` + same on roslin / starbuck / apollo / ai-server-4080 | | abc_be | `beta.abc.be.aimloperations.com` (beta only) | `adama:8017` + same on roslin / starbuck / apollo / ai-server-4080 |
| abc_worker | `beta.abc.worker.aimloperations.com` (beta only; HTTP + WebSocket upgrade) | `adama:8018` + same on roslin / starbuck / apollo / ai-server-4080 | | abc_worker | `beta.abc.worker.aimloperations.com` (beta only; HTTP + WebSocket upgrade) | `adama:8018` + same on roslin / starbuck / apollo / ai-server-4080 |
@@ -406,7 +433,10 @@ App-repo companion (`college_craft`, not this repo): deploy workflows still call
`--app client_site` (template leftover). They must become `--app college_craft` `--app client_site` (template leftover). They must become `--app college_craft`
before CI deploy will hit this catalog entry. Default branch is `master` (not `main`). before CI deploy will hit this catalog entry. Default branch is `master` (not `main`).
`print_forge` extra env (control-node secrets, not in git): `print_forge` extra env (control-node secrets, not in git) — **currently stopped**
([#33](https://git.aimloperations.com/ai_ml_operations/server-infra/issues/33)):
`host_apps` rows stay with `enabled: false`. Bring back with `enabled` removed/true
then `deploy.sh`. Secrets kept:
`~/Documents/secrets/print_forge/print_forge_prod.env` and `~/Documents/secrets/print_forge/print_forge_prod.env` and
`print_forge_beta.env`. Template: app repo `.env.prod.example`. `print_forge_beta.env`. Template: app repo `.env.prod.example`.
`DJANGO_ENV` / `DJANGO_ALLOWED_HOSTS` / `DATABASE_URL` / `WEB_PORT` / `DJANGO_ENV` / `DJANGO_ALLOWED_HOSTS` / `DATABASE_URL` / `WEB_PORT` /
@@ -421,7 +451,8 @@ on beta. Default branch is `master`.
Companion app ticket: [print_forge#1](https://git.aimloperations.com/ai_ml_operations/print_forge/issues/1) Companion app ticket: [print_forge#1](https://git.aimloperations.com/ai_ml_operations/print_forge/issues/1)
([#27](https://git.aimloperations.com/ai_ml_operations/server-infra/issues/27)). ([#27](https://git.aimloperations.com/ai_ml_operations/server-infra/issues/27)).
App Gitea workflow deploys beta on push to `master` (`--app print_forge`). App Gitea workflow still calls `--app print_forge` on `master`; deploy **no-ops**
while `enabled: false`.
`abc_be` / `abc_worker` / `abc_fe` (GIS org, not this repo) — **beta only** ([#26](https://git.aimloperations.com/ai_ml_operations/server-infra/issues/26)): `abc_be` / `abc_worker` / `abc_fe` (GIS org, not this repo) — **beta only** ([#26](https://git.aimloperations.com/ai_ml_operations/server-infra/issues/26)):
+4
View File
@@ -33,6 +33,10 @@ ansible adama -m ping
# Provision all hosts # Provision all hosts
./scripts/provision.sh ./scripts/provision.sh
# Stop a compose app (does not remove volumes/checkouts)
./scripts/stop.sh --app print_forge --env prod
./scripts/stop.sh --app print_forge --env beta
``` ```
See [IMPLEMENTATION.md](IMPLEMENTATION.md) for full architecture, CI/CD plan, and phase breakdown. See [IMPLEMENTATION.md](IMPLEMENTATION.md) for full architecture, CI/CD plan, and phase breakdown.
+1
View File
@@ -59,6 +59,7 @@ secrets_dir: "{{ lookup('ansible.builtin.env', 'HOME') }}/Documents/secrets"
web_static_root: /var/www web_static_root: /var/www
# Catalog of deployable apps. host_apps (per host_vars) references these by name. # Catalog of deployable apps. host_apps (per host_vars) references these by name.
# Optional host_apps.enabled: false skips deploy (stop.sh still compose-downs).
app_catalog: app_catalog:
company_site: company_site:
type: django type: django
+3 -2
View File
@@ -26,8 +26,9 @@ host_apps:
- { name: url_shortening_service, env: beta, port: 8015 } - { name: url_shortening_service, env: beta, port: 8015 }
- { name: college_craft, env: prod, port: 8006, compose_profiles: [worker] } - { name: college_craft, env: prod, port: 8006, compose_profiles: [worker] }
- { name: college_craft, env: beta, port: 8016, compose_profiles: [worker] } - { name: college_craft, env: beta, port: 8016, compose_profiles: [worker] }
- { name: print_forge, env: prod, port: 8007, compose_profiles: [worker] } # print_forge stopped (#33) — enabled: false keeps ports; stop.sh still matches.
- { name: print_forge, env: beta, port: 8019, compose_profiles: [worker] } - { name: print_forge, env: prod, port: 8007, compose_profiles: [worker], enabled: false }
- { name: print_forge, env: beta, port: 8019, compose_profiles: [worker], enabled: false }
- { name: abc_be, env: beta, port: 8017 } - { name: abc_be, env: beta, port: 8017 }
- { name: abc_worker, env: beta, port: 8018 } - { name: abc_worker, env: beta, port: 8018 }
- { name: abc_fe, env: beta, port: 8085 } - { name: abc_fe, env: beta, port: 8085 }
+2 -2
View File
@@ -43,8 +43,8 @@ host_apps:
- { name: url_shortening_service, env: beta, port: 8015 } - { name: url_shortening_service, env: beta, port: 8015 }
- { name: college_craft, env: prod, port: 8006 } - { name: college_craft, env: prod, port: 8006 }
- { name: college_craft, env: beta, port: 8016 } - { name: college_craft, env: beta, port: 8016 }
- { name: print_forge, env: prod, port: 8007 } - { name: print_forge, env: prod, port: 8007, enabled: false }
- { name: print_forge, env: beta, port: 8019 } - { name: print_forge, env: beta, port: 8019, enabled: false }
- { name: abc_be, env: beta, port: 8017 } - { name: abc_be, env: beta, port: 8017 }
- { name: abc_worker, env: beta, port: 8018 } - { name: abc_worker, env: beta, port: 8018 }
- { name: abc_fe, env: beta, port: 8085 } - { name: abc_fe, env: beta, port: 8085 }
+2 -2
View File
@@ -22,8 +22,8 @@ host_apps:
- { name: url_shortening_service, env: beta, port: 8015 } - { name: url_shortening_service, env: beta, port: 8015 }
- { name: college_craft, env: prod, port: 8006 } - { name: college_craft, env: prod, port: 8006 }
- { name: college_craft, env: beta, port: 8016 } - { name: college_craft, env: beta, port: 8016 }
- { name: print_forge, env: prod, port: 8007 } - { name: print_forge, env: prod, port: 8007, enabled: false }
- { name: print_forge, env: beta, port: 8019 } - { name: print_forge, env: beta, port: 8019, enabled: false }
- { name: abc_be, env: beta, port: 8017 } - { name: abc_be, env: beta, port: 8017 }
- { name: abc_worker, env: beta, port: 8018 } - { name: abc_worker, env: beta, port: 8018 }
- { name: abc_fe, env: beta, port: 8085 } - { name: abc_fe, env: beta, port: 8085 }
+2 -2
View File
@@ -22,8 +22,8 @@ host_apps:
- { name: url_shortening_service, env: beta, port: 8015 } - { name: url_shortening_service, env: beta, port: 8015 }
- { name: college_craft, env: prod, port: 8006 } - { name: college_craft, env: prod, port: 8006 }
- { name: college_craft, env: beta, port: 8016 } - { name: college_craft, env: beta, port: 8016 }
- { name: print_forge, env: prod, port: 8007 } - { name: print_forge, env: prod, port: 8007, enabled: false }
- { name: print_forge, env: beta, port: 8019 } - { name: print_forge, env: beta, port: 8019, enabled: false }
- { name: abc_be, env: beta, port: 8017 } - { name: abc_be, env: beta, port: 8017 }
- { name: abc_worker, env: beta, port: 8018 } - { name: abc_worker, env: beta, port: 8018 }
- { name: abc_fe, env: beta, port: 8085 } - { name: abc_fe, env: beta, port: 8085 }
+2 -2
View File
@@ -22,8 +22,8 @@ host_apps:
- { name: url_shortening_service, env: beta, port: 8015 } - { name: url_shortening_service, env: beta, port: 8015 }
- { name: college_craft, env: prod, port: 8006 } - { name: college_craft, env: prod, port: 8006 }
- { name: college_craft, env: beta, port: 8016 } - { name: college_craft, env: beta, port: 8016 }
- { name: print_forge, env: prod, port: 8007 } - { name: print_forge, env: prod, port: 8007, enabled: false }
- { name: print_forge, env: beta, port: 8019 } - { name: print_forge, env: beta, port: 8019, enabled: false }
- { name: abc_be, env: beta, port: 8017 } - { name: abc_be, env: beta, port: 8017 }
- { name: abc_worker, env: beta, port: 8018 } - { name: abc_worker, env: beta, port: 8018 }
- { name: abc_fe, env: beta, port: 8085 } - { name: abc_fe, env: beta, port: 8085 }
+3 -2
View File
@@ -1,8 +1,9 @@
--- ---
# Phase 2: CI-triggered app deployment. # Phase 2: CI-triggered app deployment.
# #
# Runs against all webservers; each host deploys only the apps listed in its # Runs against all webservers; each host deploys only enabled apps listed in
# host_apps. Deploy exactly one app+env at a pinned ref via extra-vars: # its host_apps (enabled: false is skipped). Deploy exactly one app+env at a
# pinned ref via extra-vars:
# #
# ansible-playbook playbooks/deploy-apps.yml \ # ansible-playbook playbooks/deploy-apps.yml \
# -e app=company_site -e app_env=prod -e app_ref=<sha> # -e app=company_site -e app_env=prod -e app_ref=<sha>
+15
View File
@@ -0,0 +1,15 @@
---
# Stop one docker-compose app+env. Inverse of deploy-apps.yml start.
#
# ansible-playbook playbooks/stop-apps.yml -e app=print_forge -e app_env=prod
#
# host_apps rows with enabled: false are still valid stop targets (so a
# disabled app can be taken down). Node-static apps are skipped.
- name: Stop applications
hosts: webservers
become: true
tasks:
- name: Stop requested compose apps
ansible.builtin.include_role:
name: app-deploy
tasks_from: stop.yml
+30 -8
View File
@@ -6,24 +6,21 @@
# at branch master. # at branch master.
# #
# host_apps (host_vars) lists what runs on THIS host; app_catalog (group_vars) # host_apps (host_vars) lists what runs on THIS host; app_catalog (group_vars)
# describes how each app is built. # describes how each app is built. host_apps.enabled: false (omit = true)
# keeps the row for ports/stop.sh but skips deploy so CI cannot resurrect it.
- name: Classify catalog by type - name: Classify catalog by type
ansible.builtin.set_fact: ansible.builtin.set_fact:
django_names: "{{ app_catalog | dict2items | selectattr('value.type', 'equalto', 'django') | map(attribute='key') | list }}" django_names: "{{ app_catalog | dict2items | selectattr('value.type', 'equalto', 'django') | map(attribute='key') | list }}"
node_names: "{{ app_catalog | dict2items | selectattr('value.type', 'equalto', 'node-static') | map(attribute='key') | list }}" node_names: "{{ app_catalog | dict2items | selectattr('value.type', 'equalto', 'node-static') | map(attribute='key') | list }}"
- name: Resolve deploy targets for {{ inventory_hostname }} - name: Resolve matching host_apps for {{ inventory_hostname }}
ansible.builtin.set_fact: ansible.builtin.set_fact:
deploy_targets: >- matching_host_apps: >-
{{ (host_apps | default([]) | selectattr('name', 'equalto', app | default('')) | selectattr('env', 'equalto', app_env | default('')) | list) {{ (host_apps | default([]) | selectattr('name', 'equalto', app | default('')) | selectattr('env', 'equalto', app_env | default('')) | list)
if (app is defined and app_env is defined) if (app is defined and app_env is defined)
else (host_apps | default([])) }} else (host_apps | default([])) }}
- name: Show deploy targets
ansible.builtin.debug:
msg: "ref={{ app_ref | default('(per-app default branch)') }} targets={{ deploy_targets | map(attribute='name') | zip(deploy_targets | map(attribute='env')) | list }}"
# Explicit --app/--env with no host_apps match used to silently no-op Django # Explicit --app/--env with no host_apps match used to silently no-op Django
# and still refresh web-static (looked like a "static-only" deploy). Fail loud. # and still refresh web-static (looked like a "static-only" deploy). Fail loud.
- name: Fail when requested app+env is not on this host - name: Fail when requested app+env is not on this host
@@ -36,7 +33,32 @@
when: when:
- app is defined - app is defined
- app_env is defined - app_env is defined
- deploy_targets | length == 0 - matching_host_apps | length == 0
# rejectattr('enabled', …) raises when the key is omitted. Omit = enabled (#35).
- name: Reset deploy targets
ansible.builtin.set_fact:
deploy_targets: []
- name: "Drop disabled host_apps (enabled: false)"
ansible.builtin.set_fact:
deploy_targets: "{{ deploy_targets + [item] }}"
loop: "{{ matching_host_apps }}"
when: item.enabled | default(true)
loop_control:
label: "{{ item.name }}/{{ item.env }}"
- name: Show skipped disabled apps
ansible.builtin.debug:
msg: "skip disabled {{ item.name }}/{{ item.env }} on {{ inventory_hostname }}"
loop: "{{ matching_host_apps }}"
when: not (item.enabled | default(true))
loop_control:
label: "{{ item.name }}/{{ item.env }}"
- name: Show deploy targets
ansible.builtin.debug:
msg: "ref={{ app_ref | default('(per-app default branch)') }} targets={{ deploy_targets | map(attribute='name') | zip(deploy_targets | map(attribute='env')) | list }}"
- name: Fail when requested app missing from app_catalog - name: Fail when requested app missing from app_catalog
ansible.builtin.fail: ansible.builtin.fail:
+47
View File
@@ -0,0 +1,47 @@
---
# Stop one app+env. CI/manual must pass: app=<name> app_env=<beta|prod>
#
# Matches host_apps including enabled: false. Hosts without that row skip.
# Django/compose → docker compose down (no -v). Node-static → message only.
- name: Fail unless app and app_env extra-vars set
ansible.builtin.fail:
msg: "stop-apps.yml requires -e app=<name> -e app_env=<beta|prod>"
when: app is not defined or app_env is not defined
- name: Classify catalog by type
ansible.builtin.set_fact:
django_names: "{{ app_catalog | dict2items | selectattr('value.type', 'equalto', 'django') | map(attribute='key') | list }}"
node_names: "{{ app_catalog | dict2items | selectattr('value.type', 'equalto', 'node-static') | map(attribute='key') | list }}"
- name: Fail when requested app missing from app_catalog
ansible.builtin.fail:
msg: >-
app={{ app }} is not in app_catalog. Known: {{ app_catalog.keys() | list }}.
when: app not in app_catalog
- name: Resolve stop targets for {{ inventory_hostname }}
ansible.builtin.set_fact:
stop_targets: "{{ host_apps | default([]) | selectattr('name', 'equalto', app) | selectattr('env', 'equalto', app_env) | list }}"
- name: Skip {{ inventory_hostname }} — no host_apps row for {{ app }}/{{ app_env }}
ansible.builtin.debug:
msg: "skip {{ inventory_hostname }}: no host_apps entry for app={{ app }} env={{ app_env }}"
when: stop_targets | length == 0
- name: Skip node-static {{ app }}/{{ app_env }} (no compose project)
ansible.builtin.debug:
msg: >-
{{ app }} is node-static. stop.sh only compose-downs Django apps.
Drop the host_apps row (or set enabled: false) and re-run deploy.sh
so web-static regenerates nginx without that vhost.
when:
- stop_targets | length > 0
- app in node_names
- name: Stop Django (docker compose) apps
ansible.builtin.include_tasks: stop_django.yml
loop: "{{ stop_targets | selectattr('name', 'in', django_names) | list }}"
loop_control:
loop_var: app_item
label: "{{ app_item.name }}/{{ app_item.env }}"
+72
View File
@@ -0,0 +1,72 @@
---
# Compose-down one Django app+env. Called per item with loop_var app_item.
# No `down -v` — volumes stay. Checkout and secrets stay.
- name: "stop[{{ app_item.name }}/{{ app_item.env }}] locals"
ansible.builtin.set_fact:
_spec: "{{ app_catalog[app_item.name] }}"
_src: "{{ apps_src_dir }}/{{ app_item.name }}_{{ app_item.env }}"
_project: "{{ app_item.name }}_{{ app_item.env }}"
- name: "stop[{{ _project }}] checkout dir"
ansible.builtin.stat:
path: "{{ _src }}"
register: _src_stat
- name: "stop[{{ _project }}] compose file"
ansible.builtin.stat:
path: "{{ _src }}/{{ _spec.compose_file }}"
register: _compose_stat
when: _src_stat.stat.exists
- name: "stop[{{ _project }}] env file"
ansible.builtin.stat:
path: "{{ _src }}/.env"
register: _env_stat
when: _src_stat.stat.exists
- name: "stop[{{ _project }}] use compose down"
ansible.builtin.set_fact:
_use_compose: "{{ _src_stat.stat.exists and _compose_stat is defined and _compose_stat.stat.exists }}"
- name: "stop[{{ _project }}] compose down"
ansible.builtin.command:
cmd: >-
docker compose -f {{ _spec.compose_file }}
{{ '--env-file .env' if (_env_stat is defined and _env_stat.stat.exists) else '' }}
down
chdir: "{{ _src }}"
environment:
COMPOSE_PROJECT_NAME: "{{ _project }}"
WEB_PORT: "{{ app_item.port }}"
COMPOSE_PROFILES: "{{ (app_item.compose_profiles | default([])) | join(',') }}"
become: true
become_user: "{{ admin_user }}"
when: _use_compose | bool
changed_when: true
- name: "stop[{{ _project }}] find leftover project containers"
ansible.builtin.command:
cmd: docker ps -aq --filter label=com.docker.compose.project={{ _project }}
register: _leftovers
changed_when: false
become: true
when: not (_use_compose | bool)
- name: "stop[{{ _project }}] remove leftover project containers"
ansible.builtin.command:
cmd: "docker rm -f {{ _leftovers.stdout_lines | join(' ') }}"
become: true
when:
- not (_use_compose | bool)
- _leftovers is defined
- _leftovers.stdout_lines | length > 0
changed_when: true
- name: "stop[{{ _project }}] nothing to stop (no checkout)"
ansible.builtin.debug:
msg: "no checkout at {{ _src }} and no containers for project {{ _project }}"
when:
- not (_use_compose | bool)
- _leftovers is defined
- _leftovers.stdout_lines | length == 0
+12 -1
View File
@@ -6,9 +6,20 @@
ansible.builtin.set_fact: ansible.builtin.set_fact:
_node_names: "{{ app_catalog | dict2items | selectattr('value.type', 'equalto', 'node-static') | map(attribute='key') | list }}" _node_names: "{{ app_catalog | dict2items | selectattr('value.type', 'equalto', 'node-static') | map(attribute='key') | list }}"
# rejectattr('enabled', …) raises when the key is omitted. Omit = enabled (#35).
- name: web-static | reset static apps
ansible.builtin.set_fact:
_static_apps: []
- name: web-static | this host's static apps - name: web-static | this host's static apps
ansible.builtin.set_fact: ansible.builtin.set_fact:
_static_apps: "{{ host_apps | default([]) | selectattr('name', 'in', _node_names) | list }}" _static_apps: "{{ _static_apps + [item] }}"
loop: "{{ host_apps | default([]) }}"
when:
- item.name in _node_names
- item.enabled | default(true)
loop_control:
label: "{{ item.name }}/{{ item.env }}"
- name: web-static | configure and run - name: web-static | configure and run
when: _static_apps | length > 0 when: _static_apps | length > 0
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
LIMIT=""
EXTRA_ARGS=()
EXTRA_VARS=()
APP=""
APP_ENV=""
usage() {
cat <<EOF
Usage: $(basename "$0") [HOST] --app NAME --env ENV [OPTIONS]
Stop a docker-compose app (compose down). Inverse of deploy.sh.
Does not remove volumes, checkouts, or secrets. Next deploy.sh of the same
app+env will start it again unless host_apps sets enabled: false.
Node-static apps have no compose project — stop.sh skips them.
HOST Optional. Limit to one host: adama, roslin, starbuck, apollo, or ai-server-4080.
Options:
--app NAME Required. App to stop (same names as deploy.sh)
--env ENV Required. Environment: beta or prod
--check Dry run
--diff Show diffs
--extra-vars V Pass raw extra vars
-h, --help Show this help
Examples:
$(basename "$0") --app print_forge --env prod
$(basename "$0") --app print_forge --env beta
$(basename "$0") adama --app chat_backend --env beta --check
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
--app)
APP="$2"
EXTRA_VARS+=(-e "app=$2")
shift 2
;;
--env)
APP_ENV="$2"
EXTRA_VARS+=(-e "app_env=$2")
shift 2
;;
--check)
EXTRA_ARGS+=(--check)
shift
;;
--diff)
EXTRA_ARGS+=(--diff)
shift
;;
--extra-vars)
EXTRA_VARS+=(-e "$2")
shift 2
;;
adama|roslin|starbuck|apollo|ai-server-4080)
LIMIT="$1"
shift
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$APP" || -z "$APP_ENV" ]]; then
echo "error: --app and --env are required" >&2
usage >&2
exit 1
fi
CMD=(ansible-playbook playbooks/stop-apps.yml "${EXTRA_ARGS[@]}" "${EXTRA_VARS[@]}")
if [[ -n "$LIMIT" ]]; then
CMD+=(--limit "$LIMIT")
echo "==> Stopping ${APP}/${APP_ENV} on: $LIMIT"
else
echo "==> Stopping ${APP}/${APP_ENV} on all webservers"
fi
exec "${CMD[@]}"