# 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: ```bash mkdir -p tasks handlers defaults templates/wireguard templates/traefik templates/pangolin templates/elastic-agent ``` **Step 2: Create defaults/main.yaml** ```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** ```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** ```bash 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** ```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** ```bash 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** ```jinja2 [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** ```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** ```bash 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** ```jinja2 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** ```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** ```bash 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** ```jinja2 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** ```yaml 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** ```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** ```bash 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** ```yaml fleet: enabled: true ``` **Step 2: Create templates/elastic-agent/docker-compose.yml.j2** ```yaml 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** ```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** ```bash 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** ```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** ```bash 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** ```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)** ```yaml 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: ```bash ansible-vault encrypt vars/group_vars/vps/secrets.yaml ``` **Step 4: Commit** ```bash 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** ```markdown # 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 ```yaml - 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.crt` → `files/elastic-agent/` - Move: `files/agent/agent/fleet-ca.crt` → `files/elastic-agent/` **Step 1: Move certificate files** Run: ```bash 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** ```bash git add files/ git commit -m "refactor(edge_vps): reorganize certificate files" ```