--- # ============================================================================= # NetBird + Reconciler + Gitea — PoC Deployment # ============================================================================= # Deploys a self-contained stack on VPS-A for end-to-end reconciler testing. # # Prerequisites: # - SSH access to VPS-A (46.225.220.61) # - DNS A record: vps-a.networkmonitor.cc -> 46.225.220.61 # - rsync installed locally and on VPS-A # - poc/ansible/group_vars/all/vault.yml (copy from vault.yml.example) # # Run: # cd poc/ansible # ansible-playbook -i inventory.yml playbook.yml # ============================================================================= - name: Deploy NetBird + Reconciler PoC on VPS-A hosts: poc_servers become: true tasks: # ========================================================================= # 1. Generate secrets (if vault values are empty) # ========================================================================= # vault_* vars come from group_vars/all/vault.yml. When left as empty # strings, the playbook auto-generates values. On subsequent runs with # filled-in vault.yml, the provided values are used instead. - name: Generate encryption key (if not provided) ansible.builtin.shell: openssl rand -base64 32 register: _gen_encryption_key changed_when: false when: vault_encryption_key | default('') | length == 0 - name: Generate TURN password (if not provided) ansible.builtin.shell: openssl rand -hex 32 register: _gen_turn_password changed_when: false when: vault_turn_password | default('') | length == 0 - name: Generate relay secret (if not provided) ansible.builtin.shell: openssl rand -hex 32 register: _gen_relay_secret changed_when: false when: vault_relay_secret | default('') | length == 0 - name: Generate reconciler token (if not provided) ansible.builtin.shell: openssl rand -hex 32 register: _gen_reconciler_token changed_when: false when: vault_reconciler_token | default('') | length == 0 - name: Set effective secrets ansible.builtin.set_fact: vault_encryption_key: "{{ vault_encryption_key if (vault_encryption_key | default('') | length > 0) else _gen_encryption_key.stdout }}" vault_turn_password: "{{ vault_turn_password if (vault_turn_password | default('') | length > 0) else _gen_turn_password.stdout }}" vault_relay_secret: "{{ vault_relay_secret if (vault_relay_secret | default('') | length > 0) else _gen_relay_secret.stdout }}" vault_reconciler_token: "{{ vault_reconciler_token if (vault_reconciler_token | default('') | length > 0) else _gen_reconciler_token.stdout }}" # ========================================================================= # 2. Install Docker # ========================================================================= - name: Update apt cache ansible.builtin.apt: update_cache: true cache_valid_time: 3600 - name: Install prerequisites ansible.builtin.apt: name: - apt-transport-https - ca-certificates - curl - gnupg - lsb-release - jq - rsync state: present - name: Check if Docker is installed ansible.builtin.command: docker --version register: _docker_check changed_when: false failed_when: false - name: Create keyrings directory ansible.builtin.file: path: /etc/apt/keyrings state: directory mode: "0755" when: _docker_check.rc != 0 - name: Add Docker GPG key ansible.builtin.shell: | curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg args: creates: /etc/apt/keyrings/docker.gpg when: _docker_check.rc != 0 - name: Add Docker repository ansible.builtin.apt_repository: repo: >- deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable state: present filename: docker when: _docker_check.rc != 0 - name: Install Docker packages ansible.builtin.apt: name: - docker-ce - docker-ce-cli - containerd.io - docker-compose-plugin state: present update_cache: true when: _docker_check.rc != 0 - name: Start and enable Docker ansible.builtin.systemd: name: docker state: started enabled: true # ========================================================================= # 3. UFW firewall # ========================================================================= - name: Install UFW ansible.builtin.apt: name: ufw state: present - name: Allow SSH community.general.ufw: rule: allow port: "22" proto: tcp - name: Allow HTTP (ACME + Caddy) community.general.ufw: rule: allow port: "80" proto: tcp - name: Allow HTTPS community.general.ufw: rule: allow port: "443" proto: tcp - name: Allow TURN UDP community.general.ufw: rule: allow port: "3478" proto: udp - name: Allow TURN TCP community.general.ufw: rule: allow port: "3478" proto: tcp - name: Allow Gitea HTTP community.general.ufw: rule: allow port: "{{ gitea_http_port | string }}" proto: tcp - name: Enable UFW (default deny incoming) community.general.ufw: state: enabled policy: deny # ========================================================================= # 4. Create directories # ========================================================================= - name: Create base directory ansible.builtin.file: path: "{{ base_dir }}" state: directory mode: "0755" - name: Create config directory ansible.builtin.file: path: "{{ base_dir }}/config" state: directory mode: "0755" - name: Create reconciler source directory ansible.builtin.file: path: "{{ base_dir }}/reconciler-src" state: directory mode: "0755" # ========================================================================= # 5. Sync reconciler source code # ========================================================================= # Uses rsync to copy the project root (minus junk) to VPS-A so that # `docker compose build` can build the reconciler image on the server. - name: Sync reconciler source to VPS-A ansible.posix.synchronize: src: "{{ playbook_dir }}/../../" dest: "{{ base_dir }}/reconciler-src/" delete: true rsync_opts: - "--exclude=.git" - "--exclude=node_modules" - "--exclude=poc" - "--exclude=data" - "--exclude=deploy" - "--exclude=.beads" # synchronize runs as the connecting user, not become. We need to # set become: false so it uses the SSH user directly for rsync. become: false # ========================================================================= # 6. Template configs # ========================================================================= - name: Deploy docker-compose.yml ansible.builtin.template: src: docker-compose.yml.j2 dest: "{{ base_dir }}/docker-compose.yml" mode: "0644" register: _compose_changed - name: Deploy management.json ansible.builtin.template: src: management.json.j2 dest: "{{ base_dir }}/config/management.json" mode: "0644" register: _management_changed - name: Deploy Caddyfile ansible.builtin.template: src: Caddyfile.j2 dest: "{{ base_dir }}/config/Caddyfile" mode: "0644" - name: Deploy dashboard.env ansible.builtin.template: src: dashboard.env.j2 dest: "{{ base_dir }}/config/dashboard.env" mode: "0640" - name: Deploy relay.env ansible.builtin.template: src: relay.env.j2 dest: "{{ base_dir }}/config/relay.env" mode: "0640" - name: Deploy turnserver.conf ansible.builtin.template: src: turnserver.conf.j2 dest: "{{ base_dir }}/config/turnserver.conf" mode: "0644" - name: Deploy reconciler.env ansible.builtin.template: src: reconciler.env.j2 dest: "{{ base_dir }}/config/reconciler.env" mode: "0640" register: _reconciler_env_changed # ========================================================================= # 7. Docker Compose — pull, build, up # ========================================================================= - name: Pull Docker images ansible.builtin.command: cmd: docker compose pull --ignore-buildable chdir: "{{ base_dir }}" changed_when: true - name: Build reconciler image ansible.builtin.command: cmd: docker compose build reconciler chdir: "{{ base_dir }}" changed_when: true - name: Start all services ansible.builtin.command: cmd: docker compose up -d chdir: "{{ base_dir }}" changed_when: true # ========================================================================= # 8. Health checks # ========================================================================= - name: Wait for management container to be running ansible.builtin.command: cmd: docker compose ps management --format json chdir: "{{ base_dir }}" register: _mgmt_status until: "'running' in _mgmt_status.stdout and 'restarting' not in _mgmt_status.stdout" retries: 15 delay: 5 changed_when: false - name: Wait for Caddy / HTTPS to respond ansible.builtin.uri: url: "https://{{ netbird_domain }}" method: GET status_code: 200 validate_certs: false register: _caddy_check until: _caddy_check.status == 200 retries: 12 delay: 5 - name: Check reconciler health (may fail if API token not yet configured) ansible.builtin.uri: url: "http://127.0.0.1:{{ reconciler_port }}/health" method: GET status_code: 200 register: _reconciler_check failed_when: false changed_when: false # ========================================================================= # 9. Summary # ========================================================================= - name: Note about NetBird API token ansible.builtin.debug: msg: >- The reconciler needs a NetBird API token to function. If vault_netbird_api_token is empty, the reconciler will crash-loop until you create an admin account via the dashboard, generate an API token, add it to vault.yml, and re-run the playbook. when: vault_netbird_api_token | default('') | length == 0 - name: Deployment summary ansible.builtin.debug: msg: | ============================================================ NetBird + Reconciler PoC deployed on VPS-A ============================================================ Dashboard: https://{{ netbird_domain }} Gitea: http://{{ netbird_domain }}:{{ gitea_http_port }} Reconciler: https://{{ netbird_domain }}/reconciler/health Reconciler status: {{ 'healthy' if (_reconciler_check.status | default(0)) == 200 else 'NOT YET READY (see note above)' }} Generated secrets (save these to vault.yml for idempotent re-runs): vault_encryption_key: {{ vault_encryption_key }} vault_turn_password: {{ vault_turn_password }} vault_relay_secret: {{ vault_relay_secret }} vault_reconciler_token: {{ vault_reconciler_token }} Next steps: 1. Open the dashboard and create an admin account 2. Go to Settings > API > generate a Personal Access Token 3. Put the token in vault.yml as vault_netbird_api_token 4. Re-run: ansible-playbook -i inventory.yml playbook.yml ============================================================