--- # ============================================================================= # 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 SSH community.general.ufw: rule: allow port: "{{ gitea_ssh_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. Gitea Actions Runner # ========================================================================= # The runner needs Gitea to be up and a registration token. # On first deploy, skip this (vault_gitea_runner_token is empty). # After Gitea is running, get the token from Site Administration → # Actions → Runners, add it to vault.yml, and re-run. - name: Create runner directory ansible.builtin.file: path: "{{ gitea_runner_dir }}" state: directory mode: "0755" when: vault_gitea_runner_token | default('') | length > 0 - name: Download act_runner binary ansible.builtin.get_url: url: "https://gitea.com/gitea/act_runner/releases/download/v{{ gitea_runner_version }}/act_runner-{{ gitea_runner_version }}-linux-amd64" dest: "{{ gitea_runner_dir }}/act_runner" mode: "0755" when: vault_gitea_runner_token | default('') | length > 0 - name: Check if runner is already registered ansible.builtin.stat: path: "{{ gitea_runner_dir }}/.runner" register: _runner_config when: vault_gitea_runner_token | default('') | length > 0 - name: Register runner with Gitea ansible.builtin.command: cmd: >- {{ gitea_runner_dir }}/act_runner register --instance https://{{ gitea_domain }} --token {{ vault_gitea_runner_token }} --name {{ gitea_runner_name }} --labels {{ gitea_runner_labels }} --no-interactive chdir: "{{ gitea_runner_dir }}" when: - vault_gitea_runner_token | default('') | length > 0 - not (_runner_config.stat.exists | default(false)) - name: Create systemd service for runner ansible.builtin.copy: dest: /etc/systemd/system/gitea-runner.service mode: "0644" content: | [Unit] Description=Gitea Actions Runner After=network.target docker.service Requires=docker.service [Service] Type=simple User=root WorkingDirectory={{ gitea_runner_dir }} ExecStart={{ gitea_runner_dir }}/act_runner daemon Restart=always RestartSec=10 [Install] WantedBy=multi-user.target when: vault_gitea_runner_token | default('') | length > 0 - name: Start and enable runner service ansible.builtin.systemd: name: gitea-runner daemon_reload: true state: started enabled: true when: vault_gitea_runner_token | default('') | length > 0 - name: Skip runner (no token provided) ansible.builtin.debug: msg: >- Skipping Gitea Actions runner — vault_gitea_runner_token is empty. After Gitea is running, get the token from https://{{ gitea_domain }}/-/admin/actions/runners and add it to vault.yml. when: vault_gitea_runner_token | default('') | length == 0 # ========================================================================= # 10. 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 + Gitea PoC deployed on VPS-A ============================================================ Dashboard: https://{{ netbird_domain }} Gitea: https://{{ gitea_domain }} Gitea SSH: ssh://git@{{ gitea_domain }}:{{ gitea_ssh_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. Open Gitea, complete install wizard, create org + repo 5. Go to Site Administration > Actions > Runners, copy token 6. Put the token in vault.yml as vault_gitea_runner_token 7. Re-run: ansible-playbook -i inventory.yml playbook.yml ============================================================