ansible: asxpio-services is managed by NixOS

With asxpio-services described in nixos/, Ansible only has asxpio-ops
left. Drop the runner, SillyTavern and Minecraft roles, the services
host from both inventories, and the WireGuard client play.

The s3fs role and playbook go as well: the mounts were switched off on
asxpio-ops, and on asxpio-services nothing read them. The credentials
file the role once wrote on asxpio-ops stays, because the backup script
there reads it.

site.yaml against asxpio-ops still changes nothing.

Assisted-by: Claude:fable-5-2
This commit is contained in:
Sergei Poljanski 2026-11-12 17:00:59 +04:00
commit c8e8212b48
30 changed files with 50 additions and 945 deletions

View file

@ -1,7 +1,8 @@
# Ansible
`site.yaml` is the desired state of both servers and is applied by the
deploy pipeline on every push to `main`. The playbooks under
`site.yaml` is the desired state of asxpio-ops and is applied by the
deploy pipeline on every push to `main`. asxpio-services runs NixOS and
is described in `../nixos`. The playbooks under
`playbooks/` are host bootstrap and are run by hand.
## Running
@ -23,9 +24,10 @@ Templates that carry secrets are never shown in `--diff` output.
- `inventory/hosts.yaml`: default. Connects as root to the WireGuard
addresses.
- `inventory/ci.yaml`: used by the pipeline. Connects as `deploy` over
the private network, reaching asxpio-services through asxpio-ops.
the private network.
`known_hosts` pins both servers' SSH host keys for the pipeline.
`known_hosts` pins both servers' SSH host keys for the pipelines (the
NixOS one uses it too).
## Variables
@ -34,16 +36,12 @@ inventory/
├── group_vars/all/
│ ├── network.yaml # VPC and WireGuard addresses
│ ├── paths.yaml # /opt
│ ├── s3.yaml # object storage buckets and s3fs mounts
│ ├── wireguard.yaml # WireGuard peers
│ └── vault.yaml # encrypted secrets
├── group_vars/ops_servers/
│ ├── traefik.yaml # traefik_version
│ └── forgejo.yaml # forgejo_version, domain and aliases
├── group_vars/services_servers/
│ ├── forgejo_runner.yaml # runner version, labels' nix image
│ └── minecraft.yaml
└── host_vars/ # per-host addresses, SSH and s3fs settings
└── host_vars/ # per-host addresses and SSH settings
```
Compose files without variables are plain files under
@ -51,8 +49,7 @@ Compose files without variables are plain files under
## Roles in site.yaml
`base`, `traefik`, `crawler_alert`, `forgejo`, `forgejo_runner`,
`sillytavern`, `minecraft`, `minecraft_fossils`. Service roles go
`base`, `traefik`, `crawler_alert`, `forgejo`. Service roles go
through `docker_service`, which writes the files, runs
`docker compose up -d`, restarts on a config change and waits for the
service to answer.
@ -65,22 +62,12 @@ update `roles/crawler_alert/files/` from there.
| Playbook | Purpose |
|----------|---------|
| `01-wireguard-setup.yaml` | WireGuard hub on asxpio-ops, peer on asxpio-services, client configs |
| `01-wireguard-setup.yaml` | WireGuard hub on asxpio-ops, client configs |
| `02-ssh-hardening.yaml` | restrict SSH to WireGuard and the private network |
| `03-setup-s3fs.yaml` | s3fs mounts on asxpio-services (off on asxpio-ops) |
| `04-add-user-zerrtika.yaml` | add a user to running servers |
| `05-swap.yaml` | 2 GB swapfile on asxpio-ops |
Always dry-run these first with `--check --diff`.
- `03-setup-s3fs.yaml` rewrites `/etc/s3fs/.passwd-s3fs`, which the
backup script on asxpio-ops also reads.
- On asxpio-services the WireGuard interface is named
`asxpio-services`, not `wg0`. Check `01-wireguard-setup.yaml` against
the host before re-running it there.
## Runner notes
The `nix-latest` label runs `nixos/nix:<forgejo_runner_nix_version>`
with a `nix-store-<version>` volume over `/nix`. To bump it: change the
variable, let the pipeline apply it, then remove the old volume.
`/etc/s3fs/.passwd-s3fs` on asxpio-ops is a leftover of the removed s3fs
role and is still read by the backup script there; nothing manages it.

View file

@ -1,9 +1,6 @@
---
# Inventory for the Forgejo Actions runner. Jobs run in containers on
# asxpio-services, so asxpio-ops is reached over the VPC. The services
# host itself is reached through asxpio-ops: a container talking to its
# own host arrives from a Docker bridge address, which sshd and the
# firewall both refuse.
# Inventory for the Forgejo Actions runner: asxpio-ops over the private
# network, as user deploy.
all:
vars:
@ -17,8 +14,3 @@ all:
hosts:
asxpio-ops:
ansible_host: "{{ vpc_ip }}"
services_servers:
hosts:
asxpio-services:
ansible_host: "{{ vpc_ip }}"
ansible_ssh_common_args: "-o ProxyJump=deploy@{{ asxpio_ops_vpc_ip }}"

View file

@ -1,39 +0,0 @@
---
# S3 Object Storage Configuration
# These should be overridden in vault or environment-specific vars
# S3 Endpoint Configuration
s3_region: hel1
s3_endpoint: "https://{{ s3_region }}.your-objectstorage.com"
# S3 Credentials
# WARNING: Store these in ansible-vault encrypted file!
# Override these in group_vars/all/vault.yaml (encrypted)
s3_access_key: "{{ vault_s3_access_key | default('') }}"
s3_secret_key: "{{ vault_s3_secret_key | default('') }}"
# S3 Bucket names
s3_bucket_terraform_main: asxpio-terraform-main
s3_bucket_terraform_repl: asxpio-terraform-repl
s3_bucket_backups_main: asxpio-backups-main
s3_bucket_backups_repl: asxpio-backups-repl
s3_bucket_immich_main: asxpio-immich-main
s3_bucket_immich_repl: asxpio-immich-repl
# S3 Buckets to mount on servers (non-replication only)
s3_buckets:
- name: "{{ s3_bucket_terraform_main }}"
mount_point: /mnt/s3/terraform
owner: root
group: root
mode: "0755"
- name: "{{ s3_bucket_backups_main }}"
mount_point: /mnt/s3/backups
owner: root
group: root
mode: "0755"
- name: "{{ s3_bucket_immich_main }}"
mount_point: /mnt/s3/immich
owner: root
group: docker
mode: "0775"

View file

@ -1,20 +0,0 @@
---
# Forgejo Runner (CI/CD) configuration
# Runner version and settings
forgejo_runner_version: "11"
forgejo_runner_capacity: 4
forgejo_runner_timeout: 3h
# Image behind the nix-latest label. Jobs mount a nix-store volume over
# /nix, seeded from this image on first use; the image's /bin/sh and
# /etc/nix/nix.conf link into that store, so a volume from another
# version breaks them. Bumping this also switches to a fresh volume.
forgejo_runner_nix_version: "2.35.3"
forgejo_runner_token: "{{ vault_forgejo_runner_token }}"
# Runner installation path
forgejo_runner_base_path: "{{ services_base_path }}/forgejo-runner"
# The runner talks to Forgejo over the VPC, not through Traefik
forgejo_http_port: 3000

View file

@ -1,4 +0,0 @@
---
minecraft_fossils_owner: asxpi
minecraft_fossils_rcon_password: "{{ vault_minecraft_fossils_rcon_password }}"
minecraft_fossils_cf_api_key: "{{ vault_minecraft_fossils_cf_api_key }}"

View file

@ -6,7 +6,3 @@ wireguard_ip: "{{ asxpio_ops_wireguard_ip }}"
# SSH: the git user may connect from anywhere (Forgejo passthrough)
ssh_allow_public_git: true
ssh_git_user: git
# s3fs is off here: its ensure_diskfree reserve never fit the 38 GB disk
s3fs_enabled: false
s3fs_ensure_diskfree: 20000

View file

@ -1,8 +0,0 @@
---
public_ip: 46.62.228.216
vpc_ip: "{{ asxpio_services_vpc_ip }}"
wireguard_ip: "{{ asxpio_services_wireguard_ip }}"
ssh_allow_public_git: false
s3fs_ensure_diskfree: 81920 # 80 GB free = max 80 GB cache

View file

@ -1,6 +1,7 @@
---
# Default inventory: run from a workstation over WireGuard, as root.
# CI uses inventory/ci.yaml instead (VPC addresses, user deploy).
# asxpio-services runs NixOS and is not managed by Ansible; see ../nixos.
all:
vars:
@ -14,7 +15,3 @@ all:
hosts:
asxpio-ops:
ansible_host: "{{ wireguard_ip }}"
services_servers:
hosts:
asxpio-services:
ansible_host: "{{ wireguard_ip }}"

View file

@ -43,73 +43,6 @@
roles:
- role: wireguard
- name: Deploy WireGuard VPN Client
hosts: services_servers
become: true
gather_facts: true
vars:
# Map each server to its client config name
wireguard_client_configs:
asxpio-services: asxpio-services
tasks:
- name: Display client deployment information
ansible.builtin.debug:
msg: |
========================================
WireGuard VPN Client Deployment
========================================
Target: {{ inventory_hostname }} (VPN Client)
Will connect to: {{ asxpio_ops_wireguard_ip }} (asxpio-ops)
========================================
- name: Get client config name for this host
ansible.builtin.set_fact:
client_config_name: "{{ wireguard_client_configs[inventory_hostname] }}"
when: inventory_hostname in wireguard_client_configs
- name: Install WireGuard
ansible.builtin.apt:
name:
- wireguard
- wireguard-tools
state: present
update_cache: true
- name: Create WireGuard config directory
ansible.builtin.file:
path: /etc/wireguard
state: directory
mode: '0700'
owner: root
group: root
- name: Copy WireGuard client configuration
ansible.builtin.copy:
src: "{{ playbook_dir }}/wireguard-configs/asxpio-ops/{{ client_config_name }}.conf"
dest: "/etc/wireguard/{{ client_config_name }}.conf"
mode: '0600'
owner: root
group: root
when: client_config_name is defined
- name: Enable and start WireGuard interface
ansible.builtin.systemd:
name: "wg-quick@{{ client_config_name }}"
enabled: true
state: started
when: client_config_name is defined
register: wg_client_start
- name: Display client status
ansible.builtin.debug:
msg: |
✅ WireGuard client configured on {{ inventory_hostname }}
Interface: wg-quick@{{ client_config_name }}
Status: {{ 'Started' if wg_client_start.changed else 'Already running' }}
when: client_config_name is defined
- name: Post-deployment verification
hosts: localhost
gather_facts: false
@ -123,10 +56,9 @@
Infrastructure deployed:
✅ asxpio-ops: WireGuard VPN Server (hub)
✅ asxpio-services: WireGuard VPN Client (connected to ops)
Server-to-server connectivity:
- asxpio-ops ↔ asxpio-services via WireGuard (10.20.0.0/24)
- asxpio-services is a peer too; its side is configured in ../nixos
IMPORTANT NEXT STEPS FOR YOUR WORKSTATION:

View file

@ -1,134 +0,0 @@
---
# Stage 3: Setup S3FS on all servers
#
# This playbook sets up s3fs to mount S3 buckets as local filesystems.
# Must be run after Stage 2 (full deployment) to ensure Docker is installed.
#
# Mounts the following buckets:
# - asxpio-terraform-main -> /mnt/s3/terraform
# - asxpio-backups-main -> /mnt/s3/backups
# - asxpio-immich-main -> /mnt/s3/immich
#
# Cache sizes (per-host):
# - asxpio-ops (40GB disk): 10GB cache max
# - asxpio-services (160GB disk): 80GB cache max
#
# Usage:
# ansible-playbook playbooks/03-setup-s3fs.yaml
#
# IMPORTANT: Ensure S3 credentials are set in group_vars/all/vault.yaml:
# vault_s3_access_key: "your-access-key"
# vault_s3_secret_key: "your-secret-key"
#
- name: Setup S3FS on all servers
hosts: ops_servers,services_servers
become: true
gather_facts: true
pre_tasks:
- name: Validate S3 credentials are set
ansible.builtin.assert:
that:
- s3_access_key is defined
- s3_access_key != ""
- s3_secret_key is defined
- s3_secret_key != ""
fail_msg: |
S3 credentials not set!
Please set vault_s3_access_key and vault_s3_secret_key in:
- group_vars/all/vault.yaml (encrypted with ansible-vault)
OR
- Pass via command line: -e "s3_access_key=xxx s3_secret_key=yyy"
success_msg: "S3 credentials validated"
- name: Display S3 configuration
ansible.builtin.debug:
msg: |
========================================
S3FS Setup on {{ inventory_hostname }}
========================================
Target: {{ inventory_hostname }}
S3 Region: {{ s3_region }}
S3 Endpoint: {{ s3_endpoint }}
Buckets to mount:
{% for bucket in s3_buckets %}
- {{ bucket.name }} -> {{ bucket.mount_point }}
{% endfor %}
========================================
roles:
- role: s3fs
post_tasks:
- name: Display mounted filesystems
ansible.builtin.command:
cmd: df -h
register: df_output
changed_when: false
- name: Show S3 mounts
ansible.builtin.debug:
msg: "{{ df_output.stdout_lines | select('search', 's3fs') | list }}"
- name: Create immich library directory structure
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: '0775'
owner: root
group: docker
loop:
- /mnt/s3/immich/library
- /mnt/s3/immich/library/backups
- /mnt/s3/immich/library/encoded-video
- /mnt/s3/immich/library/library
- /mnt/s3/immich/library/profile
- /mnt/s3/immich/library/thumbs
- /mnt/s3/immich/library/upload
when: inventory_hostname in groups['services_servers']
- name: Display post-setup information
ansible.builtin.debug:
msg: |
========================================
S3FS Setup Complete on {{ inventory_hostname }}!
========================================
✅ All S3 buckets mounted successfully
Mount points:
{% for bucket in s3_buckets %}
- {{ bucket.mount_point }} ({{ bucket.name }})
{% endfor %}
Cache configuration:
- Location: /var/cache/s3fs
- Max size: ~{{ (40 - (s3fs_ensure_diskfree / 1024)) | int }}GB
- Free space reserved: {{ (s3fs_ensure_diskfree / 1024) | int }}GB
{% if inventory_hostname in groups['services_servers'] %}
Immich library structure created at:
/mnt/s3/immich/library/
├── backups/
├── encoded-video/
├── library/
├── profile/
├── thumbs/
└── upload/
Next steps for Immich migration:
1. Manually sync data from legacy server:
rsync -avz --info=progress2 \
asxpio-old:/srv/immich/library/ \
/mnt/s3/immich/library/
2. Backup Immich PostgreSQL database on legacy server:
docker exec immich_postgres pg_dumpall -U postgres | gzip > /tmp/immich-db.sql.gz
3. Deploy Immich service configured to use S3 storage:
IMMICH_MEDIA_LOCATION=/mnt/s3/immich/library
{% endif %}
========================================

View file

@ -1,5 +0,0 @@
---
- name: Restart Forgejo Runner
community.docker.docker_compose_v2:
project_src: /opt/forgejo-runner
state: restarted

View file

@ -1,74 +0,0 @@
---
- name: Validate Forgejo Runner variables
ansible.builtin.import_tasks: validate.yaml
- name: Deploy Forgejo Runner using shared docker_service role
ansible.builtin.include_role:
name: docker_service
vars:
service_name: "forgejo-runner"
service_base_path: "{{ forgejo_runner_base_path }}"
service_directories:
- { path: "data" }
- { path: "config" }
service_templates:
- { src: "config.yaml.j2", dest: "config/config.yaml" }
- { src: "docker-compose.yml.j2", dest: "docker-compose.yml" }
health_check_enabled: false # the runner exposes no port
- name: Check if runner is already registered
ansible.builtin.stat:
path: "{{ forgejo_runner_base_path }}/data/.runner"
register: runner_registered
- name: Set permissions on data directory
ansible.builtin.file:
path: "{{ forgejo_runner_base_path }}/data"
state: directory
mode: '0777'
recurse: true
when: not runner_registered.stat.exists
- name: Stop runner for registration
community.docker.docker_compose_v2:
project_src: "{{ forgejo_runner_base_path }}"
state: stopped
when: not runner_registered.stat.exists
- name: Register runner with Forgejo
ansible.builtin.command:
cmd: >
docker run --rm
-v {{ forgejo_runner_base_path }}/data:/data
-v {{ forgejo_runner_base_path }}/config/config.yaml:/config.yaml
code.forgejo.org/forgejo/runner:{{ forgejo_runner_version }}
forgejo-runner register
--no-interactive
--token {{ forgejo_runner_token }}
--name runner-{{ inventory_hostname }}
--instance http://{{ asxpio_ops_vpc_ip }}:{{ forgejo_http_port }}
--labels ubuntu-latest:docker://node:20-bookworm,ubuntu-22.04:docker://node:20-bookworm,ubuntu-24.04:docker://node:22-bookworm,debian-latest:docker://debian:bookworm,debian-12:docker://debian:bookworm,debian-11:docker://debian:bullseye,arch-latest:docker://archlinux:latest,nginx-latest:docker://nginx:latest,nginx-alpine:docker://nginx:alpine,node-20:docker://node:20,node-22:docker://node:22,python-3.11:docker://python:3.11,python-3.12:docker://python:3.12,alpine-latest:docker://alpine:latest
when: not runner_registered.stat.exists
changed_when: true
register: registration_result
- name: Start runner after registration
community.docker.docker_compose_v2:
project_src: "{{ forgejo_runner_base_path }}"
state: present
when: not runner_registered.stat.exists
- name: Display runner status
ansible.builtin.debug:
msg: |
Forgejo Runner deployed successfully!
Runner name: runner-{{ inventory_hostname }}
Forgejo instance: http://{{ asxpio_ops_vpc_ip }}:{{ forgejo_http_port }}
{% if runner_registered.stat.exists %}
Status: Already registered (using existing registration)
{% else %}
Status: Newly registered
{% endif %}
Check runner status: docker logs forgejo-runner

View file

@ -1,26 +0,0 @@
---
# Variable validation for forgejo_runner role
- name: Validate Forgejo Runner required variables
ansible.builtin.assert:
that:
- forgejo_runner_base_path is defined
- forgejo_runner_base_path | length > 0
- forgejo_runner_token is defined
- forgejo_runner_token | length > 0
- forgejo_runner_version is defined
- forgejo_runner_version | length > 0
- forgejo_http_port is defined
- forgejo_http_port | int > 0
- asxpio_ops_vpc_ip is defined
- asxpio_ops_vpc_ip | length > 0
fail_msg: |
Forgejo Runner role requires:
- forgejo_runner_base_path (installation directory)
- forgejo_runner_token (runner registration token)
- forgejo_runner_version (e.g., '3.5.1')
- forgejo_http_port (Forgejo HTTP port)
- asxpio_ops_vpc_ip (Forgejo server IP)
WARNING: forgejo_runner_token should be stored in Ansible Vault!
success_msg: "Forgejo Runner variables validated (WARNING: token should use Ansible Vault)"

View file

@ -1,48 +0,0 @@
log:
level: info
job_level: info
runner:
file: /data/.runner
capacity: {{ forgejo_runner_capacity }}
envs: {}
env_file: .env
timeout: {{ forgejo_runner_timeout }}
shutdown_timeout: {{ forgejo_runner_timeout }}
insecure: false
fetch_timeout: 5s
fetch_interval: 2s
report_interval: 1s
labels:
- "ubuntu-latest:docker://node:20-bookworm"
- "ubuntu-22.04:docker://node:20-bookworm"
- "ubuntu-24.04:docker://node:22-bookworm"
- "debian-latest:docker://debian:bookworm"
- "debian-12:docker://debian:bookworm"
- "debian-11:docker://debian:bullseye"
- "arch-latest:docker://archlinux:latest"
- "nginx-latest:docker://nginx:latest"
- "nginx-alpine:docker://nginx:alpine"
- "node-20:docker://node:20"
- "node-22:docker://node:22"
- "python-3.11:docker://python:3.11"
- "python-3.12:docker://python:3.12"
- "alpine-latest:docker://alpine:latest"
- "nix-latest:docker://nixos/nix:{{ forgejo_runner_nix_version }}"
cache:
enabled: true
port: 0
dir: ""
container:
# "" so each workflow gets a bridge network; services (Postgres) need it.
network: ""
enable_ipv6: false
privileged: false
options: "-v nix-store-{{ forgejo_runner_nix_version }}:/nix"
valid_volumes:
- '**'
docker_host: "automount"
force_pull: false
force_rebuild: false

View file

@ -1,16 +0,0 @@
name: forgejo-runner
services:
forgejo-runner:
container_name: forgejo-runner
image: code.forgejo.org/forgejo/runner:{{ forgejo_runner_version }}
restart: unless-stopped
user: "0:0" # Run as root to access Docker socket
command: forgejo-runner daemon --config /config.yaml
environment:
- CONFIG_FILE=/config.yaml
volumes:
- ./data:/data
- ./config/config.yaml:/config.yaml
- /var/run/docker.sock:/var/run/docker.sock
network_mode: host

View file

@ -1,15 +0,0 @@
services:
minecraft:
image: eclipse-temurin:21-jre
container_name: minecraft
working_dir: /data
command: java -Xmx4G -Xms2G -jar paper-1.21.11-54.jar nogui
stdin_open: true
tty: true
restart: unless-stopped
stop_grace_period: 2m
ports:
- "25565:25565"
- "19132:19132/udp"
volumes:
- /opt/minecraft:/data

View file

@ -1,15 +0,0 @@
---
# Paper server; the world and the server jar live next to the compose
# file in /opt/minecraft and are not managed here. A compose change
# recreates the container, so it is stopped with time to save first.
- name: Deploy Minecraft
ansible.builtin.include_role:
name: docker_service
vars:
service_name: "minecraft"
service_base_path: "{{ services_base_path }}/minecraft"
service_files:
- { src: "docker-compose.yml", dest: "docker-compose.yml" }
service_graceful_stop:
- { container: minecraft, timeout: 120 }
health_check_enabled: false

View file

@ -1,87 +0,0 @@
services:
minecraft-fossils:
image: itzg/minecraft-server:java21
container_name: minecraft-fossils
restart: unless-stopped
stop_grace_period: 2m
stdin_open: true
tty: true
ports:
- "25566:25565"
env_file:
- secrets.env
environment:
EULA: "TRUE"
TYPE: FABRIC
VERSION: "1.20.1"
FABRIC_LOADER_VERSION: "0.18.4"
MEMORY: "8G"
ONLINE_MODE: "FALSE"
ENFORCE_WHITELIST: "TRUE"
ENFORCE_SECURE_PROFILE: "FALSE"
WHITELIST_FILE: /data/whitelist.json
OPS_FILE: /data/ops.json
ENABLE_RCON: "TRUE"
MOTD: "[mc.czsk.dev] Fossils Archaeology Revival: Genesis"
USE_AIKAR_FLAGS: "TRUE"
volumes:
- /opt/minecraft-fossils/data:/data
networks:
- fossils
whitelist-sync:
image: alpine:3
container_name: minecraft-fossils-wlsync
restart: unless-stopped
depends_on:
- minecraft-fossils
volumes:
- /opt/minecraft:/src:ro
- /opt/minecraft-fossils/data:/dst
env_file:
- secrets.env
environment:
RCON_HOST: minecraft-fossils
INTERVAL: "15"
command:
- sh
- -c
- |
apk add --no-cache python3 >/dev/null
cat > /rcon.py <<'PY'
import socket, struct, sys, os
host, port, pw, cmd = os.environ["RCON_HOST"], 25575, os.environ["RCON_PASSWORD"], sys.argv[1]
def pkt(i, t, b): p = struct.pack("<ii", i, t) + b.encode() + b"\x00\x00"; return struct.pack("<i", len(p)) + p
def recv(s):
ln = struct.unpack("<i", s.recv(4))[0]; d = b""
while len(d) < ln: d += s.recv(ln - len(d))
return struct.unpack("<ii", d[:8])[0], d[8:-2].decode(errors="replace")
s = socket.create_connection((host, port), timeout=5)
s.send(pkt(1, 3, pw))
rid, _ = recv(s)
if rid == -1: print("auth failed"); sys.exit(1)
s.send(pkt(2, 2, cmd))
_, out = recv(s); print(out)
PY
echo "[wlsync] started, interval=$${INTERVAL}s"
while true; do
for f in whitelist.json ops.json banned-players.json banned-ips.json; do
if [ -f /src/$$f ] && ! cmp -s /src/$$f /dst/$$f 2>/dev/null; then
cp /src/$$f /dst/$$f
echo "[wlsync] copied $$f"
case $$f in
whitelist.json) CMD="whitelist reload" ;;
ops.json) CMD="reload" ;;
banned-players.json|banned-ips.json) CMD="kick @a Ban list updated" ;;
esac
python3 /rcon.py "$$CMD" 2>&1 | sed 's/^/[wlsync] rcon: /' || echo "[wlsync] rcon failed"
fi
done
sleep $$INTERVAL
done
networks:
- fossils
networks:
fossils:
driver: bridge

View file

@ -1,8 +0,0 @@
[Unit]
Description=Daily restart of minecraft-fossils container
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
ExecStart=/usr/bin/docker restart minecraft-fossils

View file

@ -1,10 +0,0 @@
[Unit]
Description=Restart minecraft-fossils daily at 08:00 Europe/Tallinn
[Timer]
OnCalendar=*-*-* 08:00:00 Europe/Tallinn
Persistent=true
Unit=minecraft-fossils-restart.service
[Install]
WantedBy=timers.target

View file

@ -1,4 +0,0 @@
---
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: true

View file

@ -1,49 +0,0 @@
---
# Fabric server plus a sidecar that mirrors the whitelist, ops and ban
# lists from the Paper server. World data in /opt/minecraft-fossils/data
# is not managed here. A compose change recreates the container, so it
# is saved and stopped with time to finish first.
- name: Deploy Minecraft Fossils
ansible.builtin.include_role:
name: docker_service
vars:
service_name: "minecraft-fossils"
service_base_path: "{{ services_base_path }}/minecraft-fossils"
service_templates:
- src: "secrets.env.j2"
dest: "secrets.env"
owner: "{{ minecraft_fossils_owner }}"
group: "{{ minecraft_fossils_owner }}"
mode: "0600"
service_files:
- src: "docker-compose.yml"
dest: "docker-compose.yml"
owner: "{{ minecraft_fossils_owner }}"
group: "{{ minecraft_fossils_owner }}"
# secrets.env only matters at container start; never restart the
# game server as a side effect of a deploy
service_restart_on_change: false
service_graceful_stop:
- container: minecraft-fossils
timeout: 120
pre_stop: docker exec minecraft-fossils rcon-cli save-all flush
health_check_enabled: false
- name: Install the daily restart units
ansible.builtin.copy:
src: "{{ item }}"
dest: "/etc/systemd/system/{{ item }}"
mode: "0644"
loop:
- minecraft-fossils-restart.service
- minecraft-fossils-restart.timer
notify: Reload systemd
- name: Flush handlers
ansible.builtin.meta: flush_handlers
- name: Enable the daily restart timer
ansible.builtin.systemd:
name: minecraft-fossils-restart.timer
enabled: true
state: started

View file

@ -1,2 +0,0 @@
RCON_PASSWORD={{ minecraft_fossils_rcon_password }}
CF_API_KEY={{ minecraft_fossils_cf_api_key }}

View file

@ -1,56 +0,0 @@
---
# S3FS role defaults
# S3 Configuration
s3_endpoint: "https://{{ s3_region }}.your-objectstorage.com"
s3_region: "hel1"
# S3 Credentials (should be overridden in group_vars or vault)
s3_access_key: ""
s3_secret_key: ""
# false: no mounts. Unmounts and drops the fstab entries; the credentials
# file stays (backups use it with rclone, /root/backups/rclone-hetzner.sh).
s3fs_enabled: true
# S3 Buckets to mount (non-replication buckets only)
s3_buckets:
- name: asxpio-terraform-main
mount_point: /mnt/s3/terraform
owner: root
group: root
mode: "0755"
- name: asxpio-backups-main
mount_point: /mnt/s3/backups
owner: root
group: root
mode: "0755"
- name: asxpio-immich-main
mount_point: /mnt/s3/immich
owner: root
group: docker
mode: "0775"
# S3FS mount options
# Note: s3fs_ensure_diskfree is defined per-host in host_vars/
s3fs_options:
- "passwd_file=/etc/s3fs/.passwd-s3fs"
- "use_path_request_style"
- "url={{ s3_endpoint }}"
- "allow_other"
- "umask=0022"
- "uid={{ s3fs_uid | default('0') }}"
- "gid={{ s3fs_gid | default('0') }}"
- "use_cache=/var/cache/s3fs"
- "ensure_diskfree={{ s3fs_ensure_diskfree | default(81920) }}"
- "max_stat_cache_size=10000"
- "stat_cache_expire=900"
- "parallel_count=15"
- "multipart_size=52"
- "nocopyapi"
- "enable_noobj_cache"
# Package dependencies
s3fs_packages:
- s3fs
- fuse3

View file

@ -1,120 +0,0 @@
---
# S3FS role tasks - Mount S3 buckets using s3fs
- name: Install s3fs and dependencies
ansible.builtin.apt:
name: "{{ s3fs_packages }}"
state: present
update_cache: true
- name: Create s3fs credentials directory
ansible.builtin.file:
path: /etc/s3fs
state: directory
mode: '0700'
owner: root
group: root
- name: Create s3fs password file
ansible.builtin.template:
src: passwd-s3fs.j2
dest: /etc/s3fs/.passwd-s3fs
mode: '0600'
owner: root
group: root
no_log: true
- name: Create S3 cache directory
ansible.builtin.file:
path: /var/cache/s3fs
state: directory
mode: '0755'
owner: root
group: root
- name: Mount S3 buckets
when: s3fs_enabled | bool
block:
- name: Create mount points for S3 buckets
ansible.builtin.file:
path: "{{ item.mount_point }}"
state: directory
mode: "{{ item.mode }}"
owner: "{{ item.owner }}"
group: "{{ item.group }}"
loop: "{{ s3_buckets }}"
loop_control:
label: "{{ item.name }} -> {{ item.mount_point }}"
- name: Add S3 buckets to /etc/fstab
ansible.builtin.lineinfile:
path: /etc/fstab
line: "{{ item.name }} {{ item.mount_point }} fuse.s3fs _netdev,{{ s3fs_options | join(',') }} 0 0"
regexp: "^{{ item.name }}\\s+{{ item.mount_point }}"
state: present
loop: "{{ s3_buckets }}"
loop_control:
label: "{{ item.name }} -> {{ item.mount_point }}"
- name: Mount all S3 buckets
ansible.builtin.command:
cmd: "mount {{ item.mount_point }}"
loop: "{{ s3_buckets }}"
loop_control:
label: "{{ item.name }} -> {{ item.mount_point }}"
register: mount_result
changed_when: mount_result.rc == 0
failed_when: false
- name: Verify S3 buckets are mounted
ansible.builtin.command:
cmd: "mountpoint -q {{ item.mount_point }}"
loop: "{{ s3_buckets }}"
loop_control:
label: "{{ item.name }} -> {{ item.mount_point }}"
register: verify_mount
changed_when: false
failed_when: verify_mount.rc != 0
- name: Display mounted S3 buckets
ansible.builtin.debug:
msg: |
✅ S3 Buckets mounted successfully:
{% for bucket in s3_buckets %}
- {{ bucket.name }} -> {{ bucket.mount_point }}
{% endfor %}
- name: Test write access to Immich bucket
ansible.builtin.copy:
content: "S3FS test file created on {{ ansible_date_time.iso8601 }}"
dest: "{{ s3_buckets | selectattr('name', 'equalto', 'asxpio-immich-main') | map(attribute='mount_point') | first }}/.s3fs-test"
mode: '0644'
when: "'asxpio-immich-main' in s3_buckets | map(attribute='name')"
register: write_test
- name: Remove test file
ansible.builtin.file:
path: "{{ s3_buckets | selectattr('name', 'equalto', 'asxpio-immich-main') | map(attribute='mount_point') | first }}/.s3fs-test"
state: absent
when: write_test is changed
- name: Unmount S3 buckets
ansible.builtin.command:
cmd: "umount {{ item.mount_point }}"
loop: "{{ s3_buckets }}"
loop_control:
label: "{{ item.mount_point }}"
register: umount_result
changed_when: umount_result.rc == 0
failed_when: false
when: not (s3fs_enabled | bool)
- name: Remove S3 buckets from /etc/fstab
ansible.builtin.lineinfile:
path: /etc/fstab
regexp: "^{{ item.name }}\\s+{{ item.mount_point }}\\s"
state: absent
loop: "{{ s3_buckets }}"
loop_control:
label: "{{ item.name }} -> {{ item.mount_point }}"
when: not (s3fs_enabled | bool)

View file

@ -1 +0,0 @@
{{ s3_access_key }}:{{ s3_secret_key }}

View file

@ -1,11 +0,0 @@
services:
sillytavern:
image: ghcr.io/sillytavern/sillytavern:1.19.0
container_name: sillytavern
restart: unless-stopped
ports:
- "10.20.0.20:8000:8000"
volumes:
- ./config:/home/node/app/config
- ./data:/home/node/app/data
- ./plugins:/home/node/app/plugins

View file

@ -1,16 +0,0 @@
---
# Reachable on the WireGuard address only. Bump the image tag in
# files/docker-compose.yml to upgrade.
- name: Deploy SillyTavern
ansible.builtin.include_role:
name: docker_service
vars:
service_name: "sillytavern"
service_base_path: "{{ services_base_path }}/sillytavern"
service_directories:
- { path: "config" }
- { path: "data" }
- { path: "plugins" }
service_files:
- { src: "docker-compose.yml", dest: "docker-compose.yml" }
health_check_enabled: false

View file

@ -1,15 +1,16 @@
---
# Everything the deploy pipeline manages. Idempotent: a run with nothing
# Everything the deploy pipeline manages on asxpio-ops. asxpio-services
# runs NixOS and is described in ../nixos. Idempotent: a run with nothing
# to change touches nothing and restarts nothing.
#
# ansible-playbook site.yaml --check --diff # dry run
# ansible-playbook site.yaml --tags traefik # one service
#
# Host bootstrap (WireGuard, SSH hardening, s3fs, swap) is in playbooks/
# Host bootstrap (WireGuard, SSH hardening, swap) is in playbooks/
# and is run by hand.
- name: Host baseline
hosts: asxpio
hosts: ops_servers
become: true
roles:
- { role: base, tags: [base] }
@ -21,26 +22,9 @@
- { role: traefik, tags: [traefik] }
- { role: crawler_alert, tags: [crawler_alert] }
- name: asxpio-services services
hosts: services_servers
become: true
roles:
- { role: sillytavern, tags: [sillytavern] }
- { role: minecraft, tags: [minecraft] }
- { role: minecraft_fossils, tags: [minecraft_fossils] }
# Late: restarting Forgejo briefly cuts the runner off from its job log
- name: Forgejo
hosts: ops_servers
become: true
roles:
- { role: forgejo, tags: [forgejo] }
# Last: when the pipeline changes the runner it recreates the container
# that is running the pipeline. Everything else is done by then; that job
# ends as failed and the next run confirms the result.
- name: Forgejo runner
hosts: services_servers
become: true
roles:
- { role: forgejo_runner, tags: [forgejo_runner] }