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
|
||||
# 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:
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
children:
|
||||
vps:
|
||||
hosts:
|
||||
racknerd1:
|
||||
ansible_host: 100.96.198.46 # NetBird mesh IP (public: 172.245.72.108)
|
||||
ansible_user: root
|
||||
ansible_host: 100.96.198.46 # mesh IP (public: 172.245.72.108)
|
||||
racknerd2:
|
||||
ansible_host: 100.96.217.250
|
||||
ansible_user: root
|
||||
racknerd3:
|
||||
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:
|
||||
hosts:
|
||||
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):
|
||||
# homarr_secret_encryption_key : 64-char hex (openssl rand -hex 32). Homarr v1 refuses to start without it.
|
||||
- name: Deploy Homarr
|
||||
hosts: racknerd1
|
||||
become: true
|
||||
# Secret from Infisical (Semaphore env injection):
|
||||
# HOMARR_SECRET_ENCRYPTION_KEY - 64-char hex (openssl rand -hex 32)
|
||||
#
|
||||
# Usage (Semaphore Task Template):
|
||||
# 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:
|
||||
homarr_dir: /opt/homarr
|
||||
deploy_user: ansible
|
||||
homarr_dir: "/home/{{ deploy_user }}/docker/homarr"
|
||||
homarr_port: 7575
|
||||
homarr_image: ghcr.io/homarr-labs/homarr:latest
|
||||
# homarr_secret_encryption_key: "<injected by Semaphore>"
|
||||
tasks:
|
||||
- name: Ensure Docker is present
|
||||
ansible.builtin.command: docker version
|
||||
register: docker_check
|
||||
changed_when: false
|
||||
failed_when: docker_check.rc != 0
|
||||
homarr_encryption_key: "{{ lookup('env', 'HOMARR_SECRET_ENCRYPTION_KEY') }}"
|
||||
|
||||
- name: Fail early if encryption key not provided
|
||||
tasks:
|
||||
- name: Validate encryption key present (64-hex)
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- homarr_secret_encryption_key is defined
|
||||
- homarr_secret_encryption_key | length == 64
|
||||
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.
|
||||
- homarr_encryption_key | length == 64
|
||||
fail_msg: "HOMARR_SECRET_ENCRYPTION_KEY must be 64 hex chars (from Infisical)."
|
||||
|
||||
- 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:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ deploy_user }}"
|
||||
group: "{{ deploy_user }}"
|
||||
mode: "0755"
|
||||
loop:
|
||||
- "/home/{{ deploy_user }}/docker"
|
||||
- "{{ homarr_dir }}"
|
||||
- "{{ homarr_dir }}/appdata"
|
||||
|
||||
- name: Write docker-compose.yml
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ homarr_dir }}/docker-compose.yml"
|
||||
owner: "{{ deploy_user }}"
|
||||
group: "{{ deploy_user }}"
|
||||
mode: "0644"
|
||||
content: |
|
||||
services:
|
||||
@ -51,7 +67,7 @@
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- {{ homarr_dir }}/appdata:/appdata
|
||||
environment:
|
||||
- SECRET_ENCRYPTION_KEY={{ homarr_secret_encryption_key }}
|
||||
- SECRET_ENCRYPTION_KEY={{ homarr_encryption_key }}
|
||||
ports:
|
||||
- "{{ homarr_port }}:7575"
|
||||
|
||||
@ -60,17 +76,17 @@
|
||||
project_src: "{{ homarr_dir }}"
|
||||
pull: always
|
||||
state: present
|
||||
register: compose_result
|
||||
register: compose_up
|
||||
|
||||
- name: Wait for Homarr to answer
|
||||
- name: Wait for Homarr to respond
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ homarr_port }}/"
|
||||
status_code: [200, 302, 307]
|
||||
register: health
|
||||
retries: 12
|
||||
retries: 15
|
||||
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:
|
||||
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