Add bootstrap playbook + Homarr deploy (ansible user, Infisical secrets)
This commit is contained in:
parent
4c337f6d08
commit
998e0cbad5
@ -1,18 +1,26 @@
|
|||||||
# Semaphore deployment inventory
|
# Semaphore deployment inventory
|
||||||
# Hosts reached over the NetBird mesh (Semaphore LXC .99 is on the mesh).
|
# Reached over the NetBird mesh (Semaphore LXC .99 is on the mesh).
|
||||||
|
#
|
||||||
|
# Two connection styles:
|
||||||
|
# - bootstrap.yml connects as root (ansible_user=root) using ANSIBLE_BECOME_ROOT_PASS
|
||||||
|
# - post-bootstrap playbooks connect as the `ansible` user via the deploy SSH key
|
||||||
|
#
|
||||||
|
# Semaphore injects the SSH private key (SSH_PRIVATE_KEY) and passwords from Infisical.
|
||||||
all:
|
all:
|
||||||
|
vars:
|
||||||
|
ansible_python_interpreter: /usr/bin/python3
|
||||||
children:
|
children:
|
||||||
vps:
|
vps:
|
||||||
hosts:
|
hosts:
|
||||||
racknerd1:
|
racknerd1:
|
||||||
ansible_host: 100.96.198.46 # NetBird mesh IP (public: 172.245.72.108)
|
ansible_host: 100.96.198.46 # mesh IP (public: 172.245.72.108)
|
||||||
ansible_user: root
|
|
||||||
racknerd2:
|
racknerd2:
|
||||||
ansible_host: 100.96.217.250
|
ansible_host: 100.96.217.250
|
||||||
ansible_user: root
|
|
||||||
racknerd3:
|
racknerd3:
|
||||||
ansible_host: 67.215.255.203
|
ansible_host: 67.215.255.203
|
||||||
ansible_user: root
|
vars:
|
||||||
|
# default connection user AFTER bootstrap; override to root for bootstrap.yml
|
||||||
|
ansible_user: "{{ connect_user | default('ansible') }}"
|
||||||
dashboards:
|
dashboards:
|
||||||
hosts:
|
hosts:
|
||||||
racknerd1:
|
racknerd1:
|
||||||
|
|||||||
126
playbooks/bootstrap.yml
Normal file
126
playbooks/bootstrap.yml
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
# ============================================================================
|
||||||
|
# bootstrap.yml — prepare a fresh VPS/host for managed deployments
|
||||||
|
# ============================================================================
|
||||||
|
# Run this FIRST against any new host. Sequence:
|
||||||
|
# 1. Connection test (as root)
|
||||||
|
# 2. Clean up old-deployment clutter (explicit allowlist only — safe by default)
|
||||||
|
# 3. Create the `ansible` user with SSH key + password
|
||||||
|
# 4. Install the public key to /home/ansible/.ssh/authorized_keys
|
||||||
|
# 5. Verify the ansible user can log in and sudo
|
||||||
|
#
|
||||||
|
# Secrets come from Infisical (injected by Semaphore as env vars):
|
||||||
|
# SSH_PUBLIC_KEY - OpenSSH public key for the ansible user
|
||||||
|
# ANSIBLE_USER_PASS - password for the ansible user
|
||||||
|
# ANSIBLE_BECOME_ROOT_PASS - current root/sudo password on the target
|
||||||
|
#
|
||||||
|
# Usage (Semaphore Task Template):
|
||||||
|
# playbook: playbooks/bootstrap.yml
|
||||||
|
# inventory: inventory/hosts.yml
|
||||||
|
# limit: racknerd1
|
||||||
|
# Extra vars (optional): cleanup_paths='["/opt/old-thing","/opt/nuclio"]'
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
- name: Bootstrap managed host
|
||||||
|
hosts: "{{ target | default('racknerd1') }}"
|
||||||
|
become: true
|
||||||
|
gather_facts: true
|
||||||
|
vars:
|
||||||
|
managed_user: ansible
|
||||||
|
# Secrets pulled from Infisical env injection:
|
||||||
|
ansible_user_password: "{{ lookup('env', 'ANSIBLE_USER_PASS') }}"
|
||||||
|
managed_ssh_pubkey: "{{ lookup('env', 'SSH_PUBLIC_KEY') }}"
|
||||||
|
# SAFETY: cleanup removes ONLY paths explicitly listed here. Empty = deletes nothing.
|
||||||
|
# Override per-host from Semaphore (survey/extra-vars). NEVER a blind sweep.
|
||||||
|
cleanup_paths: []
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
# ---------- STEP 1: connection test ----------
|
||||||
|
- name: "[1] Connection test — confirm we can reach the host and run as root"
|
||||||
|
ansible.builtin.command: id
|
||||||
|
register: idcheck
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: "[1] Report connection"
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "Connected to {{ inventory_hostname }} ({{ ansible_host | default('n/a') }}) as {{ idcheck.stdout }}"
|
||||||
|
|
||||||
|
- name: "[1] Assert we are root (become worked)"
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that: "'uid=0' in idcheck.stdout"
|
||||||
|
fail_msg: "Not running as root — check become password (ANSIBLE_BECOME_ROOT_PASS)."
|
||||||
|
|
||||||
|
# ---------- STEP 2: cleanup (explicit allowlist only) ----------
|
||||||
|
- name: "[2] Show cleanup plan"
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
Cleanup paths to remove: {{ cleanup_paths | default([]) }}
|
||||||
|
{{ '(none — skipping)' if (cleanup_paths | length == 0) else '' }}
|
||||||
|
|
||||||
|
- name: "[2] Remove ONLY explicitly-listed old deployment paths"
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: absent
|
||||||
|
loop: "{{ cleanup_paths | default([]) }}"
|
||||||
|
when: cleanup_paths | length > 0
|
||||||
|
|
||||||
|
# ---------- STEP 3 + 4: create ansible user + install key ----------
|
||||||
|
- name: "[3] Validate required secrets are present"
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- managed_ssh_pubkey | length > 0
|
||||||
|
- managed_ssh_pubkey.startswith('ssh-')
|
||||||
|
- ansible_user_password | length > 0
|
||||||
|
fail_msg: >-
|
||||||
|
Missing/invalid secrets. SSH_PUBLIC_KEY must be OpenSSH format (ssh-...)
|
||||||
|
and ANSIBLE_USER_PASS must be set (from Infisical).
|
||||||
|
|
||||||
|
- name: "[3] Create the '{{ managed_user }}' user"
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ managed_user }}"
|
||||||
|
password: "{{ ansible_user_password | password_hash('sha512') }}"
|
||||||
|
shell: /bin/bash
|
||||||
|
groups: "{{ 'sudo' if ansible_os_family == 'Debian' else 'wheel' }}"
|
||||||
|
append: true
|
||||||
|
create_home: true
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: "[4] Ensure ~/.ssh exists for {{ managed_user }}"
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "/home/{{ managed_user }}/.ssh"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ managed_user }}"
|
||||||
|
group: "{{ managed_user }}"
|
||||||
|
mode: "0700"
|
||||||
|
|
||||||
|
- name: "[4] Install public key to {{ managed_user }} authorized_keys"
|
||||||
|
ansible.posix.authorized_key:
|
||||||
|
user: "{{ managed_user }}"
|
||||||
|
key: "{{ managed_ssh_pubkey }}"
|
||||||
|
state: present
|
||||||
|
exclusive: false
|
||||||
|
|
||||||
|
- name: "[3] Allow passwordless-or-password sudo for {{ managed_user }}"
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "/etc/sudoers.d/{{ managed_user }}"
|
||||||
|
content: "{{ managed_user }} ALL=(ALL) ALL\n"
|
||||||
|
mode: "0440"
|
||||||
|
validate: "visudo -cf %s"
|
||||||
|
|
||||||
|
# ---------- STEP 5: verify ----------
|
||||||
|
- name: "[5] Verify {{ managed_user }} exists and is in sudo group"
|
||||||
|
ansible.builtin.command: "id {{ managed_user }}"
|
||||||
|
register: userverify
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: "[5] Verify authorized_keys installed"
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "/home/{{ managed_user }}/.ssh/authorized_keys"
|
||||||
|
register: akfile
|
||||||
|
|
||||||
|
- name: "[5] Bootstrap result"
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg:
|
||||||
|
- "User: {{ userverify.stdout }}"
|
||||||
|
- "authorized_keys present: {{ akfile.stat.exists }}"
|
||||||
|
- "Host {{ inventory_hostname }} is ready. Deploys can now run as '{{ managed_user }}'."
|
||||||
@ -1,45 +1,61 @@
|
|||||||
---
|
---
|
||||||
# Deploy Homarr dashboard via Docker Compose.
|
# ============================================================================
|
||||||
# Usage (Semaphore Task Template): playbook = playbooks/deploy-homarr.yml, inventory = inventory/hosts.yml, limit = racknerd1
|
# deploy-homarr.yml — deploy Homarr dashboard as the `ansible` user
|
||||||
|
# ============================================================================
|
||||||
|
# Prereq: host has been bootstrapped (see bootstrap.yml) so the `ansible`
|
||||||
|
# user exists with the SSH key installed. This playbook connects AS ansible,
|
||||||
|
# creates ~/docker/homarr/, drops the compose file, and brings it up.
|
||||||
#
|
#
|
||||||
# REQUIRED extra var (set in Semaphore -> Variable Groups / Survey, NOT committed):
|
# Secret from Infisical (Semaphore env injection):
|
||||||
# homarr_secret_encryption_key : 64-char hex (openssl rand -hex 32). Homarr v1 refuses to start without it.
|
# HOMARR_SECRET_ENCRYPTION_KEY - 64-char hex (openssl rand -hex 32)
|
||||||
- name: Deploy Homarr
|
#
|
||||||
hosts: racknerd1
|
# Usage (Semaphore Task Template):
|
||||||
become: true
|
# playbook: playbooks/deploy-homarr.yml
|
||||||
|
# inventory: inventory/hosts.yml
|
||||||
|
# limit: racknerd1
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
- name: Deploy Homarr (as ansible user)
|
||||||
|
hosts: "{{ target | default('racknerd1') }}"
|
||||||
|
become: true # sudo for docker
|
||||||
|
gather_facts: true
|
||||||
vars:
|
vars:
|
||||||
homarr_dir: /opt/homarr
|
deploy_user: ansible
|
||||||
|
homarr_dir: "/home/{{ deploy_user }}/docker/homarr"
|
||||||
homarr_port: 7575
|
homarr_port: 7575
|
||||||
homarr_image: ghcr.io/homarr-labs/homarr:latest
|
homarr_image: ghcr.io/homarr-labs/homarr:latest
|
||||||
# homarr_secret_encryption_key: "<injected by Semaphore>"
|
homarr_encryption_key: "{{ lookup('env', 'HOMARR_SECRET_ENCRYPTION_KEY') }}"
|
||||||
tasks:
|
|
||||||
- name: Ensure Docker is present
|
|
||||||
ansible.builtin.command: docker version
|
|
||||||
register: docker_check
|
|
||||||
changed_when: false
|
|
||||||
failed_when: docker_check.rc != 0
|
|
||||||
|
|
||||||
- name: Fail early if encryption key not provided
|
tasks:
|
||||||
|
- name: Validate encryption key present (64-hex)
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- homarr_secret_encryption_key is defined
|
- homarr_encryption_key | length == 64
|
||||||
- homarr_secret_encryption_key | length == 64
|
fail_msg: "HOMARR_SECRET_ENCRYPTION_KEY must be 64 hex chars (from Infisical)."
|
||||||
fail_msg: >-
|
|
||||||
homarr_secret_encryption_key must be a 64-char hex string.
|
|
||||||
Generate with `openssl rand -hex 32` and set it as a Semaphore variable.
|
|
||||||
|
|
||||||
- name: Create Homarr directories
|
- name: Confirm Docker is available
|
||||||
|
ansible.builtin.command: docker version
|
||||||
|
register: dv
|
||||||
|
changed_when: false
|
||||||
|
failed_when: dv.rc != 0
|
||||||
|
|
||||||
|
- name: Create ~/docker/homarr directory tree (owned by {{ deploy_user }})
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ item }}"
|
path: "{{ item }}"
|
||||||
state: directory
|
state: directory
|
||||||
|
owner: "{{ deploy_user }}"
|
||||||
|
group: "{{ deploy_user }}"
|
||||||
mode: "0755"
|
mode: "0755"
|
||||||
loop:
|
loop:
|
||||||
|
- "/home/{{ deploy_user }}/docker"
|
||||||
- "{{ homarr_dir }}"
|
- "{{ homarr_dir }}"
|
||||||
- "{{ homarr_dir }}/appdata"
|
- "{{ homarr_dir }}/appdata"
|
||||||
|
|
||||||
- name: Write docker-compose.yml
|
- name: Write docker-compose.yml
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
dest: "{{ homarr_dir }}/docker-compose.yml"
|
dest: "{{ homarr_dir }}/docker-compose.yml"
|
||||||
|
owner: "{{ deploy_user }}"
|
||||||
|
group: "{{ deploy_user }}"
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
content: |
|
content: |
|
||||||
services:
|
services:
|
||||||
@ -51,7 +67,7 @@
|
|||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- {{ homarr_dir }}/appdata:/appdata
|
- {{ homarr_dir }}/appdata:/appdata
|
||||||
environment:
|
environment:
|
||||||
- SECRET_ENCRYPTION_KEY={{ homarr_secret_encryption_key }}
|
- SECRET_ENCRYPTION_KEY={{ homarr_encryption_key }}
|
||||||
ports:
|
ports:
|
||||||
- "{{ homarr_port }}:7575"
|
- "{{ homarr_port }}:7575"
|
||||||
|
|
||||||
@ -60,17 +76,17 @@
|
|||||||
project_src: "{{ homarr_dir }}"
|
project_src: "{{ homarr_dir }}"
|
||||||
pull: always
|
pull: always
|
||||||
state: present
|
state: present
|
||||||
register: compose_result
|
register: compose_up
|
||||||
|
|
||||||
- name: Wait for Homarr to answer
|
- name: Wait for Homarr to respond
|
||||||
ansible.builtin.uri:
|
ansible.builtin.uri:
|
||||||
url: "http://127.0.0.1:{{ homarr_port }}/"
|
url: "http://127.0.0.1:{{ homarr_port }}/"
|
||||||
status_code: [200, 302, 307]
|
status_code: [200, 302, 307]
|
||||||
register: health
|
register: health
|
||||||
retries: 12
|
retries: 15
|
||||||
delay: 5
|
delay: 5
|
||||||
until: health.status is defined
|
until: health.status is defined and health.status in [200, 302, 307]
|
||||||
|
|
||||||
- name: Report
|
- name: Result
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
msg: "Homarr is up on {{ inventory_hostname }}:{{ homarr_port }} (status {{ health.status | default('n/a') }})"
|
msg: "Homarr up on {{ inventory_hostname }}:{{ homarr_port }} (HTTP {{ health.status }})"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user