Files
ansible/roles/edge_vps/docs/plans/2026-02-24-edge-vps-role.md
Tuan-Dat Tran 5a8c7f0248 feat(proxmox): add hosts config
Signed-off-by: Tuan-Dat Tran <tuan-dat.tran@tudattr.dev>
2026-02-28 11:30:58 +01:00

18 KiB

Edge VPS Ansible Role Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Create a modular Ansible role for deploying edge VPS infrastructure components (WireGuard, Traefik, Pangolin, Elastic Agent).

Architecture: Modular task-based role following existing patterns in the repository. Each component has its own numbered task file. Configs are templated with secrets from ansible-vault encrypted group_vars.

Tech Stack: Ansible, Jinja2 templates, Docker Compose, WireGuard, Traefik, Pangolin, Elastic Fleet Agent


Task 1: Create Role Directory Structure

Files:

  • Create: roles/edge_vps/tasks/main.yaml
  • Create: roles/edge_vps/handlers/main.yaml
  • Create: roles/edge_vps/defaults/main.yaml
  • Create: roles/edge_vps/templates/ directory structure

Step 1: Create directory structure

Run:

mkdir -p tasks handlers defaults templates/wireguard templates/traefik templates/pangolin templates/elastic-agent

Step 2: Create defaults/main.yaml

---
edge_vps_config_base: /root/config
edge_vps_wireguard_config_dir: /etc/wireguard
edge_vps_wireguard_interface: wg0
edge_vps_wireguard_address: "10.133.7.1/24"
edge_vps_wireguard_port: 61975
edge_vps_traefik_config_dir: "{{ edge_vps_config_base }}/traefik"
edge_vps_traefik_logs_dir: "{{ edge_vps_traefik_config_dir }}/logs"
edge_vps_pangolin_config_dir: "{{ edge_vps_config_base }}/pangolin"
edge_vps_elastic_config_dir: "{{ edge_vps_config_base }}/elastic-agent"
edge_vps_elastic_state_dir: /var/lib/elastic-agent/elastic-system/elastic-agent/state

Step 3: Create handlers/main.yaml

---
- name: Restart wireguard
  ansible.builtin.systemd:
    name: "wg-quick@{{ edge_vps_wireguard_interface }}"
    state: restarted
  listen: restart wireguard

- name: Restart traefik
  ansible.builtin.command:
    cmd: docker compose restart
    chdir: "{{ edge_vps_traefik_config_dir }}"
  listen: restart traefik

Step 4: Commit

git add defaults/main.yaml handlers/main.yaml
git commit -m "feat(edge_vps): add role structure and handlers"

Task 2: Create Directory Setup Task

Files:

  • Create: roles/edge_vps/tasks/10_directories.yaml

Step 1: Create 10_directories.yaml

---
- name: Create config base directory
  ansible.builtin.file:
    path: "{{ edge_vps_config_base }}"
    state: directory
    mode: "0755"

- name: Create Traefik directories
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
    mode: "0755"
  loop:
    - "{{ edge_vps_traefik_config_dir }}"
    - "{{ edge_vps_traefik_logs_dir }}"

- name: Create Pangolin config directory
  ansible.builtin.file:
    path: "{{ edge_vps_pangolin_config_dir }}"
    state: directory
    mode: "0755"

- name: Create Elastic Agent directories
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
    mode: "0755"
  loop:
    - "{{ edge_vps_elastic_config_dir }}"
    - "{{ edge_vps_elastic_state_dir }}"

Step 2: Commit

git add tasks/10_directories.yaml
git commit -m "feat(edge_vps): add directory setup task"

Task 3: Create WireGuard Task and Template

Files:

  • Create: roles/edge_vps/tasks/20_wireguard.yaml
  • Create: roles/edge_vps/templates/wireguard/wg0.conf.j2

Step 1: Create templates/wireguard/wg0.conf.j2

[Interface]
Address = {{ edge_vps_wireguard_address }}
ListenPort = {{ edge_vps_wireguard_port }}
PrivateKey = {{ vault_edge_vps.wireguard.private_key }}

