# Server Infrastructure — Implementation Guide Ansible-based provisioning and deployment for homelab web servers. ## Architecture ```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 end subgraph cicd ["CI/CD (every merge to master)"] Gitea["Gitea push"] Gitea --> Tests["Act: unit tests"] Tests --> Deploy["Act: deploy job"] 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 | Both pipelines share the same inventory (`inventory/hosts.yml`). ## Servers | 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 | Hostname on this machine: `ryan-development-1` ## Repo Layout ``` server-infra/ ├── IMPLEMENTATION.md # This file ├── README.md # Quick start ├── ansible.cfg ├── requirements.yml # Ansible Galaxy collections ├── inventory/ │ ├── hosts.yml │ ├── group_vars/ │ │ ├── all.yml │ │ └── webservers.yml │ └── host_vars/ │ └── desktop.yml ├── playbooks/ │ ├── site.yml # Phase 1: provision │ └── deploy-apps.yml # Phase 2: CI deploy (stub) ├── roles/ │ ├── common/ # Base packages │ ├── ufw/ # Firewall │ ├── docker/ # Docker CE + compose plugin │ └── app-deploy/ # App deploy (stub for Phase 2) └── scripts/ ├── provision.sh # Wrapper with --limit support └── deploy.sh # Wrapper for deploy playbook ``` ## Prerequisites (One-Time Bootstrap) 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: ```bash ssh-copy-id westfarn@10.0.0.77 ssh-copy-id westfarn@10.0.0.176 ``` 3. Confirm passwordless SSH: ```bash ssh westfarn@10.0.0.77 ssh westfarn@10.0.0.176 ``` 4. On the desktop (control node), install Ansible: ```bash sudo apt update && sudo apt install -y ansible # or: pip install ansible ``` 5. Install Galaxy collections: ```bash 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. ## Testing on a Single Server Use `--limit` to target one host without touching the others. Helper scripts wrap this. ### Ping one host ```bash ./scripts/provision.sh adama --check # dry run ansible adama -m ping ``` ### Provision one host ```bash # Dry run (no changes) ./scripts/provision.sh adama --check # Apply for real ./scripts/provision.sh adama # Same for other hosts ./scripts/provision.sh roslin ./scripts/provision.sh desktop ``` ### Provision all hosts ```bash ./scripts/provision.sh ``` ### Deploy to one host (Phase 2) ```bash ./scripts/deploy.sh adama ./scripts/deploy.sh --check roslin ``` Under the hood, scripts pass `--limit ` to `ansible-playbook`. ## Phase 1: Provision (`site.yml`) Applies roles in order to the `webservers` group: | Role | Purpose | |------|---------| | `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 | ### UFW rules | Port | Source | Purpose | |------|--------|---------| | 22 | `10.0.0.0/24` | SSH (LAN only) | | 80 | anywhere | HTTP | | 443 | anywhere | HTTPS | | default | deny incoming | Block everything else | **Warning:** Test UFW on one host first (`./scripts/provision.sh adama`). Keep a Proxmox console session open in case SSH rules lock you out. 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: 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. ### Planned `company_site` workflow change ```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 }} - name: Deploy to all webservers run: | ~/Documents/repos/server-infra/scripts/deploy.sh \ --extra-vars "app_ref=${{ gitea.event.workflow_run.head_sha }}" ``` ### Planned `app-deploy` role (post-dockerize) Per host: 1. Clone or pull app repo at pinned SHA. 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`. ## Gitea Act Runner **Recommended:** Single self-hosted runner on the desktop. - One build artifact, one orchestration point. - VMs only run containers; 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 | 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 Keys for CI Deploy | Key | Used by | Purpose | |-----|---------|---------| | Personal key | You | Manual provisioning | | Deploy key (runner) | Act → Ansible → hosts | Automated deploy | Consider a dedicated `deploy` user with limited sudo (docker only) — future hardening step. ## Secrets (Phase 2) Use Ansible Vault for production secrets. Do not commit plaintext. ```bash ansible-vault create inventory/group_vars/webservers/vault.yml ansible-playbook playbooks/site.yml --ask-vault-pass ``` Store vault password for CI in a file readable only by the Act runner (e.g. `~/.ansible-vault-pass`, mode 600). ## Implementation Order | # | Task | Status | |---|------|--------| | 1 | Create `server-infra` repo | Done | | 2 | Inventory with all 3 hosts | Done — update desktop IP | | 3 | Bootstrap SSH to adama + roslin | Manual | | 4 | `site.yml` → common, ufw, docker | Done | | 5 | Verify `ansible webservers -m ping` | Manual | | 6 | Test on single server: `./scripts/provision.sh adama` | Manual | | 7 | Provision all: `./scripts/provision.sh` | Manual | | 8 | Deploy SSH key for Act runner | Future | | 9 | Stub `deploy-apps.yml` + update `company_site` workflow | Future | | 10 | Dockerize `company_site` | Future (separate ticket) | | 11 | Gitea container registry (optional) | Future | ## 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. ## Adding a New VM 1. Add host to `inventory/hosts.yml` under `webservers`. 2. Bootstrap SSH: `ssh-copy-id westfarn@`. 3. Test: `./scripts/provision.sh --check`. 4. Provision: `./scripts/provision.sh `. 5. Deploys automatically include new host once in `webservers` group.