updates
This commit is contained in:
+125
-56
@@ -7,10 +7,9 @@ Ansible-based provisioning and deployment for homelab web servers.
|
|||||||
```mermaid
|
```mermaid
|
||||||
flowchart TB
|
flowchart TB
|
||||||
subgraph provision ["Provisioning (manual / rare)"]
|
subgraph provision ["Provisioning (manual / rare)"]
|
||||||
Desktop1["Ubuntu Desktop\n(control node)"]
|
Control1["ai-server-4080\n(control node)"]
|
||||||
Desktop1 -->|ansible-playbook site.yml| Adama
|
Control1 -->|ansible-playbook site.yml| Adama
|
||||||
Desktop1 -->|ansible-playbook site.yml| Roslin
|
Control1 -->|ansible-playbook site.yml| Roslin
|
||||||
Desktop1 -->|ansible-playbook site.yml| DesktopTarget
|
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph cicd ["CI/CD (every merge to master)"]
|
subgraph cicd ["CI/CD (every merge to master)"]
|
||||||
@@ -20,14 +19,13 @@ flowchart TB
|
|||||||
Deploy --> AnsibleDeploy["ansible-playbook deploy-apps.yml"]
|
Deploy --> AnsibleDeploy["ansible-playbook deploy-apps.yml"]
|
||||||
AnsibleDeploy --> Adama2["adama"]
|
AnsibleDeploy --> Adama2["adama"]
|
||||||
AnsibleDeploy --> Roslin2["roslin"]
|
AnsibleDeploy --> Roslin2["roslin"]
|
||||||
AnsibleDeploy --> Desktop2["desktop"]
|
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
| Pipeline | When | Playbook | Where it runs |
|
| Pipeline | When | Playbook | Where it runs |
|
||||||
|----------|------|----------|---------------|
|
|----------|------|----------|---------------|
|
||||||
| **Provision** | New VM, OS change, firewall, Docker install | `site.yml` | Desktop — 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 desktop |
|
| **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`).
|
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 |
|
| Name | IP | Role |
|
||||||
|------|-----|------|
|
|------|-----|------|
|
||||||
| adama | 10.0.0.77 | Ubuntu Server VM (Proxmox) |
|
| adama | 10.0.0.77 | Ubuntu Server VM (Proxmox) — app host |
|
||||||
| roslin | 10.0.0.176 | Ubuntu Server VM (Proxmox) |
|
| roslin | 10.0.0.176 | Ubuntu Server VM (Proxmox) — app host |
|
||||||
| desktop | *see `host_vars/desktop.yml`* | Ubuntu Desktop — control node + deployment target |
|
| ai-server-4080 | 10.0.0.128 | Control node + Gitea act runner (no app workloads) |
|
||||||
|
|
||||||
Hostname on this machine: `ryan-development-1`
|
Hostname on this machine: `ryan-development-1`
|
||||||
|
|
||||||
@@ -52,18 +50,23 @@ server-infra/
|
|||||||
├── inventory/
|
├── inventory/
|
||||||
│ ├── hosts.yml
|
│ ├── hosts.yml
|
||||||
│ ├── group_vars/
|
│ ├── group_vars/
|
||||||
│ │ ├── all.yml
|
│ │ └── all.yml # vars + app_catalog
|
||||||
│ │ └── webservers.yml
|
|
||||||
│ └── host_vars/
|
│ └── 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/
|
├── playbooks/
|
||||||
│ ├── site.yml # Phase 1: provision
|
│ ├── site.yml # Phase 1: provision
|
||||||
│ └── deploy-apps.yml # Phase 2: CI deploy (stub)
|
│ └── deploy-apps.yml # Phase 2: CI deploy
|
||||||
├── roles/
|
├── roles/
|
||||||
│ ├── common/ # Base packages
|
│ ├── common/ # Base packages
|
||||||
│ ├── ufw/ # Firewall
|
│ ├── ufw/ # Firewall
|
||||||
│ ├── docker/ # Docker CE + compose plugin
|
│ ├── 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/
|
└── 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
|
||||||
@@ -74,7 +77,7 @@ server-infra/
|
|||||||
Ansible needs SSH + sudo on each target before playbooks work.
|
Ansible needs SSH + sudo on each target before playbooks work.
|
||||||
|
|
||||||
1. Create `westfarn` on each VM with sudo membership.
|
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
|
```bash
|
||||||
ssh-copy-id westfarn@10.0.0.77
|
ssh-copy-id westfarn@10.0.0.77
|
||||||
ssh-copy-id westfarn@10.0.0.176
|
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.77
|
||||||
ssh westfarn@10.0.0.176
|
ssh westfarn@10.0.0.176
|
||||||
```
|
```
|
||||||
4. On the desktop (control node), install Ansible:
|
4. On ai-server-4080 (control node), install Ansible:
|
||||||
```bash
|
```bash
|
||||||
sudo apt update && sudo apt install -y ansible
|
sudo apt update && sudo apt install -y ansible
|
||||||
# or: pip install ansible
|
# or: pip install ansible
|
||||||
@@ -94,7 +97,7 @@ Ansible needs SSH + sudo on each target before playbooks work.
|
|||||||
cd ~/Documents/repos/server-infra
|
cd ~/Documents/repos/server-infra
|
||||||
ansible-galaxy collection install -r requirements.yml
|
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
|
## Testing on a Single Server
|
||||||
|
|
||||||
@@ -118,7 +121,7 @@ ansible adama -m ping
|
|||||||
|
|
||||||
# Same for other hosts
|
# Same for other hosts
|
||||||
./scripts/provision.sh roslin
|
./scripts/provision.sh roslin
|
||||||
./scripts/provision.sh desktop
|
./scripts/provision.sh ai-server-4080
|
||||||
```
|
```
|
||||||
|
|
||||||
### Provision all hosts
|
### Provision all hosts
|
||||||
@@ -145,6 +148,9 @@ Applies roles in order to the `webservers` group:
|
|||||||
| `common` | apt update, git, python3, pip, curl, ca-certificates |
|
| `common` | apt update, git, python3, pip, curl, ca-certificates |
|
||||||
| `ufw` | Firewall: SSH from LAN only, HTTP/HTTPS public |
|
| `ufw` | Firewall: SSH from LAN only, HTTP/HTTPS public |
|
||||||
| `docker` | Docker CE, compose plugin, add `westfarn` to `docker` group |
|
| `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
|
### 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`)
|
## Phase 2: CI Deploy (`deploy-apps.yml`)
|
||||||
|
|
||||||
Not fully implemented yet. Planned flow:
|
### Apps
|
||||||
|
|
||||||
1. Gitea push triggers unit tests.
|
| App | Type | Hosts | Notes |
|
||||||
2. On success, Act runner on desktop runs `deploy-apps.yml`.
|
|-----|------|-------|-------|
|
||||||
3. Ansible fans out to all `webservers` hosts.
|
| `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
|
### Data model
|
||||||
# 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 }}
|
|
||||||
|
|
||||||
- name: Deploy to all webservers
|
- `app_catalog` (`group_vars/all.yml`) — how each app is built (repo, type, compose file, migrate cmd).
|
||||||
run: |
|
- `host_apps` (`host_vars/<host>.yml`) — which app+env+port runs on that host.
|
||||||
~/Documents/repos/server-infra/scripts/deploy.sh \
|
- Django app = one compose project per env: project name `<app>_<env>`, host port from `host_apps`.
|
||||||
--extra-vars "app_ref=${{ gitea.event.workflow_run.head_sha }}"
|
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.
|
### Flow
|
||||||
2. `docker compose pull && docker compose up -d`.
|
|
||||||
3. Optional health check.
|
|
||||||
|
|
||||||
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
|
## 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.
|
- One orchestration point.
|
||||||
- VMs only run containers; no runner needed on them for deploy fan-out.
|
- 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 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 |
|
| Requirement | Why |
|
||||||
|-------------|-----|
|
|-------------|-----|
|
||||||
| Ansible | Run `deploy-apps.yml` |
|
| Ansible | Run `deploy-apps.yml` |
|
||||||
| `server-infra` checkout | Playbooks + inventory |
|
| `server-infra` checkout | Playbooks + inventory |
|
||||||
| SSH key to all hosts | Including loopback to desktop |
|
| SSH key to adama + roslin | Deploy fan-out |
|
||||||
| Docker | Build images before push to hosts (Phase 2) |
|
|
||||||
|
|
||||||
## SSH Keys for CI Deploy
|
## 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 |
|
| # | Task | Status |
|
||||||
|---|------|--------|
|
|---|------|--------|
|
||||||
| 1 | Create `server-infra` repo | Done |
|
| 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 |
|
| 3 | Bootstrap SSH to adama + roslin | Manual |
|
||||||
| 4 | `site.yml` → common, ufw, docker | Done |
|
| 4 | `site.yml` → common, ufw, docker | Done |
|
||||||
| 5 | Verify `ansible webservers -m ping` | Manual |
|
| 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
|
## Open Decisions
|
||||||
|
|
||||||
1. **Desktop LAN IP** — set in `inventory/host_vars/desktop.yml`.
|
1. **Deploy user** — `westfarn` vs dedicated `deploy` for CI.
|
||||||
2. **Same app on all three?** — prod mirror vs adama=prod / roslin=staging / desktop=dev.
|
2. **NPM load balancing** — confirm jc21 NPM can express adama+roslin upstreams (Advanced tab), else active/active is just two independent instances.
|
||||||
3. **Deploy user** — `westfarn` vs dedicated `deploy` for CI.
|
3. **Secrets** — Ansible Vault vs per-host env files (currently per-host `/opt/apps/env/*.env`).
|
||||||
4. **Gitea URL** — for clone URLs in `app-deploy` role.
|
|
||||||
5. **Reverse proxy** — Caddy/nginx on host before containers? Affects Phase 2.
|
|
||||||
|
|
||||||
## Adding a New VM
|
## Adding a New VM
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,17 @@ Ansible provisioning and deployment for homelab web servers.
|
|||||||
# Install collections (once)
|
# Install collections (once)
|
||||||
ansible-galaxy collection install -r requirements.yml
|
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
|
# Test connectivity to one host
|
||||||
ansible adama -m ping
|
ansible adama -m ping
|
||||||
|
|
||||||
# Provision one host (dry run first)
|
# 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
|
./scripts/provision.sh adama
|
||||||
|
|
||||||
# Provision all hosts
|
# Provision all hosts
|
||||||
@@ -23,8 +29,8 @@ See [IMPLEMENTATION.md](IMPLEMENTATION.md) for full architecture, CI/CD plan, an
|
|||||||
|
|
||||||
## Servers
|
## Servers
|
||||||
|
|
||||||
| Host | IP |
|
| Host | IP | Role |
|
||||||
|------|-----|
|
|------|-----|------|
|
||||||
| adama | 10.0.0.77 |
|
| adama | 10.0.0.77 | app host |
|
||||||
| roslin | 10.0.0.176 |
|
| roslin | 10.0.0.176 | app host |
|
||||||
| desktop | see `inventory/host_vars/desktop.yml` |
|
| ai-server-4080 | 10.0.0.128 | control node + act runner |
|
||||||
|
|||||||
+2
-1
@@ -3,7 +3,8 @@ inventory = inventory/hosts.yml
|
|||||||
roles_path = roles
|
roles_path = roles
|
||||||
host_key_checking = False
|
host_key_checking = False
|
||||||
retry_files_enabled = False
|
retry_files_enabled = False
|
||||||
stdout_callback = yaml
|
stdout_callback = ansible.builtin.default
|
||||||
|
callback_result_format = yaml
|
||||||
interpreter_python = auto_silent
|
interpreter_python = auto_silent
|
||||||
|
|
||||||
[privilege_escalation]
|
[privilege_escalation]
|
||||||
|
|||||||
@@ -14,3 +14,57 @@ ufw_allowed_tcp_ports:
|
|||||||
# Docker
|
# Docker
|
||||||
docker_users:
|
docker_users:
|
||||||
- "{{ admin_user }}"
|
- "{{ 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
|
ansible_host: 10.0.0.77
|
||||||
roslin:
|
roslin:
|
||||||
ansible_host: 10.0.0.176
|
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)
|
# Phase 2: CI-triggered app deployment.
|
||||||
- name: Deploy application to webservers
|
#
|
||||||
|
# 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
|
hosts: webservers
|
||||||
become: true
|
become: true
|
||||||
roles:
|
roles:
|
||||||
- app-deploy
|
- app-deploy
|
||||||
|
- web-static
|
||||||
|
|||||||
@@ -6,3 +6,6 @@
|
|||||||
- common
|
- common
|
||||||
- ufw
|
- ufw
|
||||||
- docker
|
- 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.
|
# Generic app deployment.
|
||||||
# Interim: can rsync/systemd like company_site/scripts/deploy.sh
|
#
|
||||||
|
# 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:
|
ansible.builtin.debug:
|
||||||
msg: >-
|
msg: "ref={{ app_ref | default('(per-app default branch)') }} targets={{ deploy_targets | map(attribute='name') | zip(deploy_targets | map(attribute='env')) | list }}"
|
||||||
app-deploy role is a stub. Set app_ref={{ app_ref | default('unset') }}.
|
|
||||||
Implement git pull / docker compose after dockerize ticket.
|
- 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
|
- name: Update apt cache
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
update_cache: true
|
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
|
- name: Add Docker apt repository
|
||||||
ansible.builtin.apt_repository:
|
ansible.builtin.deb822_repository:
|
||||||
repo: "deb [arch={{ docker_apt_arch }} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
|
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
|
state: present
|
||||||
filename: docker
|
|
||||||
vars:
|
|
||||||
docker_apt_arch: "{{ 'arm64' if ansible_architecture == 'aarch64' else 'amd64' }}"
|
|
||||||
|
|
||||||
- name: Install Docker packages
|
- name: Install Docker packages
|
||||||
ansible.builtin.apt:
|
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
|
cat <<EOF
|
||||||
Usage: $(basename "$0") [HOST] [OPTIONS]
|
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:
|
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
|
--check Dry run
|
||||||
--diff Show diffs
|
--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
|
-h, --help Show this help
|
||||||
|
|
||||||
Examples:
|
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") adama --check
|
||||||
$(basename "$0") --extra-vars app_ref=master
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +43,18 @@ while [[ $# -gt 0 ]]; do
|
|||||||
usage
|
usage
|
||||||
exit 0
|
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)
|
--check)
|
||||||
EXTRA_ARGS+=(--check)
|
EXTRA_ARGS+=(--check)
|
||||||
shift
|
shift
|
||||||
@@ -46,7 +67,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
EXTRA_VARS+=(-e "$2")
|
EXTRA_VARS+=(-e "$2")
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
adama|roslin|desktop)
|
adama|roslin|ai-server-4080)
|
||||||
LIMIT="$1"
|
LIMIT="$1"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
|||||||
+56
-2
@@ -13,17 +13,21 @@ Usage: $(basename "$0") [HOST] [OPTIONS]
|
|||||||
|
|
||||||
Provision server(s) with site.yml.
|
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.
|
Omit to run against all webservers.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--check Dry run (ansible --check)
|
--check Dry run (ansible --check)
|
||||||
--diff Show diffs
|
--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
|
-h, --help Show this help
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
$(basename "$0") adama --check # dry run on adama only
|
$(basename "$0") adama --check # dry run on adama only
|
||||||
$(basename "$0") adama # provision adama
|
$(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
|
$(basename "$0") # provision all hosts
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@@ -42,7 +46,15 @@ while [[ $# -gt 0 ]]; do
|
|||||||
EXTRA_ARGS+=(--diff)
|
EXTRA_ARGS+=(--diff)
|
||||||
shift
|
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"
|
LIMIT="$1"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
@@ -54,6 +66,48 @@ while [[ $# -gt 0 ]]; do
|
|||||||
esac
|
esac
|
||||||
done
|
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[@]}")
|
CMD=(ansible-playbook playbooks/site.yml "${EXTRA_ARGS[@]}")
|
||||||
if [[ -n "$LIMIT" ]]; then
|
if [[ -n "$LIMIT" ]]; then
|
||||||
CMD+=(--limit "$LIMIT")
|
CMD+=(--limit "$LIMIT")
|
||||||
|
|||||||
Reference in New Issue
Block a user