PostUp   = sysctl -w net.ipv4.ip_forward=1
PostUp   = iptables -A FORWARD -i {{ edge_vps_wireguard_interface }} -j ACCEPT
PostUp   = iptables -A FORWARD -o {{ edge_vps_wireguard_interface }} -j ACCEPT
{% for route in edge_vps_wireguard_routes | default([]) %}
PostUp   = ip route add {{ route }} via {{ route.gateway }} dev {{ edge_vps_wireguard_interface }}
{% endfor %}
PostDown = iptables -D FORWARD -i {{ edge_vps_wireguard_interface }} -j ACCEPT
PostDown = iptables -D FORWARD -o {{ edge_vps_wireguard_interface }} -j ACCEPT
{% for route in edge_vps_wireguard_routes | default([]) %}
PostDown = ip route del {{ route }} via {{ route.gateway }} dev {{ edge_vps_wireguard_interface }}
{% endfor %}

{% for peer in vault_edge_vps.wireguard.peers %}
[Peer]
# {{ peer.name }}
PublicKey = {{ peer.public_key }}
PresharedKey = {{ peer.preshared_key }}
AllowedIPs = {{ peer.allowed_ips }}

{% endfor %}

Step 2: Create tasks/20_wireguard.yaml

---
- name: Install WireGuard
  ansible.builtin.apt:
    name: wireguard
    state: present
    update_cache: true

- name: Deploy WireGuard config
  ansible.builtin.template:
    src: wireguard/wg0.conf.j2
    dest: "{{ edge_vps_wireguard_config_dir }}/{{ edge_vps_wireguard_interface }}.conf"
    mode: "0600"
  notify: restart wireguard

- name: Enable WireGuard
  ansible.builtin.systemd:
    name: "wg-quick@{{ edge_vps_wireguard_interface }}"
    enabled: true
    state: started

Step 3: Commit

git add tasks/20_wireguard.yaml templates/wireguard/wg0.conf.j2
git commit -m "feat(edge_vps): add WireGuard setup task and template"

Task 4: Create Traefik Task and Template

Files:

  • Create: roles/edge_vps/tasks/30_traefik.yaml
  • Create: roles/edge_vps/templates/traefik/traefik_config.yml.j2

Step 1: Create templates/traefik/traefik_config.yml.j2

api:
  insecure: true
  dashboard: true

providers:
  http:
    endpoint: "http://pangolin:3001/api/v1/traefik-config"
    pollInterval: "5s"
  file:
    filename: "/etc/traefik/dynamic_config.yml"

experimental:
  plugins:
    badger:
      moduleName: "github.com/fosrl/badger"
      version: "v1.2.1"

log:
  level: "INFO"
  format: "common"
  maxSize: 100
  maxBackups: 3
  maxAge: 3
  compress: true

certificatesResolvers:
  letsencrypt:
    acme:
      dnsChallenge:
        provider: "cloudflare"
      email: "{{ edge_vps_acme_email }}"
      storage: "/letsencrypt/acme.json"
      caServer: "https://acme-v02.api.letsencrypt.org/directory"

entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"
    transport:
      respondingTimeouts:
        readTimeout: "30m"
    http:
      tls:
        certResolver: "letsencrypt"
  tcp-6443:
    address: ":6443/tcp"

serversTransport:
  insecureSkipVerify: true

ping:
  entryPoint: "web"

accessLog:
  filePath: "/var/log/traefik/access.log"
  format: common

Step 2: Create tasks/30_traefik.yaml

---
- name: Deploy Traefik config
  ansible.builtin.template:
    src: traefik/traefik_config.yml.j2
    dest: "{{ edge_vps_traefik_config_dir }}/traefik_config.yml"
    mode: "0644"
  notify: restart traefik

- name: Deploy Cloudflare credentials for ACME
  ansible.builtin.copy:
    content: |
      CF_DNS_API_TOKEN={{ vault_edge_vps.traefik.cloudflare_api_token }}
    dest: "{{ edge_vps_traefik_config_dir }}/cloudflare.env"
    mode: "0600"
  no_log: true

Step 3: Commit

git add tasks/30_traefik.yaml templates/traefik/traefik_config.yml.j2
git commit -m "feat(edge_vps): add Traefik setup task and template"

Task 5: Create Pangolin Task and Templates

Files:

  • Create: roles/edge_vps/tasks/40_pangolin.yaml
  • Create: roles/edge_vps/templates/pangolin/config.yml.j2
  • Create: roles/edge_vps/templates/pangolin/docker-compose.yml.j2

