Add stop.sh (compose down) and disable print_forge.
Sync runner checkout / sync (pull_request) Successful in 6s
Sync runner checkout / sync (pull_request) Successful in 6s
Closes #33. Deploy skips host_apps.enabled: false so CI cannot resurrect a stopped app.
This commit is contained in:
+38
-8
@@ -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,8 @@ 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.
|
||||||
- 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 +269,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 +302,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 +355,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 +432,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 +450,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)):
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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,22 @@
|
|||||||
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
|
||||||
|
|
||||||
|
- name: "Drop disabled host_apps (enabled: false)"
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
deploy_targets: "{{ matching_host_apps | rejectattr('enabled', 'equalto', false) | list }}"
|
||||||
|
|
||||||
|
- name: Show skipped disabled apps
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "skip disabled {{ item.name }}/{{ item.env }} on {{ inventory_hostname }}"
|
||||||
|
loop: "{{ matching_host_apps | selectattr('enabled', 'equalto', false) | list }}"
|
||||||
|
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:
|
||||||
|
|||||||
@@ -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 }}"
|
||||||
@@ -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
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
- 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: "{{ host_apps | default([]) | rejectattr('enabled', 'equalto', false) | selectattr('name', 'in', _node_names) | list }}"
|
||||||
|
|
||||||
- name: web-static | configure and run
|
- name: web-static | configure and run
|
||||||
when: _static_apps | length > 0
|
when: _static_apps | length > 0
|
||||||
|
|||||||
Executable
+95
@@ -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[@]}"
|
||||||
Reference in New Issue
Block a user