updates
This commit is contained in:
+125
-56
@@ -7,10 +7,9 @@ Ansible-based provisioning and deployment for homelab web servers.
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph provision ["Provisioning (manual / rare)"]
|
||||
Desktop1["Ubuntu Desktop\n(control node)"]
|
||||
Desktop1 -->|ansible-playbook site.yml| Adama
|
||||
Desktop1 -->|ansible-playbook site.yml| Roslin
|
||||
Desktop1 -->|ansible-playbook site.yml| DesktopTarget
|
||||
Control1["ai-server-4080\n(control node)"]
|
||||
Control1 -->|ansible-playbook site.yml| Adama
|
||||
Control1 -->|ansible-playbook site.yml| Roslin
|
||||
end
|
||||
|
||||
subgraph cicd ["CI/CD (every merge to master)"]
|
||||
@@ -20,14 +19,13 @@ flowchart TB
|
||||
Deploy --> AnsibleDeploy["ansible-playbook deploy-apps.yml"]
|
||||
AnsibleDeploy --> Adama2["adama"]
|
||||
AnsibleDeploy --> Roslin2["roslin"]
|
||||
AnsibleDeploy --> Desktop2["desktop"]
|
||||
end
|
||||
```
|
||||
|
||||
| Pipeline | When | Playbook | Where it runs |
|
||||
|----------|------|----------|---------------|
|
||||
| **Provision** | New VM, OS change, firewall, Docker install | `site.yml` | Desktop — run manually |
|
||||
| **Deploy** | Green unit tests on `master` | `deploy-apps.yml` | Gitea Act runner on desktop |
|
||||
| **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 |
|
||||
|
||||
Both pipelines share the same inventory (`inventory/hosts.yml`).
|
||||
|
||||
@@ -35,9 +33,9 @@ Both pipelines share the same inventory (`inventory/hosts.yml`).
|
||||
|
||||
| Name | IP | Role |
|
||||
|------|-----|------|
|
||||
| adama | 10.0.0.77 | Ubuntu Server VM (Proxmox) |
|
||||
| roslin | 10.0.0.176 | Ubuntu Server VM (Proxmox) |
|
||||
| desktop | *see `host_vars/desktop.yml`* | Ubuntu Desktop — control node + deployment target |
|
||||
| adama | 10.0.0.77 | Ubuntu Server VM (Proxmox) — app host |
|
||||
| roslin | 10.0.0.176 | Ubuntu Server VM (Proxmox) — app host |
|
||||
| ai-server-4080 | 10.0.0.128 | Control node + Gitea act runner (no app workloads) |
|
||||
|
||||
Hostname on this machine: `ryan-development-1`
|
||||
|
||||
@@ -52,18 +50,23 @@ server-infra/
|
||||
├── inventory/
|
||||
│ ├── hosts.yml
|
||||
│ ├── group_vars/
|
||||
│ │ ├── all.yml
|
||||
│ │ └── webservers.yml
|
||||
│ │ └── all.yml # vars + app_catalog
|
||||
│ └── host_vars/
|
||||
│ └── desktop.yml
|
||||
│ ├── adama.yml # host_apps (django + dta_webapp)
|
||||
│ ├── roslin.yml # host_apps (mirrors adama)
|
||||
│ └── ai-server-4080.yml # control node / act runner, no workloads
|
||||
├── playbooks/
|
||||
│ ├── site.yml # Phase 1: provision
|
||||
│ └── deploy-apps.yml # Phase 2: CI deploy (stub)
|
||||
│ └── deploy-apps.yml # Phase 2: CI deploy
|
||||
├── roles/
|
||||
│ ├── common/ # Base packages
|
||||
│ ├── ufw/ # Firewall
|
||||
│ ├── docker/ # Docker CE + compose plugin
|
||||
│ └── app-deploy/ # App deploy (stub for Phase 2)
|
||||
│ ├── nodejs/ # Node.js + npm + npx (NodeSource)
|
||||
│ ├── gitea-key/ # per-server SSH key + Gitea access probe
|
||||
│ ├── tianji/ # Monitoring reporter
|
||||
│ ├── app-deploy/ # django (docker) + node-static deploy
|
||||
│ └── web-static/ # nginx container serving /var/www builds
|
||||
└── scripts/
|
||||
├── provision.sh # Wrapper with --limit support
|
||||
└── deploy.sh # Wrapper for deploy playbook
|
||||
@@ -74,7 +77,7 @@ server-infra/
|
||||
Ansible needs SSH + sudo on each target before playbooks work.
|
||||
|
||||
1. Create `westfarn` on each VM with sudo membership.
|
||||
2. Copy your SSH public key from the desktop:
|
||||
2. Copy your SSH public key from the control node (ai-server-4080):
|
||||
```bash
|
||||
ssh-copy-id westfarn@10.0.0.77
|
||||
ssh-copy-id westfarn@10.0.0.176
|
||||
@@ -84,7 +87,7 @@ Ansible needs SSH + sudo on each target before playbooks work.
|
||||
ssh westfarn@10.0.0.77
|
||||
ssh westfarn@10.0.0.176
|
||||
```
|
||||
4. On the desktop (control node), install Ansible:
|
||||
4. On ai-server-4080 (control node), install Ansible:
|
||||
```bash
|
||||
sudo apt update && sudo apt install -y ansible
|
||||
# or: pip install ansible
|
||||
@@ -94,7 +97,7 @@ Ansible needs SSH + sudo on each target before playbooks work.
|
||||
cd ~/Documents/repos/server-infra
|
||||
ansible-galaxy collection install -r requirements.yml
|
||||
```
|
||||
6. Update `inventory/host_vars/desktop.yml` with this machine's LAN IP.
|
||||
6. Update `inventory/host_vars/ai-server-4080.yml` with this machine's LAN IP (`ansible_host`).
|
||||
|
||||
## Testing on a Single Server
|
||||
|
||||
@@ -118,7 +121,7 @@ ansible adama -m ping
|
||||
|
||||
# Same for other hosts
|
||||
./scripts/provision.sh roslin
|
||||
./scripts/provision.sh desktop
|
||||
./scripts/provision.sh ai-server-4080
|
||||
```
|
||||
|
||||
### Provision all hosts
|
||||
@@ -145,6 +148,9 @@ Applies roles in order to the `webservers` group:
|
||||
| `common` | apt update, git, python3, pip, curl, ca-certificates |
|
||||
| `ufw` | Firewall: SSH from LAN only, HTTP/HTTPS public |
|
||||
| `docker` | Docker CE, compose plugin, add `westfarn` to `docker` group |
|
||||
| `nodejs` | Node.js + npm + npx (NodeSource) for `dta_webapp` builds |
|
||||
| `gitea-key` | Per-server SSH key + Gitea access probe |
|
||||
| `tianji` | Monitoring reporter |
|
||||
|
||||
### UFW rules
|
||||
|
||||
@@ -161,56 +167,121 @@ After Docker install, re-SSH so the `docker` group membership takes effect.
|
||||
|
||||
## Phase 2: CI Deploy (`deploy-apps.yml`)
|
||||
|
||||
Not fully implemented yet. Planned flow:
|
||||
### Apps
|
||||
|
||||
1. Gitea push triggers unit tests.
|
||||
2. On success, Act runner on desktop runs `deploy-apps.yml`.
|
||||
3. Ansible fans out to all `webservers` hosts.
|
||||
| App | Type | Hosts | Notes |
|
||||
|-----|------|-------|-------|
|
||||
| `company_site` | django (docker) | adama + roslin | active/active behind NPM |
|
||||
| `dta_service` | django (docker) | adama + roslin | active/active behind NPM |
|
||||
| `dta_webapp` | node/vite static | adama + roslin | active/active; built to `/var/www/<env>_dta_webapp`, served by web-static nginx |
|
||||
|
||||
### Planned `company_site` workflow change
|
||||
Both environments (`beta`, `prod`) are deployed. Django apps use a **shared external
|
||||
Postgres** (via `DATABASE_URL` in each host's env file) so active/active replicas
|
||||
share one database.
|
||||
|
||||
```yaml
|
||||
# company_site/.gitea/workflows/deploy.yml (future)
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ gitea.event.workflow_run.head_sha }}
|
||||
### Data model
|
||||
|
||||
- name: Deploy to all webservers
|
||||
run: |
|
||||
~/Documents/repos/server-infra/scripts/deploy.sh \
|
||||
--extra-vars "app_ref=${{ gitea.event.workflow_run.head_sha }}"
|
||||
```
|
||||
- `app_catalog` (`group_vars/all.yml`) — how each app is built (repo, type, compose file, migrate cmd).
|
||||
- `host_apps` (`host_vars/<host>.yml`) — which app+env+port runs on that host.
|
||||
- Django app = one compose project per env: project name `<app>_<env>`, host port from `host_apps`.
|
||||
Ports match across adama/roslin so NPM can balance `adama:PORT` + `roslin:PORT`.
|
||||
|
||||
### Planned `app-deploy` role (post-dockerize)
|
||||
### Ports
|
||||
|
||||
Per host:
|
||||
| App | beta | prod |
|
||||
|-----|------|------|
|
||||
| company_site | 8010 | 8000 |
|
||||
| dta_service | 8011 | 8001 |
|
||||
| dta_webapp (nginx) | 8081 | 8080 |
|
||||
|
||||
1. Clone or pull app repo at pinned SHA.
|
||||
2. `docker compose pull && docker compose up -d`.
|
||||
3. Optional health check.
|
||||
### Flow
|
||||
|
||||
Pre-dockerize interim: role can rsync/systemd like current `company_site/scripts/deploy.sh`.
|
||||
1. Gitea push to `master` → repo's `.gitea/workflows` runs tests.
|
||||
2. On green, deploy job on the self-hosted runner calls:
|
||||
```bash
|
||||
~/Documents/repos/server-infra/scripts/deploy.sh \
|
||||
--app company_site --env prod --ref "${{ gitea.sha }}"
|
||||
```
|
||||
3. `deploy-apps.yml` runs against `webservers`; each host deploys only the
|
||||
matching app+env from its `host_apps`.
|
||||
|
||||
### `app-deploy` role behavior
|
||||
|
||||
- **django**: push per-app secret from control node `{{ secrets_dir }}/<app>/<app>_<env>.env`
|
||||
to host `{{ apps_env_dir }}` → git checkout at ref → copy `.env` into checkout →
|
||||
`docker compose build` → `up -d` → migrate (run once, shared DB).
|
||||
- **node-static**: git checkout at ref → `npm ci` → `npm run build:<env>`
|
||||
(writes to `/var/www/<env>_dta_webapp`).
|
||||
- **web-static** role: one nginx container per app host (adama + roslin) serving
|
||||
the static roots on their ports; NPM balances across both.
|
||||
|
||||
### Reverse proxy / load balancing (NPM at 10.0.0.230)
|
||||
|
||||
Ansible does **not** manage NPM. It only guarantees stable host ports. In NPM you
|
||||
point each domain at the backend(s):
|
||||
|
||||
- Single host: standard Proxy Host → `adama:PORT`.
|
||||
- Active/active: jc21 NPM's UI Proxy Host is single-target. To balance
|
||||
adama+roslin you need the **Advanced** tab with a custom `upstream {}` block
|
||||
(or a real LB). Confirm this before relying on active/active.
|
||||
|
||||
### Required changes IN each app repo (owned separately)
|
||||
|
||||
- [ ] `docker-compose.prod.yml`: drop the bundled `db` service; `web` reads
|
||||
`DATABASE_URL` / `DB_HOST` pointing at the shared external Postgres.
|
||||
- [ ] Each app has its own database + user on the shared Postgres.
|
||||
- [ ] `.gitea/workflows/deploy.yml`: replace the local `scripts/deploy.sh` step
|
||||
with a call to `server-infra/scripts/deploy.sh --app <name> --env <env> --ref <sha>`
|
||||
(keep the test/docker jobs).
|
||||
- [ ] `dta_webapp`: `npm run build:beta` / `build:prod` output to
|
||||
`/var/www/beta_dta_webapp` / `/var/www/prod_dta_webapp`.
|
||||
|
||||
### Shared Postgres (10.0.0.230, same box as NPM)
|
||||
|
||||
One shared instance; each app+env gets its own database (beta and prod MUST NOT
|
||||
share a DB — active/active replicas of the same env share one DB, different envs
|
||||
do not).
|
||||
|
||||
| app | env | database | DATABASE_URL |
|
||||
|-----|-----|----------|--------------|
|
||||
| company_site | prod | `company_site` | `postgres://westfarn:<pw>@10.0.0.230:5432/company_site` |
|
||||
| company_site | beta | `company_site_beta` | `postgres://westfarn:<pw>@10.0.0.230:5432/company_site_beta` |
|
||||
| dta_service | prod | `dta_service` | `postgres://westfarn:<pw>@10.0.0.230:5432/dta_service` |
|
||||
| dta_service | beta | `dta_service_beta` | `postgres://westfarn:<pw>@10.0.0.230:5432/dta_service_beta` |
|
||||
|
||||
Server prereqs on 10.0.0.230: create the 4 DBs + grant `westfarn`;
|
||||
`listen_addresses` covers LAN; `pg_hba.conf` allows `10.0.0.0/24`; firewall opens
|
||||
5432 to `10.0.0.0/24` only.
|
||||
|
||||
### One-time host bootstrap (per target)
|
||||
|
||||
- [x] Gitea SSH key: the `gitea-key` role (in `site.yml`) generates a key per
|
||||
server, configures SSH for port 30009, probes access, and — if the server
|
||||
can't reach Gitea yet — prints the public key to add and stops. Add the key
|
||||
(Gitea user SSH keys, or repo Deploy Keys) and re-run provisioning.
|
||||
- [ ] Create control-node secrets `{{ secrets_dir }}/<app>/<app>_<env>.env`
|
||||
(default `~/Documents/secrets/<app>/<app>_<env>.env`) with `DATABASE_URL`
|
||||
(see table), `DJANGO_ENV`, `DJANGO_SECRET_KEY`, `WEB_PORT` (matching the port
|
||||
table). Deploy pushes these to `/opt/apps/env/<app>_<env>.env` (mode 600) on
|
||||
adama + roslin. Never committed to git.
|
||||
- [x] Node.js/npm/npx for the `dta_webapp` build — installed by the `nodejs`
|
||||
role in `site.yml` (NodeSource, `node_major` default 20).
|
||||
|
||||
## Gitea Act Runner
|
||||
|
||||
**Recommended:** Single self-hosted runner on the desktop.
|
||||
**Recommended:** Single self-hosted runner on ai-server-4080.
|
||||
|
||||
- One build artifact, one orchestration point.
|
||||
- VMs only run containers; no runner needed on them for deploy fan-out.
|
||||
- One orchestration point.
|
||||
- App hosts (adama/roslin) run the workloads; no runner needed on them for deploy fan-out.
|
||||
- Runner needs: Ansible, this repo checked out, SSH key to all hosts, vault password (later).
|
||||
|
||||
### Runner requirements on desktop
|
||||
### Runner requirements on ai-server-4080
|
||||
|
||||
| Requirement | Why |
|
||||
|-------------|-----|
|
||||
| Ansible | Run `deploy-apps.yml` |
|
||||
| `server-infra` checkout | Playbooks + inventory |
|
||||
| SSH key to all hosts | Including loopback to desktop |
|
||||
| Docker | Build images before push to hosts (Phase 2) |
|
||||
| SSH key to adama + roslin | Deploy fan-out |
|
||||
|
||||
## SSH Keys for CI Deploy
|
||||
|
||||
@@ -237,7 +308,7 @@ Store vault password for CI in a file readable only by the Act runner (e.g. `~/.
|
||||
| # | Task | Status |
|
||||
|---|------|--------|
|
||||
| 1 | Create `server-infra` repo | Done |
|
||||
| 2 | Inventory with all 3 hosts | Done — update desktop IP |
|
||||
| 2 | Inventory with all 3 hosts | Done |
|
||||
| 3 | Bootstrap SSH to adama + roslin | Manual |
|
||||
| 4 | `site.yml` → common, ufw, docker | Done |
|
||||
| 5 | Verify `ansible webservers -m ping` | Manual |
|
||||
@@ -250,11 +321,9 @@ Store vault password for CI in a file readable only by the Act runner (e.g. `~/.
|
||||
|
||||
## Open Decisions
|
||||
|
||||
1. **Desktop LAN IP** — set in `inventory/host_vars/desktop.yml`.
|
||||
2. **Same app on all three?** — prod mirror vs adama=prod / roslin=staging / desktop=dev.
|
||||
3. **Deploy user** — `westfarn` vs dedicated `deploy` for CI.
|
||||
4. **Gitea URL** — for clone URLs in `app-deploy` role.
|
||||
5. **Reverse proxy** — Caddy/nginx on host before containers? Affects Phase 2.
|
||||
1. **Deploy user** — `westfarn` vs dedicated `deploy` for CI.
|
||||
2. **NPM load balancing** — confirm jc21 NPM can express adama+roslin upstreams (Advanced tab), else active/active is just two independent instances.
|
||||
3. **Secrets** — Ansible Vault vs per-host env files (currently per-host `/opt/apps/env/*.env`).
|
||||
|
||||
## Adding a New VM
|
||||
|
||||
|
||||
@@ -8,11 +8,17 @@ Ansible provisioning and deployment for homelab web servers.
|
||||
# Install collections (once)
|
||||
ansible-galaxy collection install -r requirements.yml
|
||||
|
||||
# Bootstrap SSH key to each host (one-time, before Ansible)
|
||||
ssh-copy-id westfarn@10.0.0.77
|
||||
|
||||
# Test connectivity to one host
|
||||
ansible adama -m ping
|
||||
|
||||
# Provision one host (dry run first)
|
||||
./scripts/provision.sh adama --check
|
||||
./scripts/provision.sh adama --check --ask-become-pass
|
||||
./scripts/provision.sh adama --ask-become-pass # first run; sudo password once
|
||||
|
||||
# Later runs (after common role sets passwordless sudo)
|
||||
./scripts/provision.sh adama
|
||||
|
||||
# Provision all hosts
|
||||
@@ -23,8 +29,8 @@ See [IMPLEMENTATION.md](IMPLEMENTATION.md) for full architecture, CI/CD plan, an
|
||||
|
||||
## Servers
|
||||
|
||||
| Host | IP |
|
||||
|------|-----|
|
||||
| adama | 10.0.0.77 |
|
||||
| roslin | 10.0.0.176 |
|
||||
| desktop | see `inventory/host_vars/desktop.yml` |
|
||||
| Host | IP | Role |
|
||||
|------|-----|------|
|
||||
| adama | 10.0.0.77 | app host |
|
||||
| roslin | 10.0.0.176 | app host |
|
||||
| ai-server-4080 | 10.0.0.128 | control node + act runner |
|
||||
|
||||
+2
-1
@@ -3,7 +3,8 @@ inventory = inventory/hosts.yml
|
||||
roles_path = roles
|
||||
host_key_checking = False
|
||||
retry_files_enabled = False
|
||||
stdout_callback = yaml
|
||||
stdout_callback = ansible.builtin.default
|
||||
callback_result_format = yaml
|
||||
interpreter_python = auto_silent
|
||||
|
||||
[privilege_escalation]
|
||||
|
||||
@@ -14,3 +14,57 @@ ufw_allowed_tcp_ports:
|
||||
# Docker
|
||||
docker_users:
|
||||
- "{{ admin_user }}"
|
||||
|
||||
# Tianji monitoring
|
||||
tianji_server_url: https://tianji.aimloperations.com
|
||||
tianji_workspace_id: cm7w8087y020lddswyhamadj2
|
||||
tianji_install_script_url: "https://tianji.aimloperations.com/serverStatus/{{ tianji_workspace_id }}/install.sh?url={{ tianji_server_url }}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# App deployment (Phase 2)
|
||||
# ---------------------------------------------------------------------------
|
||||
git_base_url: "ssh://git@git.aimloperations.com:30009"
|
||||
|
||||
# Where app checkouts and per-app env files live on each target host.
|
||||
apps_base_dir: /opt/apps
|
||||
apps_env_dir: "{{ apps_base_dir }}/env" # persistent .env files, never in git
|
||||
apps_src_dir: "{{ apps_base_dir }}/src" # git checkouts
|
||||
|
||||
# Control-node directory holding per-app .env secrets, pushed to hosts at
|
||||
# deploy time. Layout: <secrets_dir>/<app>/<app>_<env>.env (never in git).
|
||||
secrets_dir: "{{ lookup('ansible.builtin.env', 'HOME') }}/Documents/secrets"
|
||||
|
||||
# Static site document root base (dta_webapp writes build output here).
|
||||
web_static_root: /var/www
|
||||
|
||||
# Catalog of deployable apps. host_apps (per host_vars) references these by name.
|
||||
app_catalog:
|
||||
company_site:
|
||||
type: django
|
||||
repo: "{{ git_base_url }}/ai_ml_operations/company_site.git"
|
||||
default_branch: master
|
||||
compose_file: docker-compose.prod.yml
|
||||
web_service: web
|
||||
migrate_cmd: "uv run python manage.py migrate --noinput"
|
||||
dta_service:
|
||||
type: django
|
||||
repo: "{{ git_base_url }}/Ditch_The_Agent/dta_service.git"
|
||||
default_branch: main
|
||||
compose_file: docker-compose.prod.yml
|
||||
web_service: web
|
||||
migrate_cmd: "uv run python manage.py migrate --noinput"
|
||||
dta_webapp:
|
||||
type: node-static
|
||||
repo: "{{ git_base_url }}/Ditch_The_Agent/dta_webapp.git"
|
||||
default_branch: main
|
||||
# package.json lives in this subdir of the checkout (npm runs here).
|
||||
subdir: ditch-the-agent
|
||||
# Document root served by nginx and written by `npm run build:<env>`.
|
||||
# {env} is replaced with the entry's env (beta/prod).
|
||||
webroot_pattern: "/var/www/{env}.app.ditchtheagent/html"
|
||||
# deploy runs `npm ci` then `npm run build:<env>`; that script writes to
|
||||
# {{ web_static_root }}/<env>_dta_webapp (beta/prod), served by web-static.
|
||||
|
||||
# Deploy filter vars. CI passes these; manual runs may leave them undefined
|
||||
# to (re)deploy every app listed in the host's host_apps.
|
||||
# app: <name> app_env: beta|prod app_ref: <git sha or branch, default master>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
# Django services run active/active here and on roslin (shared external DB).
|
||||
# dta_webapp static also runs active/active (built into /var/www, served by
|
||||
# the web-static nginx container). Ports MUST match roslin so NPM can balance.
|
||||
# Each entry is one workload: django -> compose project <name>_<env> on port;
|
||||
# node-static -> /var/www/<env>_dta_webapp served on port.
|
||||
host_apps:
|
||||
- { name: company_site, env: prod, port: 8000 }
|
||||
- { name: company_site, env: beta, port: 8010 }
|
||||
- { name: dta_service, env: prod, port: 8001 }
|
||||
- { name: dta_service, env: beta, port: 8011 }
|
||||
- { name: dta_webapp, env: prod, port: 8080 }
|
||||
- { name: dta_webapp, env: beta, port: 8081 }
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
# ai-server-4080 — control node + Gitea act runner. Update IP with:
|
||||
# ip -4 addr show scope global
|
||||
ansible_host: 10.0.0.128
|
||||
|
||||
ansible_control_node: true
|
||||
act_runner_enabled: true
|
||||
|
||||
# This host's pre-existing ~/.ssh/id_ed25519 is a personal key WITH a passphrase,
|
||||
# which hangs the (non-BatchMode) gitea access probe. Use a dedicated,
|
||||
# passphrase-less deploy key here instead.
|
||||
gitea_key_path: "/home/{{ admin_user }}/.ssh/gitea_deploy"
|
||||
|
||||
# company_site prod runs here for side testing.
|
||||
host_apps:
|
||||
- { name: company_site, env: prod, port: 8000 }
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
# Update with this machine's LAN IP: ip -4 addr show scope global
|
||||
ansible_host: 10.0.0.1 # FIXME: set your desktop IP
|
||||
|
||||
ansible_control_node: true
|
||||
act_runner_enabled: true
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
# Mirrors adama for active/active. Ports MUST match adama so NPM upstreams
|
||||
# can balance adama:PORT and roslin:PORT for the same workload.
|
||||
host_apps:
|
||||
- { name: company_site, env: prod, port: 8000 }
|
||||
- { name: company_site, env: beta, port: 8010 }
|
||||
- { name: dta_service, env: prod, port: 8001 }
|
||||
- { name: dta_service, env: beta, port: 8011 }
|
||||
- { name: dta_webapp, env: prod, port: 8080 }
|
||||
- { name: dta_webapp, env: beta, port: 8081 }
|
||||
+2
-1
@@ -7,4 +7,5 @@ all:
|
||||
ansible_host: 10.0.0.77
|
||||
roslin:
|
||||
ansible_host: 10.0.0.176
|
||||
desktop:
|
||||
ai-server-4080:
|
||||
ansible_host: 10.0.0.128
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
---
|
||||
# Phase 2: CI-triggered app deployment (stub)
|
||||
- name: Deploy application to webservers
|
||||
# Phase 2: CI-triggered app deployment.
|
||||
#
|
||||
# Runs against all webservers; each host deploys only the apps listed in its
|
||||
# host_apps. Deploy exactly one app+env at a pinned ref via extra-vars:
|
||||
#
|
||||
# ansible-playbook playbooks/deploy-apps.yml \
|
||||
# -e app=company_site -e app_env=prod -e app_ref=<sha>
|
||||
#
|
||||
# Or redeploy everything on master by omitting app/app_env/app_ref.
|
||||
- name: Deploy applications
|
||||
hosts: webservers
|
||||
become: true
|
||||
roles:
|
||||
- app-deploy
|
||||
- web-static
|
||||
|
||||
@@ -6,3 +6,6 @@
|
||||
- common
|
||||
- ufw
|
||||
- docker
|
||||
- nodejs
|
||||
- gitea-key
|
||||
- tianji
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
# Deploy one Django app+env as its own docker compose project.
|
||||
# Called per item with loop_var app_item = { name, env, port }.
|
||||
|
||||
- name: "django[{{ 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 }}"
|
||||
_envfile: "{{ apps_env_dir }}/{{ app_item.name }}_{{ app_item.env }}.env"
|
||||
_secret_src: "{{ secrets_dir }}/{{ app_item.name }}/{{ app_item.name }}_{{ app_item.env }}.env"
|
||||
_project: "{{ app_item.name }}_{{ app_item.env }}"
|
||||
_ref: "{{ app_ref | default(app_catalog[app_item.name].default_branch) }}"
|
||||
|
||||
- name: "django[{{ _project }}] ensure base dirs"
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ admin_user }}"
|
||||
group: "{{ admin_user }}"
|
||||
mode: "0750"
|
||||
loop:
|
||||
- "{{ apps_src_dir }}"
|
||||
- "{{ apps_env_dir }}"
|
||||
|
||||
- name: "django[{{ _project }}] check local secret exists"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ _secret_src }}"
|
||||
register: _secret_stat
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
|
||||
- name: "django[{{ _project }}] fail when local secret missing"
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
Missing local secret {{ _secret_src }} on the control node.
|
||||
Create it (DATABASE_URL to the shared external Postgres, DJANGO_SECRET_KEY,
|
||||
WEB_PORT={{ app_item.port }}, etc.) before deploying. It is never committed
|
||||
to git.
|
||||
when: not _secret_stat.stat.exists
|
||||
|
||||
- name: "django[{{ _project }}] push secret to {{ _envfile }}"
|
||||
ansible.builtin.copy:
|
||||
src: "{{ _secret_src }}"
|
||||
dest: "{{ _envfile }}"
|
||||
owner: "{{ admin_user }}"
|
||||
group: "{{ admin_user }}"
|
||||
mode: "0600"
|
||||
|
||||
- name: "django[{{ _project }}] checkout {{ _ref }}"
|
||||
ansible.builtin.git:
|
||||
repo: "{{ _spec.repo }}"
|
||||
dest: "{{ _src }}"
|
||||
version: "{{ _ref }}"
|
||||
force: true
|
||||
accept_hostkey: true
|
||||
become: true
|
||||
become_user: "{{ admin_user }}"
|
||||
|
||||
- name: "django[{{ _project }}] install .env into checkout"
|
||||
ansible.builtin.copy:
|
||||
src: "{{ _envfile }}"
|
||||
dest: "{{ _src }}/.env"
|
||||
remote_src: true
|
||||
owner: "{{ admin_user }}"
|
||||
group: "{{ admin_user }}"
|
||||
mode: "0600"
|
||||
|
||||
- name: "django[{{ _project }}] build images"
|
||||
ansible.builtin.command:
|
||||
cmd: "docker compose -f {{ _spec.compose_file }} --env-file .env build"
|
||||
chdir: "{{ _src }}"
|
||||
environment:
|
||||
COMPOSE_PROJECT_NAME: "{{ _project }}"
|
||||
WEB_PORT: "{{ app_item.port }}"
|
||||
become: true
|
||||
become_user: "{{ admin_user }}"
|
||||
changed_when: true
|
||||
|
||||
- name: "django[{{ _project }}] start containers"
|
||||
ansible.builtin.command:
|
||||
cmd: "docker compose -f {{ _spec.compose_file }} --env-file .env up -d --remove-orphans"
|
||||
chdir: "{{ _src }}"
|
||||
environment:
|
||||
COMPOSE_PROJECT_NAME: "{{ _project }}"
|
||||
WEB_PORT: "{{ app_item.port }}"
|
||||
become: true
|
||||
become_user: "{{ admin_user }}"
|
||||
changed_when: true
|
||||
@@ -1,9 +1,56 @@
|
||||
---
|
||||
# Stub — implement after company_site is dockerized.
|
||||
# Interim: can rsync/systemd like company_site/scripts/deploy.sh
|
||||
# Generic app deployment.
|
||||
#
|
||||
# CI passes: app=<name> app_env=<beta|prod> app_ref=<git sha>
|
||||
# Manual run with none of those redeploys every app in this host's host_apps
|
||||
# at branch master.
|
||||
#
|
||||
# host_apps (host_vars) lists what runs on THIS host; app_catalog (group_vars)
|
||||
# describes how each app is built.
|
||||
|
||||
- name: App deploy not yet implemented
|
||||
- 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: Resolve deploy targets for {{ inventory_hostname }}
|
||||
ansible.builtin.set_fact:
|
||||
deploy_targets: >-
|
||||
{{ (host_apps | default([]) | selectattr('name', 'equalto', app | default('')) | selectattr('env', 'equalto', app_env | default('')) | list)
|
||||
if (app is defined and app_env is defined)
|
||||
else (host_apps | default([])) }}
|
||||
|
||||
- name: Show deploy targets
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
app-deploy role is a stub. Set app_ref={{ app_ref | default('unset') }}.
|
||||
Implement git pull / docker compose after dockerize ticket.
|
||||
msg: "ref={{ app_ref | default('(per-app default branch)') }} targets={{ deploy_targets | map(attribute='name') | zip(deploy_targets | map(attribute='env')) | list }}"
|
||||
|
||||
- name: Deploy Django (docker compose) apps
|
||||
ansible.builtin.include_tasks: django.yml
|
||||
loop: "{{ deploy_targets | selectattr('name', 'in', django_names) | list }}"
|
||||
loop_control:
|
||||
loop_var: app_item
|
||||
label: "{{ app_item.name }}/{{ app_item.env }}"
|
||||
|
||||
- name: Run Django migrations once (shared external DB)
|
||||
ansible.builtin.command:
|
||||
cmd: >-
|
||||
docker compose -f {{ app_catalog[item.name].compose_file }} --env-file .env
|
||||
exec -T {{ app_catalog[item.name].web_service }}
|
||||
{{ app_catalog[item.name].migrate_cmd }}
|
||||
chdir: "{{ apps_src_dir }}/{{ item.name }}_{{ item.env }}"
|
||||
environment:
|
||||
COMPOSE_PROJECT_NAME: "{{ item.name }}_{{ item.env }}"
|
||||
loop: "{{ deploy_targets | selectattr('name', 'in', django_names) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}/{{ item.env }}"
|
||||
become: true
|
||||
become_user: "{{ admin_user }}"
|
||||
run_once: true
|
||||
changed_when: true
|
||||
|
||||
- name: Deploy node static apps
|
||||
ansible.builtin.include_tasks: node_static.yml
|
||||
loop: "{{ deploy_targets | selectattr('name', 'in', node_names) | list }}"
|
||||
loop_control:
|
||||
loop_var: app_item
|
||||
label: "{{ app_item.name }}/{{ app_item.env }}"
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
# Build one node/vite static site into {{ web_static_root }}/<env>_<app>.
|
||||
# Called per item with loop_var app_item = { name, env, port }.
|
||||
# The app's own `npm run build:<env>` script is expected to output to
|
||||
# {{ web_static_root }}/<env>_dta_webapp.
|
||||
|
||||
- name: "static[{{ 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 }}"
|
||||
_webroot: "{{ (app_catalog[app_item.name].webroot_pattern | default(web_static_root ~ '/{env}_' ~ app_item.name)) | replace('{env}', app_item.env) }}"
|
||||
_ref: "{{ app_ref | default(app_catalog[app_item.name].default_branch) }}"
|
||||
|
||||
- name: "static[{{ app_item.name }}/{{ app_item.env }}] workdir"
|
||||
ansible.builtin.set_fact:
|
||||
_workdir: "{{ (_src ~ '/' ~ _spec.subdir) if (_spec.subdir | default('')) else _src }}"
|
||||
|
||||
- name: "static[{{ app_item.name }}/{{ app_item.env }}] ensure dirs"
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ admin_user }}"
|
||||
group: "{{ admin_user }}"
|
||||
mode: "0755"
|
||||
loop:
|
||||
- "{{ apps_src_dir }}"
|
||||
- "{{ _webroot }}"
|
||||
|
||||
- name: "static[{{ app_item.name }}/{{ app_item.env }}] checkout {{ _ref }}"
|
||||
ansible.builtin.git:
|
||||
repo: "{{ _spec.repo }}"
|
||||
dest: "{{ _src }}"
|
||||
version: "{{ _ref }}"
|
||||
force: true
|
||||
accept_hostkey: true
|
||||
become: true
|
||||
become_user: "{{ admin_user }}"
|
||||
|
||||
- name: "static[{{ app_item.name }}/{{ app_item.env }}] npm ci"
|
||||
ansible.builtin.command:
|
||||
cmd: npm ci
|
||||
chdir: "{{ _workdir }}"
|
||||
become: true
|
||||
become_user: "{{ admin_user }}"
|
||||
changed_when: true
|
||||
|
||||
- name: "static[{{ app_item.name }}/{{ app_item.env }}] build ({{ app_item.env }})"
|
||||
ansible.builtin.command:
|
||||
cmd: "npm run build:{{ app_item.env }}"
|
||||
chdir: "{{ _workdir }}"
|
||||
become: true
|
||||
become_user: "{{ admin_user }}"
|
||||
changed_when: true
|
||||
@@ -1,4 +1,11 @@
|
||||
---
|
||||
- name: Allow admin user passwordless sudo for Ansible
|
||||
ansible.builtin.copy:
|
||||
dest: "/etc/sudoers.d/{{ admin_user }}"
|
||||
content: "{{ admin_user }} ALL=(ALL) NOPASSWD:ALL\n"
|
||||
mode: "0440"
|
||||
validate: visudo -cf %s
|
||||
|
||||
- name: Update apt cache
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
---
|
||||
- name: Create keyrings directory
|
||||
ansible.builtin.file:
|
||||
path: /etc/apt/keyrings
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Add Docker GPG key
|
||||
ansible.builtin.get_url:
|
||||
url: https://download.docker.com/linux/ubuntu/gpg
|
||||
dest: /etc/apt/keyrings/docker.asc
|
||||
mode: "0644"
|
||||
|
||||
- name: Add Docker apt repository
|
||||
ansible.builtin.apt_repository:
|
||||
repo: "deb [arch={{ docker_apt_arch }} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
|
||||
ansible.builtin.deb822_repository:
|
||||
name: docker
|
||||
types: deb
|
||||
uris: "https://download.docker.com/linux/{{ ansible_distribution | lower }}"
|
||||
suites: "{{ ansible_distribution_release }}"
|
||||
components: stable
|
||||
architectures: "{{ 'arm64' if ansible_architecture == 'aarch64' else 'amd64' }}"
|
||||
signed_by: "https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg"
|
||||
state: present
|
||||
filename: docker
|
||||
vars:
|
||||
docker_apt_arch: "{{ 'arm64' if ansible_architecture == 'aarch64' else 'amd64' }}"
|
||||
|
||||
- name: Install Docker packages
|
||||
ansible.builtin.apt:
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
gitea_ssh_host: git.aimloperations.com
|
||||
gitea_ssh_port: 30009
|
||||
gitea_key_path: "/home/{{ admin_user }}/.ssh/id_ed25519"
|
||||
# Any repo the deploy user should be able to read; used only as an access probe.
|
||||
gitea_test_repo: "ai_ml_operations/company_site.git"
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
# Per-server SSH key for cloning from Gitea.
|
||||
# 1. Generate an ed25519 key for the deploy user (if absent).
|
||||
# 2. Configure SSH for the Gitea host (port + identity).
|
||||
# 3. Test access first; only surface the "add this key" step when it's missing.
|
||||
|
||||
- name: gitea-key | ensure .ssh dir
|
||||
ansible.builtin.file:
|
||||
path: "/home/{{ admin_user }}/.ssh"
|
||||
state: directory
|
||||
owner: "{{ admin_user }}"
|
||||
group: "{{ admin_user }}"
|
||||
mode: "0700"
|
||||
|
||||
- name: gitea-key | generate deploy key
|
||||
become_user: "{{ admin_user }}"
|
||||
ansible.builtin.command:
|
||||
cmd: "ssh-keygen -t ed25519 -N '' -f {{ gitea_key_path }} -C '{{ admin_user }}@{{ inventory_hostname }}-gitea'"
|
||||
creates: "{{ gitea_key_path }}"
|
||||
|
||||
- name: gitea-key | configure SSH for Gitea host
|
||||
become_user: "{{ admin_user }}"
|
||||
ansible.builtin.blockinfile:
|
||||
path: "/home/{{ admin_user }}/.ssh/config"
|
||||
create: true
|
||||
owner: "{{ admin_user }}"
|
||||
group: "{{ admin_user }}"
|
||||
mode: "0600"
|
||||
marker: "# {mark} ANSIBLE MANAGED gitea"
|
||||
block: |
|
||||
Host {{ gitea_ssh_host }}
|
||||
User git
|
||||
Port {{ gitea_ssh_port }}
|
||||
IdentityFile {{ gitea_key_path }}
|
||||
IdentitiesOnly yes
|
||||
StrictHostKeyChecking accept-new
|
||||
|
||||
- name: gitea-key | read public key
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ gitea_key_path }}.pub"
|
||||
register: _gitea_pubkey
|
||||
|
||||
- name: gitea-key | check Gitea access (permission probe)
|
||||
become_user: "{{ admin_user }}"
|
||||
ansible.builtin.command:
|
||||
cmd: "git ls-remote {{ git_base_url }}/{{ gitea_test_repo }}"
|
||||
register: _gitea_access
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: gitea-key | access OK
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ inventory_hostname }} already has Gitea access; nothing to add."
|
||||
when: _gitea_access.rc == 0
|
||||
|
||||
- name: gitea-key | ADD THIS KEY to Gitea (access missing)
|
||||
ansible.builtin.debug:
|
||||
msg: |
|
||||
================= ACTION REQUIRED on {{ inventory_hostname }} =================
|
||||
This server cannot reach Gitea yet. Add its public key:
|
||||
|
||||
{{ _gitea_pubkey.content | b64decode | trim }}
|
||||
|
||||
Where (either works):
|
||||
* User key : Gitea > Settings > SSH / GPG Keys > Add Key
|
||||
* Deploy key: repo > Settings > Deploy Keys (per repo, read-only)
|
||||
Then re-run provisioning to continue.
|
||||
==============================================================================
|
||||
when: _gitea_access.rc != 0
|
||||
|
||||
- name: gitea-key | fail until key is added
|
||||
ansible.builtin.fail:
|
||||
msg: "No Gitea access from {{ inventory_hostname }}. Add the key shown above, then re-run."
|
||||
when: _gitea_access.rc != 0
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
# Node.js major version to install from NodeSource (provides npm + npx).
|
||||
node_major: 22
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
- name: Add NodeSource apt repository
|
||||
ansible.builtin.deb822_repository:
|
||||
name: nodesource
|
||||
types: deb
|
||||
uris: "https://deb.nodesource.com/node_{{ node_major }}.x"
|
||||
suites: nodistro
|
||||
components: main
|
||||
signed_by: "https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key"
|
||||
state: present
|
||||
register: _nodesource_repo
|
||||
|
||||
- name: Force NodeSource over the distro nodejs/npm
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/apt/preferences.d/nodesource.pref
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
Package: *
|
||||
Pin: origin "deb.nodesource.com"
|
||||
Pin-Priority: 1001
|
||||
register: _nodesource_pin
|
||||
|
||||
- name: Refresh apt cache after NodeSource changes
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
when: _nodesource_repo is changed or _nodesource_pin is changed
|
||||
|
||||
- name: Remove distro nodejs/npm (NodeSource nodejs bundles its own npm + npx)
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- npm
|
||||
- nodejs
|
||||
state: absent
|
||||
purge: true
|
||||
|
||||
- name: Install Node.js from NodeSource (includes npm + npx)
|
||||
ansible.builtin.apt:
|
||||
name: nodejs
|
||||
state: latest
|
||||
allow_downgrade: true
|
||||
update_cache: true
|
||||
|
||||
- name: Verify node / npm / npx available
|
||||
ansible.builtin.command:
|
||||
cmd: "{{ item }} --version"
|
||||
loop:
|
||||
- node
|
||||
- npm
|
||||
- npx
|
||||
changed_when: false
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
- name: Install Tianji monitoring reporter
|
||||
ansible.builtin.shell: |
|
||||
curl -o- "{{ tianji_install_script_url }}" | bash
|
||||
args:
|
||||
creates: /usr/lib/systemd/system/tianji-reporter.service
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
web_static_dir: "{{ apps_base_dir }}/web-static"
|
||||
web_static_image: nginx:alpine
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
# Serves node/vite static builds (dta_webapp beta/prod) via a single nginx
|
||||
# container. Skips itself on hosts that have no node-static apps.
|
||||
|
||||
- name: web-static | node-static app names
|
||||
ansible.builtin.set_fact:
|
||||
_node_names: "{{ app_catalog | dict2items | selectattr('value.type', 'equalto', 'node-static') | map(attribute='key') | list }}"
|
||||
|
||||
- name: web-static | this host's static apps
|
||||
ansible.builtin.set_fact:
|
||||
_static_apps: "{{ host_apps | default([]) | selectattr('name', 'in', _node_names) | list }}"
|
||||
|
||||
- name: web-static | configure and run
|
||||
when: _static_apps | length > 0
|
||||
block:
|
||||
- name: web-static | ensure project dir
|
||||
ansible.builtin.file:
|
||||
path: "{{ web_static_dir }}"
|
||||
state: directory
|
||||
owner: "{{ admin_user }}"
|
||||
group: "{{ admin_user }}"
|
||||
mode: "0750"
|
||||
|
||||
- name: web-static | nginx config
|
||||
ansible.builtin.template:
|
||||
src: nginx.conf.j2
|
||||
dest: "{{ web_static_dir }}/nginx.conf"
|
||||
owner: "{{ admin_user }}"
|
||||
group: "{{ admin_user }}"
|
||||
mode: "0644"
|
||||
register: _nginx_conf
|
||||
|
||||
- name: web-static | compose file
|
||||
ansible.builtin.template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ web_static_dir }}/docker-compose.yml"
|
||||
owner: "{{ admin_user }}"
|
||||
group: "{{ admin_user }}"
|
||||
mode: "0644"
|
||||
register: _compose_file
|
||||
|
||||
- name: web-static | start container
|
||||
ansible.builtin.command:
|
||||
cmd: "docker compose up -d --remove-orphans{{ ' --force-recreate' if (_compose_file is changed) else '' }}"
|
||||
chdir: "{{ web_static_dir }}"
|
||||
become: true
|
||||
become_user: "{{ admin_user }}"
|
||||
changed_when: true
|
||||
|
||||
- name: web-static | reload nginx on config change
|
||||
ansible.builtin.command:
|
||||
cmd: "docker compose exec -T web-static nginx -s reload"
|
||||
chdir: "{{ web_static_dir }}"
|
||||
become: true
|
||||
become_user: "{{ admin_user }}"
|
||||
when: _nginx_conf is changed and _compose_file is not changed
|
||||
changed_when: true
|
||||
@@ -0,0 +1,12 @@
|
||||
# Managed by Ansible (roles/web-static). Do not edit by hand.
|
||||
services:
|
||||
web-static:
|
||||
image: {{ web_static_image }}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
{% for a in _static_apps %}
|
||||
- "{{ a.port }}:{{ a.port }}"
|
||||
{% endfor %}
|
||||
volumes:
|
||||
- {{ web_static_root }}:{{ web_static_root }}:ro
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
@@ -0,0 +1,19 @@
|
||||
# Managed by Ansible (roles/web-static). Do not edit by hand.
|
||||
{% for a in _static_apps %}
|
||||
server {
|
||||
listen {{ a.port }};
|
||||
server_name _;
|
||||
|
||||
root {{ (app_catalog[a.name].webroot_pattern | default(web_static_root ~ '/{env}_' ~ a.name)) | replace('{env}', a.env) }};
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location ~* \.(?:js|css|woff2?|png|jpg|jpeg|gif|svg|ico)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
}
|
||||
{% endfor %}
|
||||
+26
-5
@@ -12,19 +12,28 @@ usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [HOST] [OPTIONS]
|
||||
|
||||
Deploy application with deploy-apps.yml (Phase 2 stub).
|
||||
Deploy applications with deploy-apps.yml.
|
||||
|
||||
HOST Optional. Limit to one host: adama, roslin, or desktop.
|
||||
HOST Optional. Limit to one host: adama, roslin, or ai-server-4080.
|
||||
|
||||
Options:
|
||||
--app NAME App to deploy (company_site, dta_service, dta_webapp)
|
||||
--env ENV Environment: beta or prod
|
||||
--ref REF Git ref/sha to deploy (default: master)
|
||||
--check Dry run
|
||||
--diff Show diffs
|
||||
--extra-vars V Pass extra vars (e.g. app_ref=abc123)
|
||||
--extra-vars V Pass raw extra vars
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
# CI: deploy one app+env at a pinned sha
|
||||
$(basename "$0") --app company_site --env prod --ref abc123
|
||||
|
||||
# Redeploy everything on master (all hosts)
|
||||
$(basename "$0")
|
||||
|
||||
# Dry run for one host
|
||||
$(basename "$0") adama --check
|
||||
$(basename "$0") --extra-vars app_ref=master
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -34,6 +43,18 @@ while [[ $# -gt 0 ]]; do
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--app)
|
||||
EXTRA_VARS+=(-e "app=$2")
|
||||
shift 2
|
||||
;;
|
||||
--env)
|
||||
EXTRA_VARS+=(-e "app_env=$2")
|
||||
shift 2
|
||||
;;
|
||||
--ref)
|
||||
EXTRA_VARS+=(-e "app_ref=$2")
|
||||
shift 2
|
||||
;;
|
||||
--check)
|
||||
EXTRA_ARGS+=(--check)
|
||||
shift
|
||||
@@ -46,7 +67,7 @@ while [[ $# -gt 0 ]]; do
|
||||
EXTRA_VARS+=(-e "$2")
|
||||
shift 2
|
||||
;;
|
||||
adama|roslin|desktop)
|
||||
adama|roslin|ai-server-4080)
|
||||
LIMIT="$1"
|
||||
shift
|
||||
;;
|
||||
|
||||
+56
-2
@@ -13,17 +13,21 @@ Usage: $(basename "$0") [HOST] [OPTIONS]
|
||||
|
||||
Provision server(s) with site.yml.
|
||||
|
||||
HOST Optional. Limit to one host: adama, roslin, or desktop.
|
||||
HOST Optional. Limit to one host: adama, roslin, or ai-server-4080.
|
||||
Omit to run against all webservers.
|
||||
|
||||
Options:
|
||||
--check Dry run (ansible --check)
|
||||
--diff Show diffs
|
||||
--ask-pass Prompt for SSH password (bootstrap before ssh-copy-id)
|
||||
--ask-become-pass Prompt for sudo password
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
$(basename "$0") adama --check # dry run on adama only
|
||||
$(basename "$0") adama # provision adama
|
||||
$(basename "$0") adama --ask-pass --ask-become-pass # first run, password auth
|
||||
$(basename "$0") ai-server-4080 # provision the control node
|
||||
$(basename "$0") # provision all hosts
|
||||
EOF
|
||||
}
|
||||
@@ -42,7 +46,15 @@ while [[ $# -gt 0 ]]; do
|
||||
EXTRA_ARGS+=(--diff)
|
||||
shift
|
||||
;;
|
||||
adama|roslin|desktop)
|
||||
--ask-pass)
|
||||
EXTRA_ARGS+=(--ask-pass)
|
||||
shift
|
||||
;;
|
||||
--ask-become-pass)
|
||||
EXTRA_ARGS+=(--ask-become-pass)
|
||||
shift
|
||||
;;
|
||||
adama|roslin|ai-server-4080)
|
||||
LIMIT="$1"
|
||||
shift
|
||||
;;
|
||||
@@ -54,6 +66,48 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
ensure_ssh_key() {
|
||||
local key="${HOME}/.ssh/id_ed25519"
|
||||
|
||||
if [[ -f "$key" ]]; then
|
||||
echo "==> SSH key already exists: $key"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "==> Generating SSH key: $key"
|
||||
ssh-keygen -t ed25519 -N "" -f "$key" -C "$(whoami)@$(hostname)"
|
||||
}
|
||||
|
||||
ensure_gitea_ssh_config() {
|
||||
local ssh_dir="${HOME}/.ssh"
|
||||
local config="${ssh_dir}/config"
|
||||
|
||||
mkdir -p "$ssh_dir"
|
||||
chmod 700 "$ssh_dir"
|
||||
|
||||
if [[ -f "$config" ]] && grep -qE '^Host[[:space:]]+git\.aimloperations\.com$' "$config"; then
|
||||
echo "==> SSH config already has git.aimloperations.com"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -f "$config" && -s "$config" ]]; then
|
||||
printf '\n' >>"$config"
|
||||
fi
|
||||
|
||||
cat >>"$config" <<'EOF'
|
||||
Host git.aimloperations.com
|
||||
User git
|
||||
Port 30009
|
||||
AddressFamily inet
|
||||
EOF
|
||||
|
||||
chmod 600 "$config"
|
||||
echo "==> Added git.aimloperations.com to ${config}"
|
||||
}
|
||||
|
||||
ensure_ssh_key
|
||||
ensure_gitea_ssh_config
|
||||
|
||||
CMD=(ansible-playbook playbooks/site.yml "${EXTRA_ARGS[@]}")
|
||||
if [[ -n "$LIMIT" ]]; then
|
||||
CMD+=(--limit "$LIMIT")
|
||||
|
||||
Reference in New Issue
Block a user