Step 1: Create templates/pangolin/config.yml.j2

gerbil:
    start_port: 51820
    base_endpoint: "{{ edge_vps_pangolin_base_endpoint }}"

app:
    dashboard_url: "{{ edge_vps_pangolin_dashboard_url }}"
    log_level: "info"
    telemetry:
        anonymous_usage: true

domains:
    domain1:
        base_domain: "{{ edge_vps_pangolin_base_domain }}"

server:
    secret: "{{ vault_edge_vps.pangolin.server_secret }}"
    cors:
        origins: ["{{ edge_vps_pangolin_dashboard_url }}"]
        methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
        allowed_headers: ["X-CSRF-Token", "Content-Type"]
        credentials: false
    maxmind_db_path: "./config/GeoLite2-Country.mmdb"

flags:
    require_email_verification: false
    disable_signup_without_invite: true
    disable_user_create_org: false
    allow_raw_resources: true

Step 2: Create templates/pangolin/docker-compose.yml.j2

services:
  pangolin:
    image: fosrl/pangolin:latest
    container_name: pangolin
    restart: unless-stopped
    ports:
      - "3001:3001"
      - "443:443"
      - "80:80"
    volumes:
      - ./config.yml:/app/config/config.yml:ro
      - ./letsencrypt:/letsencrypt
    depends_on:
      - gerbil

  gerbil:
    image: fosrl/gerbil:latest
    container_name: gerbil
    restart: unless-stopped
    network_mode: host
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    volumes:
      - /lib/modules:/lib/modules

Step 3: Create tasks/40_pangolin.yaml

---
- name: Deploy Pangolin config
  ansible.builtin.template:
    src: pangolin/config.yml.j2
    dest: "{{ edge_vps_pangolin_config_dir }}/config.yml"
    mode: "0644"
  notify: restart pangolin

- name: Deploy Pangolin docker-compose
  ansible.builtin.template:
    src: pangolin/docker-compose.yml.j2
    dest: "{{ edge_vps_pangolin_config_dir }}/docker-compose.yml"
    mode: "0644"

- name: Create letsencrypt directory for Pangolin
  ansible.builtin.file:
    path: "{{ edge_vps_pangolin_config_dir }}/letsencrypt"
    state: directory
    mode: "0755"

- name: Start Pangolin
  community.docker.docker_compose_v2:
    project_src: "{{ edge_vps_pangolin_config_dir }}"
    state: present

Step 4: Commit

git add tasks/40_pangolin.yaml templates/pangolin/
git commit -m "feat(edge_vps): add Pangolin setup task and templates"

Task 6: Create Elastic Agent Task and Templates

Files:

  • Create: roles/edge_vps/tasks/50_elastic_agent.yaml
  • Create: roles/edge_vps/templates/elastic-agent/docker-compose.yml.j2
  • Create: roles/edge_vps/templates/elastic-agent/elastic-agent.yml.j2

Step 1: Create templates/elastic-agent/elastic-agent.yml.j2

fleet:
  enabled: true

Step 2: Create templates/elastic-agent/docker-compose.yml.j2

