Initial server-infra setup
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
*.retry
|
||||
.vault_pass
|
||||
.ansible-vault-pass
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
@@ -0,0 +1,265 @@
|
||||
# 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 <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
|
||||
|
||||
```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@<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.
|
||||
@@ -0,0 +1,30 @@
|
||||
# server-infra
|
||||
|
||||
Ansible provisioning and deployment for homelab web servers.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install collections (once)
|
||||
ansible-galaxy collection install -r requirements.yml
|
||||
|
||||
# Test connectivity to one host
|
||||
ansible adama -m ping
|
||||
|
||||
# Provision one host (dry run first)
|
||||
./scripts/provision.sh adama --check
|
||||
./scripts/provision.sh adama
|
||||
|
||||
# Provision all hosts
|
||||
./scripts/provision.sh
|
||||
```
|
||||
|
||||
See [IMPLEMENTATION.md](IMPLEMENTATION.md) for full architecture, CI/CD plan, and phase breakdown.
|
||||
|
||||
## Servers
|
||||
|
||||
| Host | IP |
|
||||
|------|-----|
|
||||
| adama | 10.0.0.77 |
|
||||
| roslin | 10.0.0.176 |
|
||||
| desktop | see `inventory/host_vars/desktop.yml` |
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
[defaults]
|
||||
inventory = inventory/hosts.yml
|
||||
roles_path = roles
|
||||
host_key_checking = False
|
||||
retry_files_enabled = False
|
||||
stdout_callback = yaml
|
||||
interpreter_python = auto_silent
|
||||
|
||||
[privilege_escalation]
|
||||
become = True
|
||||
become_method = sudo
|
||||
become_user = root
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
ansible_user: westfarn
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
|
||||
admin_user: westfarn
|
||||
|
||||
# UFW
|
||||
ufw_ssh_port: 22
|
||||
ufw_ssh_allowed_network: 10.0.0.0/24
|
||||
ufw_allowed_tcp_ports:
|
||||
- 80
|
||||
- 443
|
||||
|
||||
# Docker
|
||||
docker_users:
|
||||
- "{{ admin_user }}"
|
||||
@@ -0,0 +1,2 @@
|
||||
---
|
||||
# Shared webserver settings (add per-environment overrides here later)
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
# 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 @@
|
||||
---
|
||||
all:
|
||||
children:
|
||||
webservers:
|
||||
hosts:
|
||||
adama:
|
||||
ansible_host: 10.0.0.77
|
||||
roslin:
|
||||
ansible_host: 10.0.0.176
|
||||
desktop:
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
# Phase 2: CI-triggered app deployment (stub)
|
||||
- name: Deploy application to webservers
|
||||
hosts: webservers
|
||||
become: true
|
||||
roles:
|
||||
- app-deploy
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
- name: Provision webservers
|
||||
hosts: webservers
|
||||
become: true
|
||||
roles:
|
||||
- common
|
||||
- ufw
|
||||
- docker
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
collections:
|
||||
- name: community.general
|
||||
version: ">=8.0.0"
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
# Stub — implement after company_site is dockerized.
|
||||
# Interim: can rsync/systemd like company_site/scripts/deploy.sh
|
||||
|
||||
- name: App deploy not yet implemented
|
||||
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.
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
- name: Update apt cache
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
|
||||
- name: Install base packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- git
|
||||
- python3
|
||||
- python3-pip
|
||||
- python3-venv
|
||||
- curl
|
||||
- ca-certificates
|
||||
- gnupg
|
||||
- apt-transport-https
|
||||
- software-properties-common
|
||||
state: present
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
- 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"
|
||||
state: present
|
||||
filename: docker
|
||||
vars:
|
||||
docker_apt_arch: "{{ 'arm64' if ansible_architecture == 'aarch64' else 'amd64' }}"
|
||||
|
||||
- name: Install Docker packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- docker-ce
|
||||
- docker-ce-cli
|
||||
- containerd.io
|
||||
- docker-buildx-plugin
|
||||
- docker-compose-plugin
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Ensure Docker service is enabled and running
|
||||
ansible.builtin.service:
|
||||
name: docker
|
||||
state: started
|
||||
enabled: true
|
||||
|
||||
- name: Add users to docker group
|
||||
ansible.builtin.user:
|
||||
name: "{{ item }}"
|
||||
groups: docker
|
||||
append: true
|
||||
loop: "{{ docker_users }}"
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
- name: Install ufw
|
||||
ansible.builtin.apt:
|
||||
name: ufw
|
||||
state: present
|
||||
|
||||
- name: Set UFW default incoming policy to deny
|
||||
community.general.ufw:
|
||||
direction: incoming
|
||||
policy: deny
|
||||
|
||||
- name: Set UFW default outgoing policy to allow
|
||||
community.general.ufw:
|
||||
direction: outgoing
|
||||
policy: allow
|
||||
|
||||
- name: Allow SSH from LAN only
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "{{ ufw_ssh_port }}"
|
||||
proto: tcp
|
||||
from_ip: "{{ ufw_ssh_allowed_network }}"
|
||||
|
||||
- name: Allow HTTP and HTTPS
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "{{ item }}"
|
||||
proto: tcp
|
||||
loop: "{{ ufw_allowed_tcp_ports }}"
|
||||
|
||||
- name: Enable UFW
|
||||
community.general.ufw:
|
||||
state: enabled
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
LIMIT=""
|
||||
EXTRA_ARGS=()
|
||||
EXTRA_VARS=()
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [HOST] [OPTIONS]
|
||||
|
||||
Deploy application with deploy-apps.yml (Phase 2 stub).
|
||||
|
||||
HOST Optional. Limit to one host: adama, roslin, or desktop.
|
||||
|
||||
Options:
|
||||
--check Dry run
|
||||
--diff Show diffs
|
||||
--extra-vars V Pass extra vars (e.g. app_ref=abc123)
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
$(basename "$0") adama --check
|
||||
$(basename "$0") --extra-vars app_ref=master
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--check)
|
||||
EXTRA_ARGS+=(--check)
|
||||
shift
|
||||
;;
|
||||
--diff)
|
||||
EXTRA_ARGS+=(--diff)
|
||||
shift
|
||||
;;
|
||||
--extra-vars)
|
||||
EXTRA_VARS+=(-e "$2")
|
||||
shift 2
|
||||
;;
|
||||
adama|roslin|desktop)
|
||||
LIMIT="$1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
CMD=(ansible-playbook playbooks/deploy-apps.yml "${EXTRA_ARGS[@]}" "${EXTRA_VARS[@]}")
|
||||
if [[ -n "$LIMIT" ]]; then
|
||||
CMD+=(--limit "$LIMIT")
|
||||
echo "==> Deploying to: $LIMIT"
|
||||
else
|
||||
echo "==> Deploying to all webservers"
|
||||
fi
|
||||
|
||||
exec "${CMD[@]}"
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
LIMIT=""
|
||||
EXTRA_ARGS=()
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [HOST] [OPTIONS]
|
||||
|
||||
Provision server(s) with site.yml.
|
||||
|
||||
HOST Optional. Limit to one host: adama, roslin, or desktop.
|
||||
Omit to run against all webservers.
|
||||
|
||||
Options:
|
||||
--check Dry run (ansible --check)
|
||||
--diff Show diffs
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
$(basename "$0") adama --check # dry run on adama only
|
||||
$(basename "$0") adama # provision adama
|
||||
$(basename "$0") # provision all hosts
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--check)
|
||||
EXTRA_ARGS+=(--check)
|
||||
shift
|
||||
;;
|
||||
--diff)
|
||||
EXTRA_ARGS+=(--diff)
|
||||
shift
|
||||
;;
|
||||
adama|roslin|desktop)
|
||||
LIMIT="$1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
CMD=(ansible-playbook playbooks/site.yml "${EXTRA_ARGS[@]}")
|
||||
if [[ -n "$LIMIT" ]]; then
|
||||
CMD+=(--limit "$LIMIT")
|
||||
echo "==> Targeting host: $LIMIT"
|
||||
else
|
||||
echo "==> Targeting all webservers"
|
||||
fi
|
||||
|
||||
exec "${CMD[@]}"
|
||||
Reference in New Issue
Block a user