Files
server-infra/IMPLEMENTATION.md
T
2026-07-06 15:34:49 -05:00

8.0 KiB

Server Infrastructure — Implementation Guide

Ansible-based provisioning and deployment for homelab web servers.

Architecture

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:
    ssh-copy-id westfarn@10.0.0.77
    ssh-copy-id westfarn@10.0.0.176
    
  3. Confirm passwordless SSH:
    ssh westfarn@10.0.0.77
    ssh westfarn@10.0.0.176
    
  4. On the desktop (control node), install Ansible:
    sudo apt update && sudo apt install -y ansible
    # or: pip install ansible
    
  5. Install Galaxy collections:
    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

./scripts/provision.sh adama --check   # dry run
ansible adama -m ping

Provision one host

# 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

./scripts/provision.sh

Deploy to one host (Phase 2)

./scripts/deploy.sh adama
./scripts/deploy.sh --check roslin

Under the hood, scripts pass --limit <hostname> 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

# 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.

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 userwestfarn 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@<new-ip>.
  3. Test: ./scripts/provision.sh <hostname> --check.
  4. Provision: ./scripts/provision.sh <hostname>.
  5. Deploys automatically include new host once in webservers group.