services:
  elastic-agent:
    image: docker.elastic.co/elastic-agent/elastic-agent:{{ edge_vps_elastic_version }}
    container_name: elastic-agent
    restart: always
    network_mode: host
    dns:
      - {{ edge_vps_elastic_dns_server }}
    dns_search:
      - elastic-system.svc.cluster.local
      - svc.cluster.local
      - cluster.local
    user: "0:0"
    privileged: true
    entrypoint: ["/usr/bin/env", "bash", "-c"]
    command:
      - |
        set -e
        if [[ -f /mnt/elastic-internal/elasticsearch-association/elastic-system/elasticsearch/certs/ca.crt ]]; then
          if [[ -f /usr/bin/update-ca-trust ]]; then
            cp /mnt/elastic-internal/elasticsearch-association/elastic-system/elasticsearch/certs/ca.crt /etc/pki/ca-trust/source/anchors/
            /usr/bin/update-ca-trust
          elif [[ -f /usr/sbin/update-ca-certificates ]]; then
            cp /mnt/elastic-internal/elasticsearch-association/elastic-system/elasticsearch/certs/ca.crt /usr/local/share/ca-certificates/
            /usr/sbin/update-ca-certificates
          fi
        fi
        exec /usr/bin/tini -- /usr/local/bin/docker-entrypoint -e -c /etc/agent/elastic-agent.yml
    environment:
      - FLEET_CA=/mnt/elastic-internal/fleetserver-association/elastic-system/fleet-server/certs/ca.crt
      - FLEET_ENROLL=true
      - FLEET_ENROLLMENT_TOKEN={{ vault_edge_vps.elastic.fleet_enrollment_token }}
      - FLEET_URL={{ edge_vps_elastic_fleet_url }}
      - STATE_PATH=/usr/share/elastic-agent/state
      - CONFIG_PATH=/usr/share/elastic-agent/state
      - NODE_NAME={{ inventory_hostname }}
    volumes:
      - {{ edge_vps_elastic_state_dir }}:/usr/share/elastic-agent/state
      - ./elastic-agent.yml:/etc/agent/elastic-agent.yml:ro
      - ./elasticsearch-ca.crt:/mnt/elastic-internal/elasticsearch-association/elastic-system/elasticsearch/certs/ca.crt:ro
      - ./fleet-ca.crt:/mnt/elastic-internal/fleetserver-association/elastic-system/fleet-server/certs/ca.crt:ro
      - {{ edge_vps_traefik_logs_dir }}:/var/log/traefik:ro

Step 3: Create tasks/50_elastic_agent.yaml

---
- name: Deploy Elastic Agent config
  ansible.builtin.template:
    src: elastic-agent/elastic-agent.yml.j2
    dest: "{{ edge_vps_elastic_config_dir }}/elastic-agent.yml"
    mode: "0644"

- name: Deploy Elastic Agent docker-compose
  ansible.builtin.template:
    src: elastic-agent/docker-compose.yml.j2
    dest: "{{ edge_vps_elastic_config_dir }}/docker-compose.yml"
    mode: "0644"

- name: Deploy Elasticsearch CA certificate
  ansible.builtin.copy:
    src: elastic-agent/elasticsearch-ca.crt
    dest: "{{ edge_vps_elastic_config_dir }}/elasticsearch-ca.crt"
    mode: "0644"

- name: Deploy Fleet CA certificate
  ansible.builtin.copy:
    src: elastic-agent/fleet-ca.crt
    dest: "{{ edge_vps_elastic_config_dir }}/fleet-ca.crt"
    mode: "0644"

- name: Start Elastic Agent
  community.docker.docker_compose_v2:
    project_src: "{{ edge_vps_elastic_config_dir }}"
    state: present

Step 4: Commit

git add tasks/50_elastic_agent.yaml templates/elastic-agent/
git commit -m "feat(edge_vps): add Elastic Agent setup task and templates"

Task 7: Create Main Task Orchestrator

Files:

  • Create: roles/edge_vps/tasks/main.yaml

Step 1: Create tasks/main.yaml

---
- name: Setup directories
  ansible.builtin.include_tasks: 10_directories.yaml

- name: Setup WireGuard
  ansible.builtin.include_tasks: 20_wireguard.yaml

- name: Setup Traefik
  ansible.builtin.include_tasks: 30_traefik.yaml

- name: Setup Pangolin
  ansible.builtin.include_tasks: 40_pangolin.yaml

- name: Setup Elastic Agent
  ansible.builtin.include_tasks: 50_elastic_agent.yaml

Step 2: Commit

git add tasks/main.yaml
git commit -m "feat(edge_vps): add main task orchestrator"

Task 8: Create Inventory Variables

Files:

  • Create: vars/group_vars/vps/vars.yaml
  • Create: vars/group_vars/vps/secrets.yaml

Step 1: Create vars/group_vars/vps/vars.yaml

edge_vps_wireguard_address: "10.133.7.1/24"
edge_vps_wireguard_port: 61975
edge_vps_wireguard_routes:
  - network: "10.43.0.0/16"
    gateway: "10.133.7.4"

edge_vps_pangolin_dashboard_url: "https://pangolin.seyshiro.de"
edge_vps_pangolin_base_endpoint: "pangolin.seyshiro.de"
edge_vps_pangolin_base_domain: "seyshiro.de"

edge_vps_acme_email: "me+acme@tudattr.dev"

edge_vps_elastic_version: "9.2.2"
edge_vps_elastic_dns_server: "10.43.0.10"
edge_vps_elastic_fleet_url: "https://fleet-server-agent-http.elastic-system.svc:8220"

