452 lines
16 KiB
YAML
452 lines
16 KiB
YAML
---
|
|
# =============================================================================
|
|
# 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
|
|
============================================================
|