netbird-gitops/poc/ansible/playbook.yml
2026-03-06 17:11:28 +02:00

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
============================================================