Step 2: Create vars/group_vars/vps/secrets.yaml (template)

vault_edge_vps:
  wireguard:
    private_key: "YOUR_WIREGUARD_PRIVATE_KEY"
    peers:
      - name: lilcrow
        public_key: "PEER_PUBLIC_KEY"
        preshared_key: "PEER_PRESHARED_KEY"
        allowed_ips: "10.133.7.2/32"
      - name: homelab
        public_key: "PEER_PUBLIC_KEY"
        preshared_key: "PEER_PRESHARED_KEY"
        allowed_ips: "10.133.7.3/32"
      - name: k3s
        public_key: "PEER_PUBLIC_KEY"
        preshared_key: "PEER_PRESHARED_KEY"
        allowed_ips: "10.133.7.4/32, 10.43.0.0/16"
  pangolin:
    server_secret: "YOUR_PANGOLIN_SERVER_SECRET"
  traefik:
    cloudflare_api_token: "YOUR_CLOUDFLARE_API_TOKEN"
  elastic:
    fleet_enrollment_token: "YOUR_FLEET_ENROLLMENT_TOKEN"

Step 3: Encrypt secrets file

Run:

ansible-vault encrypt vars/group_vars/vps/secrets.yaml

Step 4: Commit

git add vars/group_vars/vps/
git commit -m "feat(edge_vps): add inventory variables for VPS group"

Task 9: Update README

Files:

  • Modify: roles/edge_vps/README.md

Step 1: Update README.md

# Edge VPS

Configures edge VPS instances with WireGuard VPN, Traefik reverse proxy, Pangolin, and Elastic Fleet Agent.

## Requirements

- Docker and Docker Compose installed
- Ansible community.docker collection

## Role Variables

### WireGuard

| Variable | Default | Description |
|----------|---------|-------------|
| `edge_vps_wireguard_address` | `10.133.7.1/24` | WireGuard interface address |
| `edge_vps_wireguard_port` | `61975` | WireGuard listen port |
| `edge_vps_wireguard_interface` | `wg0` | WireGuard interface name |
| `edge_vps_wireguard_routes` | `[]` | List of routes to add (network, gateway) |

### Traefik

| Variable | Default | Description |
|----------|---------|-------------|
| `edge_vps_traefik_config_dir` | `/root/config/traefik` | Traefik config directory |
| `edge_vps_acme_email` | - | Email for Let's Encrypt |

### Pangolin

| Variable | Default | Description |
|----------|---------|-------------|
| `edge_vps_pangolin_dashboard_url` | - | Pangolin dashboard URL |
| `edge_vps_pangolin_base_endpoint` | - | Pangolin base endpoint |
| `edge_vps_pangolin_base_domain` | - | Base domain for Pangolin |

### Elastic Agent

| Variable | Default | Description |
|----------|---------|-------------|
| `edge_vps_elastic_version` | `9.2.2` | Elastic Agent version |
| `edge_vps_elastic_fleet_url` | - | Fleet server URL |
| `edge_vps_elastic_dns_server` | `10.43.0.10` | DNS server for agent |

## Secrets

Store secrets in `vars/group_vars/vps/secrets.yaml` (ansible-vault encrypted):

```yaml
vault_edge_vps:
  wireguard:
    private_key: "..."
    peers: [...]
  pangolin:
    server_secret: "..."
  traefik:
    cloudflare_api_token: "..."
  elastic:
    fleet_enrollment_token: "..."

Dependencies

None.

Example Playbook

- hosts: vps
  roles:
    - role: edge_vps

License

MIT


**Step 2: Commit**

```bash
git add README.md
git commit -m "docs(edge_vps): update README with role documentation"

Task 10: Move Certificate Files

Files:

  • Move: files/agent/agent/elasticsearch-ca.crtfiles/elastic-agent/
  • Move: files/agent/agent/fleet-ca.crtfiles/elastic-agent/

Step 1: Move certificate files

Run:

mkdir -p files/elastic-agent
mv files/agent/agent/elasticsearch-ca.crt files/elastic-agent/
mv files/agent/agent/fleet-ca.crt files/elastic-agent/
rm -rf files/agent

Step 2: Commit

git add files/
git commit -m "refactor(edge_vps): reorganize certificate files"