refactor: consolidate all roles into ansible/roles/ and update ansible.cfg

- Move all roles from playbooks/roles/ to roles/
- Update roles_path in ansible.cfg
- Add cast user to common role
- Create standalone podman role
- Add semaphore role with Podman + Quadlet support
This commit is contained in:
Hermes Agent service account
2026-05-26 22:22:08 -05:00
parent 9250b0f193
commit 92b2a9d609
98 changed files with 158 additions and 78 deletions

View File

@@ -0,0 +1,6 @@
---
- name: Deploy Semaphore on figment
hosts: figment
become: true
roles:
- semaphore

View File

@@ -1,11 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/authentik/defaults/main.yml
# ------------------------------------------------------------------------------
authentik_base_dir: /opt/docker/authentik
# All secrets MUST be set in vault
authentik_secret_key: ""
authentik_admin_password: ""
authentik_database_password: ""

View File

@@ -1,8 +0,0 @@
# ------------------------------------------------------------------------------
# FILE: roles/authentik/handlers/main.yml
# ------------------------------------------------------------------------------
- name: restart authentik
community.docker.docker_compose_v2:
project_src: "{{ authentik_base_dir }}"
state: restarted

View File

@@ -1,51 +0,0 @@
---
# ──────────────────────────────────────────────────────────────────────────────
# FILE: roles/authentik/tasks/main.yml
# DESCRIPTION: Deploys Authentik from boilerplate-generated files in the repo.
# The compose.yaml lives in boilerplates/authentik/.
# The .env file is templated to inject secrets from Ansible vault.
# Custom blueprints are synced from boilerplates/authentik/blueprints/.
# ──────────────────────────────────────────────────────────────────────────────
- name: Create Authentik directory structure
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ ansible_user }}"
group: docker
mode: '0775'
loop:
- "{{ authentik_base_dir }}"
- "{{ authentik_base_dir }}/blueprints"
- name: Copy compose.yaml from boilerplate
ansible.builtin.copy:
src: "{{ playbook_dir }}/../../boilerplates/authentik/compose.yaml"
dest: "{{ authentik_base_dir }}/compose.yaml"
owner: "{{ ansible_user }}"
group: docker
mode: '0644'
notify: restart authentik
- name: Sync custom blueprints
ansible.builtin.copy:
src: "{{ playbook_dir }}/../../boilerplates/authentik/blueprints/"
dest: "{{ authentik_base_dir }}/blueprints/"
owner: "{{ ansible_user }}"
group: docker
mode: '0644'
notify: restart authentik
- name: Deploy .env with secrets from vault
ansible.builtin.template:
src: env.j2
dest: "{{ authentik_base_dir }}/.env"
owner: "{{ ansible_user }}"
group: docker
mode: '0600'
notify: restart authentik
- name: Start Authentik
community.docker.docker_compose_v2:
project_src: "{{ authentik_base_dir }}"
state: present

View File

@@ -1,13 +0,0 @@
# Managed by Ansible — do not edit manually
AUTHENTIK_SECRET_KEY={{ authentik_secret_key }}
DATABASE_PASSWORD={{ authentik_database_password }}
AUTHENTIK_ADMIN_PASSWORD={{ authentik_admin_password }}
# OIDC client credentials for blueprints
# These are substituted into blueprint YAML via ${VAR} syntax
PROXMOX_OIDC_CLIENT_ID={{ vault_proxmox_oidc_client_id }}
PROXMOX_OIDC_CLIENT_SECRET={{ vault_proxmox_oidc_client_secret }}
STEPCA_OIDC_CLIENT_ID={{ vault_stepca_oidc_client_id }}
STEPCA_OIDC_CLIENT_SECRET={{ vault_stepca_oidc_client_secret }}
GITEA_OIDC_CLIENT_ID={{ vault_gitea_oidc_client_id }}
GITEA_OIDC_CLIENT_SECRET={{ vault_gitea_oidc_client_secret }}

View File

@@ -1,35 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/common/defaults/main.yml
# ------------------------------------------------------------------------------
common_timezone: America/Chicago
common_ntp_server: "sundial.local.mk-labs.cloud"
common_packages:
- acl
- curl
- wget
- vim
- htop
- git
- jq
- unzip
- ca-certificates
- gnupg
- lsb-release
- net-tools
- dnsutils
- lvm2
# ------------------------------------------------------------------------------
# LVM root volume expansion
# Override common_root_pv/vg/lv per host if the template uses different names.
# Ubuntu 24.04 cloud templates use ubuntu-vg/ubuntu-lv by default.
# ------------------------------------------------------------------------------
common_expand_root_lvm: true
common_root_pv: /dev/sda3 # Ubuntu cloud-init partition layout
common_root_vg: ubuntu-vg
common_root_lv: ubuntu-lv

View File

@@ -1,9 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/common/handlers/main.yml
# ------------------------------------------------------------------------------
- name: restart chrony
systemd:
name: chrony
state: restarted

View File

@@ -1,22 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/common/defaults/main.yml
# ------------------------------------------------------------------------------
common_timezone: America/Chicago
common_ntp_server: sundial.local.mk-labs.cloud
common_packages:
- curl
- wget
- vim
- htop
- git
- jq
- unzip
- ca-certificates
- gnupg
- lsb-release
- net-tools
- dnsutils

View File

@@ -1,77 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/common/tasks/main.yml
# DESCRIPTION: Baseline configuration applied to all managed Ubuntu hosts.
# Handles hostname, timezone, core packages, NTP, ansible user,
# and optional LVM root volume expansion.
# ------------------------------------------------------------------------------
- name: Set hostname
hostname:
name: "{{ inventory_hostname | replace('_', '-') }}"
- name: Set timezone
timezone:
name: "{{ common_timezone }}"
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Install base utility packages
apt:
name: "{{ common_packages }}"
state: present
- name: Install chrony
apt:
name: chrony
state: present
- name: Configure chrony to use sundial
template:
src: chrony.conf.j2
dest: /etc/chrony/chrony.conf
mode: '0644'
notify: restart chrony
- name: Ensure chrony is enabled and running
systemd:
name: chrony
state: started
enabled: yes
- name: Ensure ansible user has passwordless sudo
lineinfile:
path: /etc/sudoers.d/{{ ansible_user }}
line: "{{ ansible_user }} ALL=(ALL) NOPASSWD:ALL"
create: yes
mode: '0440'
validate: 'visudo -cf %s'
# ------------------------------------------------------------------------------
# LVM root volume expansion
# Extends the root PV to the full disk size and grows the LV + filesystem.
# This codifies what was previously done manually after provisioning.
# Runs only when common_expand_root_lvm is true (default: true).
# Safe to re-run — pvresize and lvextend are idempotent when already at max.
# ------------------------------------------------------------------------------
- name: Expand root PV to full disk size
command: pvresize {{ common_root_pv }}
register: pvresize_result
changed_when: "'changed' in pvresize_result.stdout or pvresize_result.rc == 0"
when: common_expand_root_lvm | bool
- name: Extend root LV to 100% of free VG space
lvol:
vg: "{{ common_root_vg }}"
lv: "{{ common_root_lv }}"
size: +100%FREE
resizefs: yes
when:
- common_expand_root_lvm | bool
ignore_errors: yes
# ignore_errors because lvextend returns non-zero when already at max size.
# resizefs: yes handles the resize2fs call inline — no separate task needed.

View File

@@ -1,136 +0,0 @@
# ------------------------------------------------------------------------------
# FILE: roles/common/tasks/step_ca_client.yml
# DESCRIPTION: Enroll host as a step-ca SSH client. Installs the step CLI,
# bootstraps trust with the CA, configures sshd to accept
# CA-signed user certificates, signs a host certificate, and
# sets up automated renewal via systemd timer.
# ------------------------------------------------------------------------------
- name: Install step CLI
apt:
deb: "https://dl.smallstep.com/cli/docs-ca-install/latest/step-cli_amd64.deb"
state: present
- name: Bootstrap step-ca trust
command: >
step ca bootstrap
--ca-url {{ step_ca_url }}
--fingerprint {{ step_ca_fingerprint }}
--install
args:
creates: /root/.step/certs/root_ca.crt
- name: Get SSH user CA public key
command: step ssh config --roots
register: ssh_user_ca_key
changed_when: false
- name: Write SSH user CA public key
copy:
content: "{{ ssh_user_ca_key.stdout }}"
dest: /etc/ssh/ssh_user_key.pub
mode: '0644'
notify: restart sshd
- name: Configure sshd to trust CA-signed user certificates
lineinfile:
path: /etc/ssh/sshd_config
line: "TrustedUserCAKeys /etc/ssh/ssh_user_key.pub"
regexp: "^#?TrustedUserCAKeys"
state: present
notify: restart sshd
# --- Host certificate signing ---
- name: Write provisioner password to temp file on controller
copy:
content: "{{ stepca_ca_password }}"
dest: /tmp/step_provisioner_pw
mode: '0600'
delegate_to: localhost
run_once: true
no_log: true
- name: Generate one-time host token
command: >
step ca token {{ inventory_hostname }}.local.mk-labs.cloud
--ssh --host
--provisioner {{ step_ca_provisioner_name }}
--password-file /tmp/step_provisioner_pw
delegate_to: localhost
register: host_token
changed_when: false
- name: Sign host certificate
command: >
step ssh certificate {{ inventory_hostname }}.local.mk-labs.cloud
/etc/ssh/ssh_host_ecdsa_key.pub
--host --sign
--token {{ host_token.stdout }}
--no-password --insecure
args:
creates: /etc/ssh/ssh_host_ecdsa_key-cert.pub
notify: restart sshd
- name: Configure sshd to present host certificate
blockinfile:
path: /etc/ssh/sshd_config
marker: "# {mark} STEP-CA HOST CERTIFICATE"
block: |
HostCertificate /etc/ssh/ssh_host_ecdsa_key-cert.pub
HostKey /etc/ssh/ssh_host_ecdsa_key
notify: restart sshd
- name: Clean up provisioner password from controller
file:
path: /tmp/step_provisioner_pw
state: absent
delegate_to: localhost
run_once: true
no_log: true
# --- Automated renewal via systemd ---
- name: Create host cert renewal script
copy:
content: |
#!/bin/bash
step ssh renew --force /etc/ssh/ssh_host_ecdsa_key-cert.pub /etc/ssh/ssh_host_ecdsa_key
systemctl restart sshd
dest: /usr/local/bin/step-renew-host-cert.sh
mode: '0755'
- name: Create systemd timer for host cert renewal
copy:
content: |
[Unit]
Description=Renew step-ca SSH host certificate
[Timer]
OnCalendar=weekly
Persistent=true
RandomizedDelaySec=3600
[Install]
WantedBy=timers.target
dest: /etc/systemd/system/step-renew-host-cert.timer
mode: '0644'
- name: Create systemd service for host cert renewal
copy:
content: |
[Unit]
Description=Renew step-ca SSH host certificate
[Service]
Type=oneshot
ExecStart=/usr/local/bin/step-renew-host-cert.sh
dest: /etc/systemd/system/step-renew-host-cert.service
mode: '0644'
- name: Enable and start renewal timer
systemd:
name: step-renew-host-cert.timer
state: started
enabled: yes
daemon_reload: yes

View File

@@ -1,5 +0,0 @@
# Managed by Ansible — do not edit manually
server {{ common_ntp_server }} iburst
driftfile /var/lib/chrony/chrony.drift
makestep 1.0 3
rtcsync

View File

@@ -1,59 +0,0 @@
# ------------------------------------------------------------------------------
# FILE: roles/control-plane/tasks/main.yml
# ------------------------------------------------------------------------------
- name: Check if cluster is already initialized
stat:
path: /etc/kubernetes/admin.conf
register: kube_init_stat
- name: Initialize the Kubernetes cluster
when: not kube_init_stat.stat.exists
command: >
kubeadm init
--pod-network-cidr=10.244.0.0/16
--control-plane-endpoint={{ inventory_hostname }}
register: kubeadm_init
- name: Create .kube directory for the user
become: no
file:
path: "{{ ansible_env.HOME }}/.kube"
state: directory
mode: '0755'
delegate_to: localhost
run_once: true
- name: Copy admin.conf to user's .kube directory on controller
when: not kube_init_stat.stat.exists
fetch:
src: /etc/kubernetes/admin.conf
dest: "{{ ansible_env.HOME }}/.kube/config"
flat: yes
- name: Install Calico CNI
become: no
command: kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/calico.yaml
delegate_to: localhost
run_once: true
when: not kube_init_stat.stat.exists
- name: Generate join command for control plane nodes
when: not kube_init_stat.stat.exists
command: kubeadm token create --print-join-command
register: control_plane_join_command_raw
- name: Generate certificate key for control plane join
when: not kube_init_stat.stat.exists
command: kubeadm init phase upload-certs --upload-certs
register: control_plane_cert_key
- name: Store control plane join command
set_fact:
control_plane_join_command: "{{ control_plane_join_command_raw.stdout }} --control-plane --certificate-key {{ control_plane_cert_key.stdout_lines[-1] }}"
when: not kube_init_stat.stat.exists
- name: Join other control plane nodes to the cluster
when:
- inventory_hostname != groups['control_plane'][0]
- not kube_init_stat.stat.exists
command: "{{ hostvars[groups['control_plane'][0]].control_plane_join_command }}"

View File

@@ -1,18 +0,0 @@
---
# Default variables for dns-manager role
# DNS Configuration
dns_management_enabled: true
use_hosts_file_fallback: false
dns_ttl: 360
dns_propagation_wait: 2
create_ptr_record: true
# Cluster Configuration (to be provided by calling role)
# cluster_endpoint: "fastpass.local.mk-labs.cloud"
# cluster_vip: "10.1.71.53"
# DNS Server Configuration (from group_vars)
# dns_server: "monorail"
# base_domain: "local.mk-labs.cloud"
# vault_technitium_api_key: "{{ vault_technitium_api_key }}"

View File

@@ -1,17 +0,0 @@
---
# role: dns-manager
# description: Reusable role for managing DNS entries across clusters using Technitium DNS
# author: mk-labs
# version: 1.0.0
- name: Create DNS entry using Technitium DNS task
ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks/add_technitium_dns_entry.yml"
- name: Wait for DNS propagation
ansible.builtin.pause:
seconds: "{{ dns_propagation_wait | default(10) }}"
when: dns_management_enabled | default(true)
- name: Look up A (IPv4) records for example.org
ansible.builtin.debug:
msg: "{{ query('community.dns.lookup', 'fastpass') }}"

View File

@@ -1,8 +0,0 @@
# ------------------------------------------------------------------------------
# FILE: roles/docker-host/handlers/main.yml
# ------------------------------------------------------------------------------
- name: restart docker
systemd:
name: docker
state: restarted

View File

@@ -1,88 +0,0 @@
# ------------------------------------------------------------------------------
# FILE: roles/docker-host/tasks/main.yml
# DESCRIPTION: Prepares an Ubuntu VM to run Docker Compose workloads.
# Installs Docker CE, configures the daemon, and ensures
# the ansible user (wed) can manage containers.
# ------------------------------------------------------------------------------
- name: Install prerequisite packages
apt:
name:
- ca-certificates
- curl
- gnupg
- lsb-release
state: present
update_cache: yes
- name: Create keyrings directory
file:
path: /etc/apt/keyrings
state: directory
mode: '0755'
- name: Add Docker GPG key
get_url:
url: https://download.docker.com/linux/ubuntu/gpg
dest: /etc/apt/keyrings/docker.asc
mode: '0644'
- name: Add Docker repository
apt_repository:
repo: >-
deb [arch={{ ansible_architecture | replace('x86_64', 'amd64') }}
signed-by=/etc/apt/keyrings/docker.asc]
https://download.docker.com/linux/ubuntu
{{ ansible_distribution_release }} stable
state: present
filename: docker
- name: Install Docker CE and Compose plugin
apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
update_cache: yes
- name: Ensure Docker service is started and enabled
systemd:
name: docker
state: started
enabled: yes
- name: Add ansible user to docker group
user:
name: "{{ ansible_user }}"
groups: docker
append: yes
- name: Configure Docker daemon
copy:
dest: /etc/docker/daemon.json
content: |
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"default-address-pools": [
{
"base": "172.17.0.0/16",
"size": 24
}
]
}
mode: '0644'
notify: restart docker
- name: Create Docker Compose project directory
file:
path: /opt/docker
state: directory
owner: "{{ ansible_user }}"
group: docker
mode: '0775'

View File

@@ -1,72 +0,0 @@
---
# Container runtime setup for Kubernetes
- name: Add containerd repository
become: true
ansible.builtin.yum_repository:
name: docker-ce
description: "Docker CE Repository"
baseurl: "https://download.docker.com/linux/fedora/{{ ansible_distribution_major_version }}/x86_64/stable"
gpgkey: "https://download.docker.com/linux/fedora/gpg"
gpgcheck: true
enabled: true
state: present
- name: Install containerd
become: true
ansible.builtin.dnf:
name: containerd
state: present
disable_excludes: docker-ce-stable
- name: Create containerd config directory
become: true
ansible.builtin.file:
path: /etc/containerd
state: directory
mode: '0755'
- name: Generate default containerd config
become: true
ansible.builtin.shell: |
containerd config default > /etc/containerd/config.toml
args:
creates: /etc/containerd/config.toml
- name: Configure containerd for Kubernetes
become: true
ansible.builtin.replace:
path: /etc/containerd/config.toml
regexp: 'SystemdCgroup = false'
replace: 'SystemdCgroup = true'
- name: Create containerd service override directory
become: true
ansible.builtin.file:
path: /etc/systemd/system/containerd.service.d
state: directory
mode: '0755'
- name: Configure containerd service override
become: true
ansible.builtin.copy:
dest: /etc/systemd/system/containerd.service.d/override.conf
content: |
[Service]
ExecStartPre=-/sbin/modprobe overlay
ExecStartPre=-/sbin/modprobe br_netfilter
mode: '0644'
- name: Restart and enable containerd service
become: true
ansible.builtin.systemd:
name: containerd
state: restarted
enabled: true
daemon_reload: true
- name: Verify containerd is running
become: true
ansible.builtin.systemd:
name: containerd
state: started

View File

@@ -1,16 +0,0 @@
---
# FastPass Kubernetes Cluster Preparation for Fedora
# This role prepares all nodes for Kubernetes installation
- name: Include preflight checks
ansible.builtin.include_tasks: preflight.yml
when: preflight_checks | default(true)
- name: Include system preparation
ansible.builtin.include_tasks: system-prep.yml
- name: Include container runtime setup
ansible.builtin.include_tasks: container-runtime.yml
- name: Include Kubernetes installation
ansible.builtin.include_tasks: kubernetes-install.yml

View File

@@ -1,40 +0,0 @@
---
# Preflight checks for Kubernetes installation
- name: Check if running on supported OS
ansible.builtin.assert:
that:
- ansible_os_family == "RedHat"
- ansible_distribution == "Fedora"
fail_msg: "This role only supports Fedora"
success_msg: "OS check passed"
- name: Check minimum memory requirement (2GB)
ansible.builtin.assert:
that: ansible_memtotal_mb >= 2048
fail_msg: "Minimum 2GB RAM required, found {{ ansible_memtotal_mb }}MB"
success_msg: "Memory check passed: {{ ansible_memtotal_mb }}MB"
- name: Check minimum CPU cores (2)
ansible.builtin.assert:
that: ansible_processor_cores >= 2
fail_msg: "Minimum 2 CPU cores required, found {{ ansible_processor_cores }}"
success_msg: "CPU check passed: {{ ansible_processor_cores }} cores"
- name: Check available disk space (10GB)
ansible.builtin.assert:
that: ansible_mounts | selectattr('mount', 'equalto', '/') | map(attribute='size_available') | first >= 10737418240
fail_msg: "Minimum 10GB free space required on root filesystem"
success_msg: "Disk space check passed"
- name: Check if system is 64-bit
ansible.builtin.assert:
that: ansible_architecture in ['x86_64', 'amd64']
fail_msg: "Only 64-bit architectures are supported"
success_msg: "Architecture check passed: {{ ansible_architecture }}"
- name: Check if running as root or with sudo
ansible.builtin.assert:
that: ansible_become | default(false)
fail_msg: "This role requires root privileges (become: true)"
success_msg: "Privilege check passed"

View File

@@ -1,83 +0,0 @@
---
# System preparation for Kubernetes
- name: Disable swap
ansible.builtin.command: swapoff -a
become: true
changed_when: false
when: disable_swap | default(true)
- name: Persist swap off by commenting out swap in fstab
become: true
ansible.builtin.replace:
path: /etc/fstab
regexp: '^(\s*)([^#\n]+\s+swap\s+.*)$'
replace: '#\2'
backup: true
when: disable_swap | default(true)
- name: Load required kernel modules
become: true
community.general.modprobe:
name: "{{ item }}"
state: present
loop:
- overlay
- br_netfilter
- name: Persist kernel modules on boot
become: true
ansible.builtin.copy:
dest: /etc/modules-load.d/k8s.conf
content: |
overlay
br_netfilter
mode: '0644'
- name: Configure required sysctl params for Kubernetes
become: true
ansible.builtin.copy:
dest: /etc/sysctl.d/k8s.conf
content: |
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
mode: '0644'
- name: Apply sysctl params without reboot
become: true
ansible.builtin.command: sysctl --system
changed_when: false
- name: Install required packages
become: true
ansible.builtin.dnf:
name:
- python3-libselinux
- dnf-plugins-core
- curl
- wget
- ca-certificates
state: present
- name: Set SELinux to permissive mode
become: true
ansible.posix.selinux:
policy: targeted
state: "{{ selinux_mode | default('permissive') }}"
- name: Stop and disable firewalld
become: true
ansible.builtin.systemd:
name: firewalld
state: stopped
enabled: false
when: not firewall_enabled | default(false)
- name: Update system packages
become: true
ansible.builtin.dnf:
name: "*"
state: latest
async: 300
poll: 10

View File

@@ -1,39 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: ansible/playbooks/roles/gitea/defaults/main.yml
# ------------------------------------------------------------------------------
# Directory layout on mad-tea-party
gitea_compose_dir: /opt/gitea
# Gitea application settings
gitea_domain: gitea.mk-labs.cloud
gitea_root_url: "https://{{ gitea_domain }}"
gitea_http_port: 3000
gitea_ssh_port: 2221
# Trusted proxy (lightning-lane)
gitea_trusted_proxies: 10.1.71.35
# Timezone
gitea_timezone: America/Chicago
# Database
gitea_db_name: gitea
gitea_db_user: gitea
# Traefik dynamic config destination on lightning-lane
traefik_dynamic_config_dir: /opt/traefik/dynamic
# DNS
gitea_cname_name: gitea
gitea_cname_domain: mk-labs.cloud
gitea_cname_target: mad-tea-party.local.mk-labs.cloud
# OIDC / Authentik SSO
gitea_oidc_enabled: true
gitea_oidc_client_id: "{{ vault_gitea_oidc_client_id }}"
gitea_oidc_client_secret: "{{ vault_gitea_oidc_client_secret }}"
gitea_oidc_discovery_url: "https://authentik.local.mk-labs.cloud/application/o/gitea/.well-known/openid-configuration"
gitea_oidc_admin_group: "mk-labs-admins"
gitea_admin_api_token: "{{ vault_gitea_admin_api_token }}"

View File

@@ -1,10 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: ansible/playbooks/roles/gitea/handlers/main.yml
# ------------------------------------------------------------------------------
- name: restart gitea
community.docker.docker_compose_v2:
project_src: "{{ gitea_compose_dir }}"
state: present
recreate: always

View File

@@ -1,27 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: ansible/playbooks/roles/gitea/tasks/configure_oidc.yml
# DESCRIPTION: Registers Authentik as an OAuth2 source in Gitea via the admin
# API. Idempotent — skips creation if source already exists.
# ------------------------------------------------------------------------------
- name: Check if authentik auth source already exists
ansible.builtin.command:
cmd: docker exec -u git gitea-gitea-1 gitea admin auth list
register: _auth_list
changed_when: false
- name: Create Authentik OAuth2 auth source
ansible.builtin.command:
cmd: >
docker exec -u git gitea-gitea-1 gitea admin auth add-oauth
--name authentik
--provider openidConnect
--key {{ gitea_oidc_client_id }}
--secret {{ gitea_oidc_client_secret }}
--auto-discover-url {{ gitea_oidc_discovery_url }}
--scopes "email profile"
--group-claim-name groups
--admin-group {{ gitea_oidc_admin_group }}
--skip-local-2fa false
when: "'authentik' not in _auth_list.stdout"

View File

@@ -1,67 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: ansible/playbooks/roles/gitea/tasks/main.yml
# ------------------------------------------------------------------------------
# ── Directories ────────────────────────────────────────────────────────────────
- name: Create Gitea compose directory
ansible.builtin.file:
path: "{{ gitea_compose_dir }}"
state: directory
owner: root
group: root
mode: "0755"
# ── Compose & env ──────────────────────────────────────────────────────────────
- name: Deploy Gitea compose file from boilerplate
ansible.builtin.copy:
src: "{{ playbook_dir }}/../../boilerplates/gitea/compose.yaml"
dest: "{{ gitea_compose_dir }}/compose.yaml"
owner: root
group: root
mode: "0644"
notify: restart gitea
- name: Deploy .env file with secrets from vault
ansible.builtin.template:
src: env.j2
dest: "{{ gitea_compose_dir }}/.env"
owner: root
group: root
mode: "0600"
notify: restart gitea
# ── Compose stack ──────────────────────────────────────────────────────────────
- name: Start Gitea stack
community.docker.docker_compose_v2:
project_src: "{{ gitea_compose_dir }}"
state: present
recreate: always
# ── Traefik route ──────────────────────────────────────────────────────────────
# - name: Deploy Traefik dynamic config for Gitea
# ansible.builtin.copy:
# src: "{{ playbook_dir }}/../../boilerplates/traefik/dynamic/gitea.yml"
# dest: "{{ traefik_dynamic_config_dir }}/gitea.yml"
# owner: root
# group: root
# mode: "0644"
# delegate_to: localhost
# ── DNS ────────────────────────────────────────────────────────────────────────
# - name: Create DNS CNAME for Gitea
# ansible.builtin.include_role:
# name: add_service_route
# vars:
# service_name: "{{ gitea_cname_name }}"
# service_domain: "{{ gitea_cname_domain }}"
# target_host: "{{ gitea_cname_target }}"
- name: Configure Authentik OIDC source
ansible.builtin.import_tasks: configure_oidc.yml
when: gitea_oidc_enabled | bool
tags: oidc

View File

@@ -1,12 +0,0 @@
# ------------------------------------------------------------------------------
# FILE: ansible/playbooks/roles/gitea/templates/env.j2
# DESCRIPTION: Environment file for Gitea Docker Compose stack.
# Populated from Ansible Vault at deploy time.
# ------------------------------------------------------------------------------
DATABASE_PASSWORD={{ gitea_db_password }}
# OIDC auto-registration (Gitea reads these at runtime)
GITEA__oauth2__ENABLE_AUTO_REGISTRATION=true
GITEA__oauth2__UPDATE_AVATAR=true
GITEA__oauth2__ACCOUNT_LINKING=login

View File

@@ -1,147 +0,0 @@
---
# ansible/roles/haproxy/defaults/main.yml
# HAProxy version and installation
haproxy_version: "3.2"
haproxy_install_method: "package" # package or source
# System configuration
haproxy_user: haproxy
haproxy_group: haproxy
haproxy_chroot_dir: /var/lib/haproxy
haproxy_config_dir: /etc/haproxy
haproxy_log_dir: /var/log/haproxy
# Global settings
haproxy_global_maxconn: 4096
haproxy_global_log_facility: local0
haproxy_global_log_level: info
haproxy_global_nbthread: "{{ ansible_processor_vcpus }}"
# Default timeouts (milliseconds)
haproxy_timeout_connect: 5000
haproxy_timeout_client: 50000
haproxy_timeout_server: 50000
# Stats interface
haproxy_stats_enable: true
haproxy_stats_port: 8404
haproxy_stats_uri: /stats
haproxy_stats_refresh: 30s
haproxy_stats_username: admin
haproxy_stats_password: "{{ vault_haproxy_stats_password | default('changeme') }}"
# SSL/TLS configuration
haproxy_ssl_cert_dir: "{{ haproxy_config_dir }}/certs"
haproxy_ssl_default_crt: "{{ haproxy_ssl_cert_dir }}/default.pem"
haproxy_ssl_ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384"
haproxy_ssl_options: "ssl-min-ver TLSv1.2 no-tls-tickets"
# Health check defaults
haproxy_health_check_interval: 2000
haproxy_health_check_timeout: 1000
haproxy_health_check_rise: 2
haproxy_health_check_fall: 3
# Service discovery integration
haproxy_netbox_integration: false # true
haproxy_netbox_url: "http://fire-station.local.mk-labs.cloud:8000"
haproxy_netbox_token: "{{ vault_netbox_token | default('') }}"
# Frontends configuration
haproxy_frontends: []
# - name: http_front
# bind: "*:80"
# mode: http
# redirect: "scheme https code 301"
#
# - name: https_front
# bind: "*:443 ssl crt {{ haproxy_ssl_cert_dir }}"
# mode: http
# options:
# - "http-server-close"
# - "forwardfor"
# acls:
# - "netbox hdr(host) -i fire-station.local.mk-labs.cloud"
# - "wordpress hdr(host) -i be-our-guest.local.mk-labs.cloud"
# use_backends:
# - "netbox_back if netbox"
# - "wordpress_back if wordpress"
# Backends configuration
haproxy_backends: []
# - name: netbox_back
# mode: http
# balance: roundrobin
# options:
# - "httpchk GET /"
# servers:
# - name: fire-station
# address: 10.1.71.102:8000
# check: true
# TCP services configuration
haproxy_tcp_services: []
# - name: postgres
# frontend_port: 5432
# backend_port: 5432
# balance: leastconn
# servers:
# - name: netbox-db
# address: 10.1.71.102
# Firewall configuration
haproxy_configure_firewall: true
haproxy_firewall_allowed_ports:
- 80/tcp
- 443/tcp
- 8404/tcp
# Monitoring and observability
haproxy_prometheus_exporter: false
haproxy_prometheus_port: 9101
# Logging
haproxy_rsyslog_config: true
haproxy_log_rotation: true
# Certificate management - Certbot with Cloudflare DNS
haproxy_certbot_enable: true
haproxy_certbot_challenge_method: "dns-cloudflare" # or "webroot" for HTTP-01
haproxy_certbot_email: "ryan.blundon@protonmail.com"
haproxy_certbot_staging: false # Set to true for testing with Let's Encrypt staging
# Cloudflare DNS configuration
haproxy_certbot_cloudflare_api_token: "{{ vault_cloudflare_api_token }}"
haproxy_certbot_cloudflare_credentials_file: "/etc/letsencrypt/cloudflare.ini"
# Certificate domains - DNS-01 supports wildcards
haproxy_certbot_domains:
- domain: "*.mk-labs.cloud"
include_base: true
- domain: "*.local.mk-labs.cloud"
include_base: true # Also include local.mk-labs.cloud
- domain: "lightning-lane.local.mk-labs.cloud"
include_base: false
# Alternative: specific domains without wildcard
# haproxy_certbot_domains:
# - domain: "lightning-lane.local.mk-labs.cloud"
# - domain: "fire-station.local.mk-labs.cloud"
# - domain: "be-our-guest.local.mk-labs.cloud"
# Certbot renewal settings
haproxy_certbot_renewal_cron_minute: "0"
haproxy_certbot_renewal_cron_hour: "3" # 3 AM daily
haproxy_certbot_renewal_cron_day: "*"
haproxy_certbot_post_hook: "{{ haproxy_config_dir }}/certbot-post-hook.sh"
# Certificate storage
haproxy_certbot_cert_dir: "/etc/letsencrypt/live"
haproxy_certbot_archive_dir: "/etc/letsencrypt/archive"
# DNS propagation wait time (seconds)
haproxy_certbot_dns_propagation_seconds: 60
# Fallback to self-signed if Certbot fails
haproxy_certbot_fallback_selfsigned: true

View File

@@ -1,26 +0,0 @@
---
# ansible/roles/haproxy/handlers/main.yml
- name: Reload haproxy
ansible.builtin.systemd:
name: haproxy
state: reloaded
listen: reload haproxy
- name: Restart haproxy
ansible.builtin.systemd:
name: haproxy
state: restarted
daemon_reload: true
listen: restart haproxy
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: true
listen: reload systemd
- name: Restart rsyslog
ansible.builtin.systemd:
name: rsyslog
state: restarted
listen: restart rsyslog

View File

@@ -1,202 +0,0 @@
---
# ansible/roles/haproxy/tasks/certbot-cloudflare.yml
- name: Install Certbot and Cloudflare DNS plugin
ansible.builtin.package:
name:
- certbot
- python3-certbot-dns-cloudflare
state: present
- name: Create Let's Encrypt configuration directory
ansible.builtin.file:
path: /etc/letsencrypt
state: directory
owner: root
group: root
mode: '0755'
- name: Create Cloudflare credentials file
ansible.builtin.template:
src: cloudflare-credentials.ini.j2
dest: "{{ haproxy_certbot_cloudflare_credentials_file }}"
owner: root
group: root
mode: '0600'
no_log: true
- name: Create Certbot post-renewal hook script
ansible.builtin.template:
src: certbot-post-hook.sh.j2
dest: "{{ haproxy_certbot_post_hook }}"
owner: root
group: root
mode: '0755'
- name: Create Certbot renewal hooks directory
ansible.builtin.file:
path: /etc/letsencrypt/renewal-hooks/deploy
state: directory
mode: '0755'
- name: Link post-hook to renewal hooks directory
ansible.builtin.file:
src: "{{ haproxy_certbot_post_hook }}"
dest: /etc/letsencrypt/renewal-hooks/deploy/haproxy-reload.sh
state: link
force: true
- name: Check for existing certificates
ansible.builtin.stat:
path: "{{ haproxy_certbot_cert_dir }}/{{ item.domain | regex_replace('\\*\\.', '') }}"
register: existing_certs
loop: "{{ haproxy_certbot_domains }}"
loop_control:
label: "{{ item.domain }}"
- name: Build certificate request commands
ansible.builtin.set_fact:
certbot_commands: "{{ certbot_commands | default([]) + [command_item] }}"
vars:
cert_name: "{{ item.item.domain | regex_replace('\\*\\.', '') }}"
domain_list: >-
{{ [item.item.domain] +
(item.item.include_base | default(false) | ternary(
[item.item.domain | regex_replace('\\*\\.', '')],
[]
))
}}
command_item:
domain: "{{ item.item.domain }}"
cert_name: "{{ cert_name }}"
domains: "{{ domain_list }}"
exists: "{{ item.stat.exists }}"
loop: "{{ existing_certs.results }}"
loop_control:
label: "{{ item.item.domain }}"
- name: Display certificate request plan
ansible.builtin.debug:
msg: |
Will request certificates for:
{% for cmd in certbot_commands %}
- {{ cmd.cert_name }} ({{ cmd.domains | join(', ') }}) - {{ 'EXISTS' if cmd.exists else 'NEW' }}
{% endfor %}
- name: Request certificates from Let's Encrypt via Cloudflare DNS
ansible.builtin.command: >
certbot certonly
--non-interactive
--agree-tos
--email {{ haproxy_certbot_email }}
--dns-cloudflare
--dns-cloudflare-credentials {{ haproxy_certbot_cloudflare_credentials_file }}
--dns-cloudflare-propagation-seconds {{ haproxy_certbot_dns_propagation_seconds }}
--cert-name {{ item.cert_name }}
{% for domain in item.domains %}
--domain {{ domain }}
{% endfor %}
{% if haproxy_certbot_staging %}
--staging
{% endif %}
--deploy-hook {{ haproxy_certbot_post_hook }}
loop: "{{ certbot_commands }}"
loop_control:
label: "{{ item.cert_name }}"
when: not item.exists
register: certbot_result
failed_when:
- certbot_result.rc != 0
- "'Certificate not yet due for renewal' not in certbot_result.stdout"
- "'too many certificates already issued' not in certbot_result.stderr"
- name: Display certificate request results
ansible.builtin.debug:
msg: "Certificate for {{ item.item.cert_name }}: {{ 'SUCCESS' if item.rc == 0 else 'SKIPPED/FAILED' }}"
loop: "{{ certbot_result.results }}"
loop_control:
label: "{{ item.item.cert_name }}"
when: certbot_result.results is defined
- name: Run post-hook to combine certificates for HAProxy
ansible.builtin.command: "{{ haproxy_certbot_post_hook }}"
changed_when: true
when: certbot_result.changed
- name: Setup automatic certificate renewal via cron
ansible.builtin.cron:
name: "Certbot certificate renewal"
minute: "{{ haproxy_certbot_renewal_cron_minute }}"
hour: "{{ haproxy_certbot_renewal_cron_hour }}"
day: "{{ haproxy_certbot_renewal_cron_day }}"
job: "/usr/bin/certbot renew --quiet --deploy-hook {{ haproxy_certbot_post_hook }} 2>&1 | logger -t certbot"
user: root
state: present
- name: Create systemd timer for certificate renewal (alternative to cron)
when: ansible_service_mgr == "systemd"
block:
- name: Create certbot renewal service
ansible.builtin.copy:
content: |
[Unit]
Description=Certbot Renewal
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --deploy-hook {{ haproxy_certbot_post_hook }}
StandardOutput=journal
StandardError=journal
dest: /etc/systemd/system/certbot-renewal.service
mode: '0644'
- name: Create certbot renewal timer
ansible.builtin.copy:
content: |
[Unit]
Description=Certbot Renewal Timer
[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
dest: /etc/systemd/system/certbot-renewal.timer
mode: '0644'
- name: Enable and start certbot renewal timer
ansible.builtin.systemd:
name: certbot-renewal.timer
enabled: true
state: started
daemon_reload: true
- name: Test certificate renewal (dry-run)
ansible.builtin.command: certbot renew --dry-run --dns-cloudflare --dns-cloudflare-credentials {{ haproxy_certbot_cloudflare_credentials_file }}
register: renewal_test
changed_when: false
failed_when: false
- name: Display renewal test results
ansible.builtin.debug:
msg: |
Certificate renewal dry-run: {{ 'SUCCESS ✓' if renewal_test.rc == 0 else 'FAILED ✗' }}
{% if renewal_test.rc == 0 %}
Your certificates will renew automatically.
{% else %}
Please check the output above for errors.
{% endif %}
- name: List all certificates
ansible.builtin.command: certbot certificates
register: cert_list
changed_when: false
- name: Display certificate information
ansible.builtin.debug:
msg: "{{ cert_list.stdout_lines }}"

View File

@@ -1,78 +0,0 @@
---
# ansible/roles/haproxy/tasks/certificates.yml
- name: Check if default certificate exists
ansible.builtin.stat:
path: "{{ haproxy_ssl_default_crt }}"
register: default_cert
- name: Generate self-signed certificate for development
when: not default_cert.stat.exists
block:
- name: Generate private key
openssl_privatekey:
path: "{{ haproxy_ssl_cert_dir }}/default.key"
size: 2048
mode: '0600'
owner: "{{ haproxy_user }}"
group: "{{ haproxy_group }}"
- name: Generate certificate signing request
openssl_csr:
path: "{{ haproxy_ssl_cert_dir }}/default.csr"
privatekey_path: "{{ haproxy_ssl_cert_dir }}/default.key"
common_name: "lightning-lane.local.mk-labs.cloud"
subject_alt_name:
- "DNS:lightning-lane.local.mk-labs.cloud"
- "DNS:*.local.mk-labs.cloud"
owner: "{{ haproxy_user }}"
group: "{{ haproxy_group }}"
- name: Generate self-signed certificate
openssl_certificate:
path: "{{ haproxy_ssl_cert_dir }}/default.crt"
privatekey_path: "{{ haproxy_ssl_cert_dir }}/default.key"
csr_path: "{{ haproxy_ssl_cert_dir }}/default.csr"
provider: selfsigned
selfsigned_not_after: "+3650d"
owner: "{{ haproxy_user }}"
group: "{{ haproxy_group }}"
- name: Combine certificate and key for HAProxy
ansible.builtin.shell: |
cat {{ haproxy_ssl_cert_dir }}/default.crt \
{{ haproxy_ssl_cert_dir }}/default.key \
> {{ haproxy_ssl_default_crt }}
args:
creates: "{{ haproxy_ssl_default_crt }}"
- name: Set permissions on combined certificate
ansible.builtin.file:
path: "{{ haproxy_ssl_default_crt }}"
owner: "{{ haproxy_user }}"
group: "{{ haproxy_group }}"
mode: '0600'
- name: Create certificate directory structure
ansible.builtin.file:
path: "{{ haproxy_ssl_cert_dir }}/{{ item }}"
state: directory
owner: "{{ haproxy_user }}"
group: "{{ haproxy_group }}"
mode: '0755'
loop:
- live
- archive
- name: Deploy additional certificates
when: haproxy_ssl_certificates is defined
block:
- name: Copy certificate files
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ haproxy_ssl_cert_dir }}/live/{{ item.name }}.pem"
owner: "{{ haproxy_user }}"
group: "{{ haproxy_group }}"
mode: '0600'
loop: "{{ haproxy_ssl_certificates }}"
notify: Reload haproxy

View File

@@ -1,94 +0,0 @@
---
# ansible/roles/haproxy/tasks/configure.yml
- name: Fetch services from NetBox
when: haproxy_netbox_integration
block:
- name: Query NetBox for service configurations
ansible.builtin.uri:
url: "{{ haproxy_netbox_url }}/api/ipam/services/"
method: GET
headers:
Authorization: "Token {{ haproxy_netbox_token }}"
Accept: "application/json"
return_content: true
register: netbox_services
changed_when: false
- name: Parse NetBox services
ansible.builtin.set_fact:
haproxy_frontends: "{{ haproxy_frontends | default([]) + netbox_parsed_frontends }}"
haproxy_backends: "{{ haproxy_backends | default([]) + netbox_parsed_backends }}"
vars:
netbox_parsed_frontends: "{{ netbox_services.json.results | map(attribute='custom_fields') | list }}"
netbox_parsed_backends: "{{ netbox_services.json.results | map(attribute='custom_fields') | list }}"
when: netbox_services.json.results is defined
- name: Generate HAProxy main configuration
ansible.builtin.template:
src: haproxy.cfg.j2
dest: "{{ haproxy_config_dir }}/haproxy.cfg"
owner: root
group: root
mode: '0644'
backup: true
validate: 'haproxy -c -f %s'
notify: reload haproxy
- name: Configure rsyslog for HAProxy
when: haproxy_rsyslog_config
block:
- name: Create rsyslog configuration for HAProxy
ansible.builtin.copy:
content: |
# HAProxy logging configuration
$ModLoad imudp
$UDPServerRun 514
# Create separate log files for HAProxy
local0.* {{ haproxy_log_dir }}/haproxy.log
local0.info {{ haproxy_log_dir }}/haproxy-info.log
local0.notice {{ haproxy_log_dir }}/haproxy-notice.log
local0.err {{ haproxy_log_dir }}/haproxy-error.log
# Stop processing HAProxy logs
& stop
dest: /etc/rsyslog.d/49-haproxy.conf
mode: '0644'
notify: restart rsyslog
- name: Configure logrotate for HAProxy
when: haproxy_log_rotation
ansible.builtin.template:
src: logrotate.j2
dest: /etc/logrotate.d/haproxy
mode: '0644'
- name: Create HAProxy systemd override directory
ansible.builtin.file:
path: /etc/systemd/system/haproxy.service.d
state: directory
mode: '0755'
- name: Configure HAProxy systemd service overrides
ansible.builtin.copy:
content: |
[Service]
# Increase file descriptor limits
LimitNOFILE=65536
# Runtime directory
RuntimeDirectory=haproxy
RuntimeDirectoryMode=0755
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths={{ haproxy_log_dir }}
dest: /etc/systemd/system/haproxy.service.d/override.conf
mode: '0644'
notify:
- Reload systemd
- Restart haproxy

View File

@@ -1,128 +0,0 @@
---
# ansible/roles/haproxy/tasks/install.yml
- name: Install HAProxy repository (Ubuntu)
when: ansible_os_family == "Debian"
block:
- name: Add HAProxy PPA
ansible.builtin.apt_repository:
repo: "ppa:vbernat/haproxy-{{ haproxy_version }}"
state: present
update_cache: true
- name: Install HAProxy
ansible.builtin.apt:
name: haproxy
state: present
update_cache: true
- name: Install HAProxy (Fedora/RHEL)
when: ansible_os_family == "RedHat"
block:
- name: Install HAProxy
ansible.builtin.dnf:
name: haproxy
state: present
- name: Install additional packages
ansible.builtin.package:
name:
- rsyslog
- logrotate
- socat # For HAProxy socket communication
state: present
- name: Create HAProxy system user
ansible.builtin.user:
name: "{{ haproxy_user }}"
group: "{{ haproxy_group }}"
system: true
shell: /usr/sbin/nologin
home: "{{ haproxy_chroot_dir }}"
create_home: true
- name: Create required directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ haproxy_user }}"
group: "{{ haproxy_group }}"
mode: '0755'
loop:
- "{{ haproxy_config_dir }}"
- "{{ haproxy_ssl_cert_dir }}"
- "{{ haproxy_log_dir }}"
- "{{ haproxy_chroot_dir }}"
- /var/lib/haproxy/dev
- name: Create HAProxy socket directory
ansible.builtin.file:
path: /run/haproxy
state: directory
owner: "{{ haproxy_user }}"
group: "{{ haproxy_group }}"
mode: '0755'
- name: Configure firewall rules
when: haproxy_configure_firewall
block:
- name: Open HAProxy ports (UFW)
when: ansible_os_family == "Debian"
community.general.ufw:
rule: allow
port: "{{ item.split('/')[0] }}"
proto: "{{ item.split('/')[1] }}"
loop: "{{ haproxy_firewall_allowed_ports }}"
- name: Open HAProxy ports (firewalld)
when: ansible_os_family == "RedHat"
ansible.posix.firewalld:
port: "{{ item }}"
permanent: true
state: enabled
immediate: true
loop: "{{ haproxy_firewall_allowed_ports }}"
- name: Install Prometheus HAProxy exporter
when: haproxy_prometheus_exporter
block:
- name: Download HAProxy exporter
ansible.builtin.get_url:
url: "https://github.com/prometheus/haproxy_exporter/releases/download/v0.15.0/haproxy_exporter-0.15.0.linux-amd64.tar.gz"
dest: /tmp/haproxy_exporter.tar.gz
mode: '0644'
- name: Extract HAProxy exporter
ansible.builtin.unarchive:
src: /tmp/haproxy_exporter.tar.gz
dest: /usr/local/bin/
remote_src: true
extra_opts:
- --strip-components=1
- --wildcards
- '*/haproxy_exporter'
- name: Create HAProxy exporter systemd service
ansible.builtin.copy:
content: |
[Unit]
Description=HAProxy Exporter
After=network.target
[Service]
Type=simple
User={{ haproxy_user }}
ExecStart=/usr/local/bin/haproxy_exporter --haproxy.scrape-uri=unix:/run/haproxy/admin.sock
Restart=on-failure
[Install]
WantedBy=multi-user.target
dest: /etc/systemd/system/haproxy-exporter.service
mode: '0644'
- name: Enable and start HAProxy exporter
ansible.builtin.systemd:
name: haproxy-exporter
state: started
enabled: true
daemon_reload: true

View File

@@ -1,40 +0,0 @@
---
# ansible/roles/haproxy/tasks/main.yml
# - name: Include OS-specific variables
# include_vars: "{{ ansible_os_family }}.yml"
# tags: [haproxy, always]
- name: Include installation tasks
ansible.builtin.include_tasks: install.yml
tags: [haproxy, install]
- name: Include Certbot with Cloudflare DNS tasks
ansible.builtin.include_tasks: certbot-cloudflare.yml
when:
- haproxy_certbot_enable
- haproxy_certbot_challenge_method == "dns-cloudflare"
tags: [haproxy, certificates, certbot, cloudflare]
- name: Include fallback self-signed certificate tasks
ansible.builtin.include_tasks: certificates.yml
when:
- not haproxy_certbot_enable
- haproxy_certbot_fallback_selfsigned | default(false)
tags: [haproxy, certificates, selfsigned]
- name: Include configuration tasks
ansible.builtin.include_tasks: configure.yml
tags: [haproxy, configure]
- name: Include validation tasks
ansible.builtin.include_tasks: validate.yml
tags: [haproxy, validate]
- name: Ensure HAProxy is started and enabled
ansible.builtin.systemd:
name: haproxy
state: started
enabled: true
daemon_reload: true
tags: [haproxy, service]

View File

@@ -1,47 +0,0 @@
---
# ansible/roles/haproxy/tasks/validate.yml
- name: Validate HAProxy configuration syntax
command: haproxy -c -f {{ haproxy_config_dir }}/haproxy.cfg
register: haproxy_validation
changed_when: false
failed_when: haproxy_validation.rc != 0
- name: Display validation results
debug:
msg: "{{ haproxy_validation.stdout_lines }}"
when: haproxy_validation.stdout_lines is defined
- name: Check HAProxy service status
systemd:
name: haproxy
state: started
register: haproxy_status
changed_when: false
- name: Verify HAProxy is listening on configured ports
wait_for:
host: "{{ ansible_default_ipv4.address }}"
port: "{{ item }}"
timeout: 10
loop:
- 80
- 443
- "{{ haproxy_stats_port }}"
when: haproxy_status.status.ActiveState == "active"
- name: Test stats interface accessibility
uri:
url: "http://localhost:{{ haproxy_stats_port }}{{ haproxy_stats_uri }}"
user: "{{ haproxy_stats_username }}"
password: "{{ haproxy_stats_password }}"
force_basic_auth: true
status_code: 200
register: stats_test
changed_when: false
failed_when: false
- name: Display stats interface status
debug:
msg: "Stats interface is {{ 'accessible' if stats_test.status == 200 else 'not accessible' }}"

View File

@@ -1,107 +0,0 @@
#!/bin/bash
# {{ ansible_managed }}
# Certbot post-renewal hook for HAProxy
# Combines certificate and private key for HAProxy consumption
set -euo pipefail
# Configuration
LETSENCRYPT_LIVE="/etc/letsencrypt/live"
HAPROXY_CERT_DIR="{{ haproxy_ssl_cert_dir }}/live"
HAPROXY_USER="{{ haproxy_user }}"
HAPROXY_GROUP="{{ haproxy_group }}"
LOG_FILE="{{ haproxy_log_dir }}/certbot-hook.log"
# Ensure HAProxy cert directory exists
mkdir -p "$HAPROXY_CERT_DIR"
# Logging function
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
log "Starting certificate deployment for HAProxy"
# Track if any certificates were updated
CERTS_UPDATED=false
# Process each certificate in Let's Encrypt live directory
for cert_dir in "$LETSENCRYPT_LIVE"/*; do
if [ ! -d "$cert_dir" ] || [ "$(basename "$cert_dir")" = "README" ]; then
continue
fi
CERT_NAME=$(basename "$cert_dir")
# Check if certificate files exist
if [ ! -f "$cert_dir/fullchain.pem" ] || [ ! -f "$cert_dir/privkey.pem" ]; then
log "WARNING: Missing certificate files for $CERT_NAME, skipping"
continue
fi
log "Processing certificate: $CERT_NAME"
# Create combined PEM file (fullchain + privkey)
# HAProxy requires the certificate chain and private key in a single file
COMBINED_PEM="$HAPROXY_CERT_DIR/${CERT_NAME}.pem"
# Combine certificates
cat "$cert_dir/fullchain.pem" "$cert_dir/privkey.pem" > "$COMBINED_PEM.tmp"
# Check if certificate actually changed
if [ -f "$COMBINED_PEM" ] && cmp -s "$COMBINED_PEM" "$COMBINED_PEM.tmp"; then
log "Certificate $CERT_NAME unchanged, skipping"
rm "$COMBINED_PEM.tmp"
continue
fi
# Move new certificate into place
mv "$COMBINED_PEM.tmp" "$COMBINED_PEM"
# Set proper permissions
chown "$HAPROXY_USER:$HAPROXY_GROUP" "$COMBINED_PEM"
chmod 600 "$COMBINED_PEM"
log "Created/Updated: $COMBINED_PEM"
CERTS_UPDATED=true
done
# Create/update default certificate symlink
# Use the first wildcard cert or the first available cert as default
DEFAULT_CERT="{{ haproxy_ssl_default_crt }}"
WILDCARD_CERT="$HAPROXY_CERT_DIR/local.mk-labs.cloud.pem"
if [ -f "$WILDCARD_CERT" ]; then
ln -sf "$WILDCARD_CERT" "$DEFAULT_CERT"
log "Updated default certificate symlink to wildcard cert"
elif [ -n "$(ls -A "$HAPROXY_CERT_DIR" 2>/dev/null)" ]; then
FIRST_CERT=$(ls "$HAPROXY_CERT_DIR"/*.pem 2>/dev/null | head -n1)
ln -sf "$FIRST_CERT" "$DEFAULT_CERT"
log "Updated default certificate symlink to $FIRST_CERT"
fi
# Only reload HAProxy if certificates were actually updated
if [ "$CERTS_UPDATED" = true ]; then
log "Certificates updated, validating HAProxy configuration"
# Validate HAProxy configuration
if haproxy -c -f {{ haproxy_config_dir }}/haproxy.cfg >/dev/null 2>&1; then
log "HAProxy configuration valid, reloading service"
if systemctl reload haproxy; then
log "HAProxy reloaded successfully"
else
log "ERROR: Failed to reload HAProxy"
exit 1
fi
else
log "ERROR: HAProxy configuration validation failed"
haproxy -c -f {{ haproxy_config_dir }}/haproxy.cfg 2>&1 | tee -a "$LOG_FILE"
exit 1
fi
else
log "No certificates were updated, skipping HAProxy reload"
fi
log "Certificate deployment complete"
exit 0

View File

@@ -1,12 +0,0 @@
# {{ ansible_managed }}
# Cloudflare API credentials for Certbot DNS-01 challenge
# This file contains sensitive information and should be protected
# Cloudflare API Token (RECOMMENDED)
# Create at: https://dash.cloudflare.com/profile/api-tokens
# Required permissions: Zone:DNS:Edit for specific zone
dns_cloudflare_api_token = {{ haproxy_certbot_cloudflare_api_token }}
# Alternative: Global API Key (NOT RECOMMENDED - too permissive)
# dns_cloudflare_email = your-email@example.com
# dns_cloudflare_api_key = your-global-api-key

View File

@@ -1,188 +0,0 @@
{# ansible/roles/haproxy/templates/haproxy.cfg.j2 #}
#---------------------------------------------------------------------
# HAProxy Configuration for {{ inventory_hostname }}
# Generated by Ansible - DO NOT EDIT MANUALLY
# Magic Kingdom Labs - lightning-lane load balancer
#---------------------------------------------------------------------
#---------------------------------------------------------------------
# Global settings
#---------------------------------------------------------------------
global
log 127.0.0.1 {{ haproxy_global_log_facility }} {{ haproxy_global_log_level }}
chroot {{ haproxy_chroot_dir }}
pidfile /var/run/haproxy.pid
maxconn {{ haproxy_global_maxconn }}
user {{ haproxy_user }}
group {{ haproxy_group }}
daemon
nbthread {{ haproxy_global_nbthread }}
# Runtime socket for management
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
# SSL/TLS configuration
ssl-default-bind-ciphers {{ haproxy_ssl_ciphers }}
ssl-default-bind-options {{ haproxy_ssl_options }}
ssl-default-server-ciphers {{ haproxy_ssl_ciphers }}
ssl-default-server-options {{ haproxy_ssl_options }}
# Performance tuning
tune.ssl.default-dh-param 2048
tune.ssl.cachesize 100000
tune.ssl.lifetime 600
#---------------------------------------------------------------------
# Common defaults
#---------------------------------------------------------------------
defaults
mode http
log global
option httplog
option dontlognull
option http-server-close
option redispatch
retries 3
timeout connect {{ haproxy_timeout_connect }}
timeout client {{ haproxy_timeout_client }}
timeout server {{ haproxy_timeout_server }}
timeout http-request 10s
timeout http-keep-alive 10s
timeout check 10s
maxconn 3000
# Error pages
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
#---------------------------------------------------------------------
# Stats interface
#---------------------------------------------------------------------
{% if haproxy_stats_enable %}
listen stats
bind *:{{ haproxy_stats_port }}
mode http
stats enable
stats uri {{ haproxy_stats_uri }}
stats refresh {{ haproxy_stats_refresh }}
stats realm Magic\ Kingdom\ Labs\ -\ HAProxy\ Statistics
stats auth {{ haproxy_stats_username }}:{{ haproxy_stats_password }}
stats admin if TRUE
{% endif %}
#---------------------------------------------------------------------
# HTTP Redirect Frontend (HTTP -> HTTPS)
#---------------------------------------------------------------------
frontend http_front
bind *:80
mode http
# Redirect all HTTP traffic to HTTPS
redirect scheme https code 301
#---------------------------------------------------------------------
# HTTPS Frontend with SNI
#---------------------------------------------------------------------
frontend https_front
bind *:443 ssl crt {{ haproxy_ssl_cert_dir }}/live crt {{ haproxy_ssl_default_crt }} alpn h2,http/1.1
mode http
# Security headers
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
http-response set-header X-Frame-Options "SAMEORIGIN"
http-response set-header X-Content-Type-Options "nosniff"
http-response set-header X-XSS-Protection "1; mode=block"
# Logging
option httplog
option forwardfor
{% if haproxy_frontends is defined and haproxy_frontends | length > 0 %}
{% for frontend in haproxy_frontends %}
{% if frontend.acls is defined %}
# ACLs for {{ frontend.name }}
{% for acl in frontend.acls %}
acl {{ acl }}
{% endfor %}
{% endif %}
{% endfor %}
{% for frontend in haproxy_frontends %}
{% if frontend.use_backends is defined %}
# Backend routing for {{ frontend.name }}
{% for use_backend in frontend.use_backends %}
use_backend {{ use_backend }}
{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
# Default backend
default_backend no_match_back
#---------------------------------------------------------------------
# Backends
#---------------------------------------------------------------------
{% for backend in haproxy_backends %}
backend {{ backend.name }}
mode {{ backend.mode | default('http') }}
balance {{ backend.balance | default('roundrobin') }}
{% if backend.options is defined %}
{% for option in backend.options %}
option {{ option }}
{% endfor %}
{% endif %}
{% if backend.http_check is defined %}
option httpchk {{ backend.http_check }}
{% endif %}
{% if backend.servers is defined %}
{% for server in backend.servers %}
server {{ server.name }} {{ server.address }}{% if server.check | default(false) %} check{% endif %}{% if server.backup | default(false) %} backup{% endif %}{% if server.weight is defined %} weight {{ server.weight }}{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
# Default backend for unmatched requests
backend no_match_back
mode http
http-request deny deny_status 404
#---------------------------------------------------------------------
# TCP Services
#---------------------------------------------------------------------
{% for tcp_service in haproxy_tcp_services %}
# {{ tcp_service.name | upper }} Service (TCP)
frontend {{ tcp_service.name }}_front
bind *:{{ tcp_service.frontend_port }}
mode tcp
option tcplog
{% if tcp_service.timeout_client is defined %}
timeout client {{ tcp_service.timeout_client }}
{% endif %}
default_backend {{ tcp_service.name }}_back
backend {{ tcp_service.name }}_back
mode tcp
balance {{ tcp_service.balance | default('roundrobin') }}
{% if tcp_service.timeout_server is defined %}
timeout server {{ tcp_service.timeout_server }}
{% endif %}
{% if tcp_service.timeout_connect is defined %}
timeout connect {{ tcp_service.timeout_connect }}
{% endif %}
{% if tcp_service.servers is defined %}
{% for server in tcp_service.servers %}
server {{ server.name }} {{ server.address }}:{{ tcp_service.backend_port }}{% if server.check | default(true) %} check{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

View File

@@ -1,14 +0,0 @@
{# ansible/roles/haproxy/templates/logrotate.j2 #}
{{ haproxy_log_dir }}/*.log {
daily
rotate 14
missingok
notifempty
compress
delaycompress
sharedscripts
postrotate
/bin/kill -HUP `cat /var/run/rsyslog.pid 2> /dev/null` 2> /dev/null || true
/bin/kill -HUP `cat /var/run/haproxy.pid 2> /dev/null` 2> /dev/null || true
endscript
}

View File

@@ -1,15 +0,0 @@
---
# Hermes service user
hermes_user: hermes
hermes_group: hermes
hermes_home: /home/hermes
# Install flags
# Set to true if you don't need browser automation (skips Playwright/Chromium)
hermes_skip_browser: false
# systemd service name
hermes_service_name: hermes
# Path where hermes binary will be accessible system-wide
hermes_bin_symlink: /usr/local/bin/hermes

View File

@@ -1,9 +0,0 @@
---
- name: reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: restart hermes
ansible.builtin.systemd:
name: "{{ hermes_service_name }}"
state: restarted

View File

@@ -1,156 +0,0 @@
---
# ---------------------------------------------------------------------------
# 1. System prerequisites (run as root via become)
# ---------------------------------------------------------------------------
- name: Install system packages required by Hermes installer
ansible.builtin.apt:
name:
- git
- curl
- ffmpeg
- ripgrep
state: present
update_cache: true
become: true
- name: Install Node.js 22 (required for browser automation and WhatsApp bridge)
block:
- name: Download NodeSource setup script
ansible.builtin.get_url:
url: https://deb.nodesource.com/setup_22.x
dest: /tmp/nodesource_setup.sh
mode: "0755"
- name: Run NodeSource setup script
ansible.builtin.command: bash /tmp/nodesource_setup.sh
args:
creates: /etc/apt/sources.list.d/nodesource.list
- name: Install nodejs
ansible.builtin.apt:
name: nodejs
state: present
update_cache: true
become: true
# ---------------------------------------------------------------------------
# 2. Install Playwright system deps for Chromium (root-only step)
# Per docs: this is the one thing that genuinely needs root.
# Skipped entirely if hermes_skip_browser is true.
# ---------------------------------------------------------------------------
- name: Install Playwright Chromium system dependencies
ansible.builtin.command: npx --yes playwright install-deps chromium
become: true
when: not hermes_skip_browser
changed_when: true
environment:
DEBIAN_FRONTEND: noninteractive
# ---------------------------------------------------------------------------
# 3. Create dedicated hermes service user
# ---------------------------------------------------------------------------
- name: Create hermes group
ansible.builtin.group:
name: "{{ hermes_group }}"
state: present
system: true
become: true
- name: Create hermes user
ansible.builtin.user:
name: "{{ hermes_user }}"
group: "{{ hermes_group }}"
home: "{{ hermes_home }}"
shell: /bin/bash
system: true
create_home: true
comment: "Hermes Agent service account"
become: true
- name: Grant hermes user passwordless sudo
ansible.builtin.copy:
content: "hermes ALL=(ALL) NOPASSWD:ALL\n"
dest: /etc/sudoers.d/hermes
owner: root
group: root
mode: "0440"
validate: /usr/sbin/visudo -cf %s
become: true
- name: Ensure hermes home directory has correct permissions
ansible.builtin.file:
path: "{{ hermes_home }}"
owner: "{{ hermes_user }}"
group: "{{ hermes_group }}"
mode: "0750"
state: directory
become: true
# ---------------------------------------------------------------------------
# 4. Run the Hermes installer as the hermes user
# ---------------------------------------------------------------------------
- name: Check if hermes is already installed
ansible.builtin.stat:
path: "{{ hermes_home }}/.hermes/hermes-agent/venv/bin/hermes"
register: hermes_binary
- name: Run Hermes installer as hermes user
ansible.builtin.shell: |
curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh \
| bash -s -- --skip-setup{% if hermes_skip_browser %} --skip-browser{% endif %}
args:
executable: /bin/bash
become: true
become_user: "{{ hermes_user }}"
environment:
HOME: "{{ hermes_home }}"
PATH: "{{ hermes_home }}/.local/bin:/usr/local/bin:/usr/bin:/bin"
when: not hermes_binary.stat.exists
changed_when: true
# ---------------------------------------------------------------------------
# 5. Symlink hermes binary into system PATH
# Per docs: service accounts often lack ~/.local/bin in PATH; symlink
# into /usr/local/bin so hermes is always accessible.
# ---------------------------------------------------------------------------
- name: Symlink hermes binary to system PATH
ansible.builtin.file:
src: "{{ hermes_home }}/.hermes/hermes-agent/venv/bin/hermes"
dest: "{{ hermes_bin_symlink }}"
state: link
force: true
become: true
# ---------------------------------------------------------------------------
# 6. Install systemd service unit
# NOTE: The service starts in 'gateway' mode for persistent operation.
# You must run 'sudo -u hermes hermes setup' interactively on first boot
# to configure your LLM provider and any messaging gateways before
# enabling the service.
# ---------------------------------------------------------------------------
- name: Deploy hermes systemd service unit
ansible.builtin.template:
src: hermes.service.j2
dest: /etc/systemd/system/{{ hermes_service_name }}.service
owner: root
group: root
mode: "0644"
become: true
notify:
- reload systemd
- name: Flush handlers to reload systemd now
ansible.builtin.meta: flush_handlers
- name: Enable hermes service (but do not start — config required first)
ansible.builtin.systemd:
name: "{{ hermes_service_name }}"
enabled: true
state: stopped
become: true

View File

@@ -1,23 +0,0 @@
[Unit]
Description=Hermes Agent (Nous Research)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{ hermes_user }}
Group={{ hermes_group }}
WorkingDirectory={{ hermes_home }}
Environment="HOME={{ hermes_home }}"
Environment="PATH={{ hermes_home }}/.local/bin:/usr/local/bin:/usr/bin:/bin"
Environment="HERMES_HOME={{ hermes_home }}/.hermes"
ExecStart={{ hermes_home }}/.hermes/hermes-agent/venv/bin/hermes gateway
TimeoutStopSec=200
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=hermes
[Install]
WantedBy=multi-user.target

View File

@@ -1,95 +0,0 @@
---
# Role to install and configure a n8n server
- name: 1. Pre-install system updates
become: true
ansible.builtin.apt:
name: '*'
state: present
update_cache: true
- name: 2. Install required packages
become: true
ansible.builtin.package:
name: "{{ item }}"
state: present
loop:
- curl
- wget
- gnupg
- lsb-release
- ca-certificates
- software-properties-common
- name: 3. Enable UFW
become: true
community.general.ufw:
state: enabled
- name: 4. Open firewall ports for n8n
become: true
community.general.ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop:
- 22
- 5678
- name: 5. Download node installer
# become: true
ansible.builtin.get_url:
url: 'https://deb.nodesource.com/setup_lts.x'
dest: '/tmp/setup_lts.x'
mode: '0755'
- name: 6. Run node installer
become: true
ansible.builtin.shell:
cmd: '/tmp/setup_lts.x'
- name: 7. Install Node.js and npm
become: true
ansible.builtin.package:
name: nodejs
state: present
- name: 8. Install npm@latest
become: true
ansible.builtin.shell:
cmd: 'sudo npm install -g npm@latest'
- name: 9. Install "n8n" node.js package globally.
become: true
community.general.npm:
name: n8n
global: true
- name: 10. Copy n8n.service to /etc/systemd/system
become: true
ansible.builtin.template:
dest: /etc/systemd/system/n8n.service
owner: root
group: root
mode: '0644'
src: n8n.service.j2
- name: 11. Create n8n user
become: true
ansible.builtin.user:
name: n8n
state: present
shell: /bin/bash
home: /home/n8n
createhome: true
- name: 12. Reload systemd to re-read configs
ansible.builtin.systemd_service:
daemon_reload: true
- name: 13. Start n8n service
become: true
ansible.builtin.systemd:
name: n8n.service
state: restarted
enabled: true

View File

@@ -1,22 +0,0 @@
[Unit]
Description=n8n - Workflow Automation Tool
After=network.target
[Service]
Type=simple
User=n8n
Group=n8n
ExecStart=/usr/local/bin/n8n
WorkingDirectory=/home/n8n
Environment=NODE_ENV=production
Environment=N8N_BASIC_AUTH_ACTIVE=true
Environment=N8N_BASIC_AUTH_USER=admin
#Environment=N8N_BASIC_AUTH_PASSWORD=your_secure_password
Environment=N8N_HOST=0.0.0.0
Environment=N8N_PORT=5678
Environment=N8N_PROTOCOL=http
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

View File

@@ -1,34 +0,0 @@
---
# Role to disable swap
- name: 1. Check if swap is enabled
ansible.builtin.shell: swapon --show=NAME --noheadings
register: swap_status
changed_when: false
- name: 2. Disable swap
become: true
when: swap_status.stdout != ''
block:
- name: 2.1 Disable swap
ansible.builtin.shell: swapoff -a
- name: 2.2 Persist swap off by commenting out swap in fstab
ansible.builtin.replace:
path: /etc/fstab
regexp: '^([^#].*?\sswap\s+sw\s+.*)$'
replace: '# \1'
backup: true
- name: 2.3 Remove swap file
become: true
ansible.builtin.file:
path: "{{ swap_status.stdout }}"
state: absent
- name: 2.4 Remove zram-generator-defaults
become: true
when: ansible_os_family == "RedHat"
ansible.builtin.file:
path: /etc/systemd/zram-generator.conf
state: absent

View File

@@ -1,46 +0,0 @@
---
# Role to install and configure an Chrony NTP server
- name: 1. Install Chrony NTP for time synchronization
become: true
ansible.builtin.dnf:
name: chrony
state: present
- name: 2.1 Remove DHCP NTP servers from config
become: true
ansible.builtin.lineinfile:
path: /etc/chrony.conf
line: "server unifi.int.mk-labs.cloud iburst"
state: absent
- name: 2.2 Ensure specific NTP servers are present
become: true
ansible.builtin.lineinfile:
path: /etc/chrony.conf
line: "server {{ item }} iburst"
state: present
loop: "{{ ntp_servers }}"
- name: 3. Ensure network allowed are present
become: true
ansible.builtin.lineinfile:
path: /etc/chrony.conf
line: "allow {{ item }}"
state: present
loop: "{{ allowed_networks }}"
- name: 4.Restart ntpd service
become: true
ansible.builtin.systemd:
name: chronyd
state: restarted
enabled: true
- name: 5. Allow NTP traffic
become: true
ansible.posix.firewalld:
service: ntp
permanent: true
immediate: true
state: enabled

View File

@@ -1,11 +0,0 @@
---
prometheus_version: v2.40.1
grafana_version: "9.2.5"
alertmanager_version: v0.24.0
#alertmanager_smtp_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
64306663363562356132323065396635636630373031303739323666373262663961393132316333
6135653763363566303331313639633030623530646239310a353236343035643132646230333466
36336439376131333630346563323833313164353265313264643232373465633561663331396133
3163303166373166390a396131303239356139653063616437363933333130393563646338663933
3966

View File

@@ -1,8 +0,0 @@
apiVersion: 1
providers:
- name: Pre-loaded local dashboards
type: file
options:
foldersFromFilesStructure: true
path: /var/lib/grafana/dashboards

View File

@@ -1,814 +0,0 @@
{
"__inputs": [
{
"name": "DS_PROMETHEUS",
"label": "prometheus",
"description": "Prometheus as the datasource is obligatory",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}
],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "7.4.5"
},
{
"type": "panel",
"id": "graph",
"name": "Graph",
"version": ""
},
{
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "1.0.0"
},
{
"type": "panel",
"id": "table",
"name": "Table",
"version": ""
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": 14282,
"graphTooltip": 0,
"id": null,
"iteration": 1617715580880,
"links": [],
"panels": [
{
"collapsed": false,
"datasource": "Prometheus",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 8,
"panels": [],
"title": "CPU",
"type": "row"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 7,
"w": 24,
"x": 0,
"y": 1
},
"hiddenSeries": false,
"id": 15,
"legend": {
"alignAsTable": true,
"avg": true,
"current": false,
"max": true,
"min": false,
"rightSide": true,
"show": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null as zero",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.4.5",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": true,
"steppedLine": false,
"targets": [
{
"expr": "sum(rate(container_cpu_usage_seconds_total{instance=~\"$host\",name=~\"$container\",name=~\".+\"}[5m])) by (name) *100",
"hide": false,
"interval": "",
"legendFormat": "{{name}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "CPU Usage",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"$$hashKey": "object:606",
"format": "percent",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"$$hashKey": "object:607",
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"collapsed": false,
"datasource": "Prometheus",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 8
},
"id": 11,
"panels": [],
"title": "Memory",
"type": "row"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 9
},
"hiddenSeries": false,
"id": 9,
"legend": {
"alignAsTable": true,
"avg": true,
"current": false,
"max": true,
"min": false,
"rightSide": true,
"show": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null as zero",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.4.5",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": true,
"steppedLine": false,
"targets": [
{
"expr": "sum(container_memory_rss{instance=~\"$host\",name=~\"$container\",name=~\".+\"}) by (name)",
"hide": false,
"interval": "",
"legendFormat": "{{name}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Memory Usage",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"$$hashKey": "object:606",
"format": "bytes",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"$$hashKey": "object:607",
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 9
},
"hiddenSeries": false,
"id": 14,
"legend": {
"alignAsTable": true,
"avg": true,
"current": false,
"max": true,
"min": false,
"rightSide": true,
"show": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null as zero",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.4.5",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": true,
"steppedLine": false,
"targets": [
{
"expr": "sum(container_memory_cache{instance=~\"$host\",name=~\"$container\",name=~\".+\"}) by (name)",
"hide": false,
"interval": "",
"legendFormat": "{{name}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Memory Cached",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"$$hashKey": "object:606",
"format": "bytes",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"$$hashKey": "object:607",
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"collapsed": false,
"datasource": "Prometheus",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 17
},
"id": 2,
"panels": [],
"title": "Network",
"type": "row"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 18
},
"hiddenSeries": false,
"id": 4,
"legend": {
"alignAsTable": true,
"avg": true,
"current": false,
"hideEmpty": false,
"hideZero": false,
"max": true,
"min": false,
"rightSide": true,
"show": true,
"sideWidth": null,
"total": false,
"values": true
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.4.5",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "sum(rate(container_network_receive_bytes_total{instance=~\"$host\",name=~\"$container\",name=~\".+\"}[5m])) by (name)",
"hide": false,
"interval": "",
"legendFormat": "{{name}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Received Network Traffic",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"$$hashKey": "object:674",
"format": "Bps",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"$$hashKey": "object:675",
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 18
},
"hiddenSeries": false,
"id": 6,
"legend": {
"alignAsTable": true,
"avg": true,
"current": false,
"max": true,
"min": false,
"rightSide": true,
"show": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.4.5",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "sum(rate(container_network_transmit_bytes_total{instance=~\"$host\",name=~\"$container\",name=~\".+\"}[5m])) by (name)",
"interval": "",
"legendFormat": "{{name}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Sent Network Traffic",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"$$hashKey": "object:832",
"format": "Bps",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"$$hashKey": "object:833",
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"collapsed": false,
"datasource": "Prometheus",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 26
},
"id": 19,
"panels": [],
"title": "Misc",
"type": "row"
},
{
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {
"align": null,
"filterable": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "id"
},
"properties": [
{
"id": "custom.width",
"value": 260
}
]
},
{
"matcher": {
"id": "byName",
"options": "Running"
},
"properties": [
{
"id": "unit",
"value": "d"
},
{
"id": "decimals",
"value": 1
},
{
"id": "custom.displayMode",
"value": "color-text"
},
{
"id": "color",
"value": {
"fixedColor": "dark-green",
"mode": "fixed"
}
}
]
}
]
},
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 27
},
"id": 17,
"options": {
"showHeader": true,
"sortBy": []
},
"pluginVersion": "7.4.5",
"targets": [
{
"expr": "(time() - container_start_time_seconds{instance=~\"$host\",name=~\"$container\",name=~\".+\"})/86400",
"format": "table",
"instant": true,
"interval": "",
"legendFormat": "{{name}}",
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Containers Info",
"transformations": [
{
"id": "filterFieldsByName",
"options": {
"include": {
"names": [
"container_label_com_docker_compose_project",
"container_label_com_docker_compose_project_working_dir",
"image",
"instance",
"name",
"Value",
"container_label_com_docker_compose_service"
]
}
}
},
{
"id": "organize",
"options": {
"excludeByName": {},
"indexByName": {},
"renameByName": {
"Value": "Running",
"container_label_com_docker_compose_project": "Label",
"container_label_com_docker_compose_project_working_dir": "Working dir",
"container_label_com_docker_compose_service": "Service",
"image": "Registry Image",
"instance": "Instance",
"name": "Name"
}
}
}
],
"type": "table"
}
],
"schemaVersion": 27,
"style": "dark",
"tags": ["cadvisor", "docker"],
"templating": {
"list": [
{
"allValue": ".*",
"current": {},
"datasource": "Prometheus",
"definition": "label_values({__name__=~\"container.*\"},instance)",
"description": null,
"error": null,
"hide": 0,
"includeAll": true,
"label": "Host",
"multi": false,
"name": "host",
"options": [],
"query": {
"query": "label_values({__name__=~\"container.*\"},instance)",
"refId": "Prometheus-host-Variable-Query"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 5,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
},
{
"allValue": ".*",
"current": {},
"datasource": "Prometheus",
"definition": "label_values({__name__=~\"container.*\", instance=~\"$host\"},name)",
"description": null,
"error": null,
"hide": 0,
"includeAll": true,
"label": "Container",
"multi": false,
"name": "container",
"options": [],
"query": {
"query": "label_values({__name__=~\"container.*\", instance=~\"$host\"},name)",
"refId": "Prometheus-container-Variable-Query"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Cadvisor exporter",
"uid": "pMEd7m0Mz",
"version": 1,
"description": "Simple exporter for cadvisor only"
}

View File

@@ -1,7 +0,0 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://192.168.0.1:9090

View File

@@ -1,11 +0,0 @@
groups:
- name: AllInstances
rules:
- alert: InstanceDown
expr: up == 0
for: 1m
annotations:
title: 'Instance {{ $labels.instance }} down'
description: '{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minute.'
labels:
severity: 'critical'

View File

@@ -1,28 +0,0 @@
global:
scrape_interval: 15s
scrape_configs:
- job_name: prometheus
scrape_interval: 30s
static_configs:
- targets: ["localhost:9090"]
- job_name: node-exporter
scrape_interval: 30s
static_configs:
- targets:
["192.168.0.1:9100", "192.168.0.10:9100", "192.168.0.11:9100"]
- job_name: cadvisor
scrape_interval: 30s
static_configs:
- targets: ["192.168.0.1:9101", "192.168.0.11:9101"]
rule_files:
- prometheus_alerts_rules.yml
alerting:
alertmanagers:
- static_configs:
- targets:
- 192.168.0.1:9093

View File

@@ -1,85 +0,0 @@
- name: Create Folder /srv/prometheus if not exist
file:
path: /srv/prometheus
mode: 0755
state: directory
- name: Create Folder /srv/grafana if not exist
file:
path: /srv/grafana
mode: 0755
state: directory
- name: Create Folder /srv/alertmanager if not exist
file:
path: /srv/alertmanager
mode: 0755
state: directory
- name: Create prometheus configuration file
copy:
dest: /srv/prometheus/prometheus.yml
src: prometheus_main.yml
mode: 0644
- name: Create prometheus alert configuration file
copy:
dest: /srv/prometheus/prometheus_alerts_rules.yml
src: prometheus_alerts_rules.yml
mode: 0644
- name: Create grafana configuration files
copy:
dest: /srv/
src: grafana
mode: 0644
- name: Create alertmanager configuration file
template:
dest: /srv/alertmanager/alertmanager.yml
src: alertmanager/alertmanager.j2
mode: 0644
- name: Create Prometheus container
docker_container:
name: prometheus
restart_policy: always
image: prom/prometheus:
volumes:
- /srv/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- /srv/prometheus/prometheus_alerts_rules.yml:/etc/prometheus/prometheus_alerts_rules.yml
- prometheus_main_data:/prometheus
command: >
--config.file=/etc/prometheus/prometheus.yml
--storage.tsdb.path=/prometheus
--web.console.libraries=/etc/prometheus/console_libraries
--web.console.templates=/etc/prometheus/consoles
--web.enable-lifecycle
published_ports: "9090:9090"
- name: Create Grafana container
docker_container:
name: grafana
restart_policy: always
image: grafana/grafana:
volumes:
- grafana-data:/var/lib/grafana
- /srv/grafana/provisioning:/etc/grafana/provisioning
- /srv/grafana/dashboards:/var/lib/grafana/dashboards
env:
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_AUTH_ANONYMOUS_ORG_ROLE: "Admin"
published_ports: "3000:3000"
- name: Create Alertmanager container
docker_container:
name: alertmanager
restart_policy: always
image: prom/alertmanager:
volumes:
- alertmanager-data:/data
- /srv/alertmanager:/config
command: >
--config.file=/config/alertmanager.yml
--log.level=debug
published_ports: "9093:9093"

View File

@@ -1,13 +0,0 @@
route:
receiver: "mail"
repeat_interval: 4h
group_by: [ alertname ]
receivers:
- name: "mail"
email_configs:
- smarthost: "outlook.office365.com:587"
auth_username: "test@padok.fr"
auth_password: "{{ alertmanager_smtp_password }}"
from: "test@padok.fr"
to: "test@padok.fr"

View File

@@ -1,28 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/ollama/defaults/main.yml
# ------------------------------------------------------------------------------
# ROCm
ollama_rocm_version: "6.2" # ROCm release stream — update when upgrading
ollama_rocm_packages:
- hip-runtime-amd # Pulls in full ROCm stack
- rocminfo # GPU visibility verification tool
- rocm-smi-lib # ROCm system management interface
# Ollama
ollama_version: "latest"
ollama_port: 11434
ollama_host: "0.0.0.0" # Listen on all interfaces for cluster access
ollama_default_model: "qwen3:8b"
# HSA override — required for RX 5000-series (gfx1010 / RDNA 1)
# Tells ROCm to treat the GPU as gfx1010 which has supported LLVM targets.
ollama_hsa_override_gfx_version: "10.1.0"
# Data disk
ollama_setup_data_disk: true
ollama_data_disk: /dev/sdb
ollama_data_dir: /var/lib/ollama # Ollama default models path
ollama_data_vg: ollama-vg
ollama_data_lv: ollama-lv

View File

@@ -1,20 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/ollama/handlers/main.yml
# ------------------------------------------------------------------------------
- name: update apt cache
apt:
update_cache: yes
- name: update initramfs
command: update-initramfs -u
- name: reload systemd
systemd:
daemon_reload: yes
- name: restart ollama
systemd:
name: ollama
state: restarted

View File

@@ -1,229 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/ollama/tasks/main.yml
# DESCRIPTION: Deploys Ollama with AMD ROCm GPU acceleration on Ubuntu 24.04.
# Handles:
# - ROCm repository and driver stack installation
# - Data disk preparation (LVM, ext4, persistent mount)
# - Ollama installation and systemd service configuration
# - HSA override for RX 5000-series (gfx1010) compatibility
# - Initial model pull
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# ROCm prerequisites
# ------------------------------------------------------------------------------
- name: Install ROCm prerequisite packages
apt:
name:
- wget
- gnupg
- ca-certificates
- lvm2
state: present
update_cache: yes
- name: Add ROCm apt repository signing key
apt_key:
url: https://repo.radeon.com/rocm/rocm.gpg.key
state: present
- name: Add ROCm apt repository
apt_repository:
repo: "deb [arch=amd64] https://repo.radeon.com/rocm/apt/{{ ollama_rocm_version }} {{ ansible_distribution_release }} main"
state: present
filename: rocm
notify: update apt cache
- name: Flush handlers to update apt cache before ROCm install
meta: flush_handlers
# ------------------------------------------------------------------------------
# ROCm driver stack
# hip-runtime-amd pulls in the full ROCm stack as a dependency.
# rocminfo is used to verify GPU visibility after install.
# ------------------------------------------------------------------------------
- name: Install ROCm packages
apt:
name: "{{ ollama_rocm_packages }}"
state: present
notify: update initramfs
- name: Create /opt/rocm symlink to versioned directory
file:
src: "/opt/rocm-{{ ollama_rocm_version }}.0"
dest: /opt/rocm
state: link
force: false
- name: Add ROCm binaries to system PATH
copy:
dest: /etc/profile.d/rocm.sh
content: |
export PATH=$PATH:/opt/rocm/bin
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/rocm/lib
mode: '0644'
- name: Add wed user to render and video groups for GPU access
user:
name: "{{ ansible_user }}"
groups:
- render
- video
append: yes
- name: Add ollama user to render and video groups (created by Ollama installer)
user:
name: ollama
groups:
- render
- video
append: yes
ignore_errors: yes
# ollama user does not exist yet at this point — Ollama installer creates it.
# This task re-runs after install via the post-install handler.
# ------------------------------------------------------------------------------
# Data disk — LVM setup on /dev/sdb
# Follows the same pattern as the monitoring role (cinderella-castle).
# Wipes any stale GPT/partition signatures before creating the PV to avoid
# the "GPT protective partition" error encountered during monitoring deploy.
# ------------------------------------------------------------------------------
- name: Wipe any existing partition signatures from data disk
command: wipefs -a {{ ollama_data_disk }}
args:
creates: /dev/{{ ollama_data_vg }}
when: ollama_setup_data_disk | bool
- name: Create LVM physical volume on data disk
command: pvcreate {{ ollama_data_disk }}
args:
creates: /dev/{{ ollama_data_vg }}
when: ollama_setup_data_disk | bool
- name: Create LVM volume group
lvg:
vg: "{{ ollama_data_vg }}"
pvs: "{{ ollama_data_disk }}"
when: ollama_setup_data_disk | bool
- name: Create LVM logical volume using 100% of VG
lvol:
vg: "{{ ollama_data_vg }}"
lv: "{{ ollama_data_lv }}"
size: 100%FREE
shrink: false
when: ollama_setup_data_disk | bool
- name: Format logical volume as ext4
filesystem:
fstype: ext4
dev: "/dev/{{ ollama_data_vg }}/{{ ollama_data_lv }}"
opts: "-L ollama-data"
when: ollama_setup_data_disk | bool
- name: Create Ollama model directory mount point
file:
path: "{{ ollama_data_dir }}"
state: directory
mode: '0755'
- name: Mount Ollama data volume and add fstab entry
mount:
path: "{{ ollama_data_dir }}"
src: "/dev/{{ ollama_data_vg }}/{{ ollama_data_lv }}"
fstype: ext4
opts: defaults,noatime
state: mounted
when: ollama_setup_data_disk | bool
# noatime: eliminates inode access time writes on every model file read.
# Meaningful win for large sequential reads like model loading.
# ------------------------------------------------------------------------------
# Ollama installation
# Uses the official install script which creates the ollama user and
# systemd service unit. We override the service with our own config.
# ------------------------------------------------------------------------------
- name: Download Ollama install script
get_url:
url: https://ollama.ai/install.sh
dest: /tmp/ollama_install.sh
mode: '0755'
- name: Run Ollama install script
command: /tmp/ollama_install.sh
environment: "{{ {} if ollama_version == 'latest' else {'OLLAMA_VERSION': ollama_version} }}"
args:
creates: /usr/local/bin/ollama
- name: Add ollama user to render and video groups (post-install)
user:
name: ollama
groups:
- render
- video
append: yes
- name: Set ownership of Ollama data directory to ollama user
file:
path: "{{ ollama_data_dir }}"
owner: ollama
group: ollama
recurse: yes
# ------------------------------------------------------------------------------
# Systemd service override
# Injects HSA_OVERRIDE_GFX_VERSION for RX 5000-series compatibility and
# sets OLLAMA_MODELS to the data disk mount point.
# ------------------------------------------------------------------------------
- name: Create systemd override directory for ollama service
file:
path: /etc/systemd/system/ollama.service.d
state: directory
mode: '0755'
- name: Deploy ollama systemd service override
template:
src: ollama-override.conf.j2
dest: /etc/systemd/system/ollama.service.d/override.conf
mode: '0644'
notify:
- reload systemd
- restart ollama
- name: Flush handlers to apply service config before model pull
meta: flush_handlers
- name: Enable and start Ollama service
systemd:
name: ollama
state: started
enabled: yes
daemon_reload: yes
- name: Wait for Ollama API to become available
uri:
url: "http://localhost:{{ ollama_port }}/api/tags"
status_code: 200
register: ollama_health
retries: 12
delay: 5
until: ollama_health.status == 200
# ------------------------------------------------------------------------------
# Initial model pull
# ------------------------------------------------------------------------------
- name: Pull initial model
command: ollama pull {{ ollama_default_model }}
environment:
OLLAMA_HOST: "http://localhost:{{ ollama_port }}"
register: model_pull
changed_when: "'pulled' in model_pull.stdout or model_pull.rc == 0"
timeout: 600
# Qwen3 8B Q4_K_M is ~5GB — allow 10 minutes on first pull.

View File

@@ -1,13 +0,0 @@
# ------------------------------------------------------------------------------
# FILE: roles/ollama/templates/ollama-override.conf.j2
# DESCRIPTION: Systemd drop-in for the ollama service.
# Sets environment variables required for AMD GPU operation
# and configures the model storage path.
# Managed by Ansible — do not edit manually.
# ------------------------------------------------------------------------------
[Service]
Environment="OLLAMA_HOST={{ ollama_host }}:{{ ollama_port }}"
Environment="OLLAMA_MODELS={{ ollama_data_dir }}"
Environment="HSA_OVERRIDE_GFX_VERSION={{ ollama_hsa_override_gfx_version }}"
Environment="ROCR_VISIBLE_DEVICES=0"

View File

@@ -1,24 +0,0 @@
---
# Proxmox Authentik OIDC Configuration
proxmox_oidc_realm_name: "authentik"
proxmox_oidc_issuer_url: "https://authentik.local.mk-labs.cloud/application/o/proxmox/"
proxmox_oidc_username_claim: "username"
proxmox_oidc_scopes: "openid email profile"
proxmox_oidc_autocreate: true
proxmox_oidc_default_realm: false
proxmox_oidc_comment: "Authentik SSO"
# Client credentials - override via vault
proxmox_oidc_client_id: "{{ vault_proxmox_oidc_client_id }}"
proxmox_oidc_client_key: "{{ vault_proxmox_oidc_client_key }}"
# ACL mappings for Authentik users
# Format: list of dicts with path, user or group, and role
proxmox_oidc_acl_entries: []
# Example:
# - path: "/"
# user: "rblundon@authentik"
# role: "Administrator"
# - path: "/"
# group: "admins-authentik"
# role: "Administrator"

View File

@@ -1,12 +0,0 @@
---
# ansible/roles/proxmox/tasks/main.yml
#
# Main entry point for the proxmox role.
# Add existing tasks above the OIDC include as needed.
- name: Configure Authentik OIDC authentication
ansible.builtin.include_tasks: oidc.yml
tags:
- proxmox-oidc
- oidc
- authentik

View File

@@ -1,59 +0,0 @@
---
# ansible/roles/proxmox/tasks/oidc.yml
#
# Configures Proxmox to authenticate against Authentik via OpenID Connect.
# This only needs to run on ONE node in the cluster since realm config
# is stored in /etc/pve/domains.cfg (cluster-wide via pmxcfs).
- name: Check if OIDC realm already exists
ansible.builtin.command:
cmd: pveum realm list --output-format json
register: proxmox_realms
changed_when: false
- name: Parse existing realms
ansible.builtin.set_fact:
proxmox_realm_exists: >-
{{ proxmox_realms.stdout | from_json | selectattr('realm', 'equalto', proxmox_oidc_realm_name) | list | length > 0 }}
- name: Add OIDC realm for Authentik
ansible.builtin.shell: >-
pveum realm add {{ proxmox_oidc_realm_name }}
--type openid
--issuer-url '{{ proxmox_oidc_issuer_url }}'
--client-id '{{ proxmox_oidc_client_id }}'
--client-key '{{ proxmox_oidc_client_key }}'
--username-claim '{{ proxmox_oidc_username_claim }}'
--scopes '{{ proxmox_oidc_scopes }}'
--autocreate {{ proxmox_oidc_autocreate | ternary('1', '0') }}
--comment '{{ proxmox_oidc_comment }}'
{% if proxmox_oidc_default_realm %}--default 1{% endif %}
when: not proxmox_realm_exists
no_log: true
- name: Update OIDC realm for Authentik
ansible.builtin.shell: >-
pveum realm modify {{ proxmox_oidc_realm_name }}
--issuer-url '{{ proxmox_oidc_issuer_url }}'
--client-id '{{ proxmox_oidc_client_id }}'
--client-key '{{ proxmox_oidc_client_key }}'
--scopes '{{ proxmox_oidc_scopes }}'
--autocreate {{ proxmox_oidc_autocreate | ternary('1', '0') }}
--comment '{{ proxmox_oidc_comment }}'
{% if proxmox_oidc_default_realm %}--default 1{% endif %}
when: proxmox_realm_exists
no_log: true
# --- ACL Configuration ---
- name: Configure ACL entries for OIDC users
ansible.builtin.command:
cmd: >-
pveum acl modify {{ item.path }}
{% if item.user is defined %}--users {{ item.user }}{% endif %}
{% if item.group is defined %}--groups {{ item.group }}{% endif %}
--roles {{ item.role }}
loop: "{{ proxmox_oidc_acl_entries }}"
loop_control:
label: "{{ item.user | default(item.group) }} -> {{ item.role }} on {{ item.path }}"
when: proxmox_oidc_acl_entries | length > 0

View File

@@ -1,5 +0,0 @@
---
# file: roles/semaphore/defaults/main.yml
semaphore_compose_dir: /opt/docker/semaphore
semaphore_ssh_key_file: "~/.ssh/ansible"

View File

@@ -1,7 +0,0 @@
---
# file: roles/semaphore/handlers/main.yml
- name: Restart Semaphore
community.docker.docker_compose_v2:
project_src: "{{ semaphore_compose_dir }}"
state: restarted

View File

@@ -1,50 +0,0 @@
---
# file: roles/semaphore/tasks/main.yml
- name: Create Semaphore directory
ansible.builtin.file:
path: "{{ semaphore_compose_dir }}"
state: directory
owner: wed
group: docker
mode: '0755'
- name: Copy Compose file from boilerplate
ansible.builtin.copy:
src: "{{ playbook_dir }}/../../boilerplates/semaphore/compose.yaml"
dest: "{{ semaphore_compose_dir }}/compose.yaml"
owner: wed
group: docker
mode: '0644'
- name: Deploy Semaphore .env file
ansible.builtin.template:
src: env.j2
dest: "{{ semaphore_compose_dir }}/.env"
owner: wed
group: docker
mode: '0600'
- name: Copy SSH key for Ansible authentication
ansible.builtin.copy:
src: "{{ semaphore_ssh_key_file }}"
dest: "{{ semaphore_compose_dir }}/ansible_key"
owner: "1001"
group: "1001"
mode: '0600'
- name: Start Semaphore containers
community.docker.docker_compose_v2:
project_src: "{{ semaphore_compose_dir }}"
state: present
register: semaphore_compose
- name: Wait for Semaphore to be ready
ansible.builtin.uri:
url: "http://localhost:3000/api/ping"
method: GET
status_code: 200
register: semaphore_health
retries: 48
delay: 5
until: semaphore_health.status == 200

View File

@@ -1,3 +0,0 @@
DATABASE_PASSWORD={{ vault_semaphore_database_password }}
SEMAPHORE_ADMIN_PASSWORD={{ vault_semaphore_admin_password }}
SEMAPHORE_ACCESS_KEY_ENCRYPTION={{ vault_semaphore_access_key_encryption }}

View File

@@ -1,20 +0,0 @@
---
# file: roles/step-ca/defaults/main.yml
# Docker image
stepca_image: "smallstep/step-ca"
stepca_image_tag: "latest"
stepca_container_name: "step-ca"
# Paths
stepca_base_dir: "/opt/docker/step-ca"
stepca_data_dir: "{{ stepca_base_dir }}/data"
# CA configuration
stepca_ca_name: "mk-labs CA"
stepca_listen_port: 9000
stepca_ssh_enabled: true
stepca_remote_management: true
# Network
stepca_docker_network: "proxy"

View File

@@ -1,7 +0,0 @@
---
# file: roles/step-ca/handlers/main.yml
- name: restart step-ca
community.docker.docker_compose_v2:
project_src: "{{ stepca_base_dir }}"
state: restarted

View File

@@ -1,123 +0,0 @@
---
# file: roles/step-ca/tasks/main.yml
- name: Create step-ca base directory
ansible.builtin.file:
path: "{{ stepca_base_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Create step-ca data directory
ansible.builtin.file:
path: "{{ stepca_data_dir }}"
state: directory
owner: "1000"
group: "1000"
mode: "0755"
- name: Create Docker network for step-ca
community.docker.docker_network:
name: "{{ stepca_docker_network }}"
state: present
- name: Deploy step-ca compose file
ansible.builtin.template:
src: compose.yaml.j2
dest: "{{ stepca_base_dir }}/compose.yaml"
owner: root
group: root
mode: "0644"
notify: restart step-ca
- name: Deploy step-ca environment file
ansible.builtin.template:
src: env.j2
dest: "{{ stepca_base_dir }}/.env"
owner: root
group: root
mode: "0600"
no_log: true
notify: restart step-ca
- name: Create step-ca secrets directory
ansible.builtin.file:
path: "{{ stepca_data_dir }}/secrets"
state: directory
owner: "1000"
group: "1000"
mode: "0700"
- name: Write CA password file
ansible.builtin.copy:
content: "{{ stepca_ca_password }}"
dest: "{{ stepca_data_dir }}/secrets/password"
owner: "1000"
group: "1000"
mode: "0600"
no_log: true
- name: Check if step-ca is already initialized
ansible.builtin.stat:
path: "{{ stepca_data_dir }}/config/ca.json"
register: stepca_config
- name: Initialize step-ca (first run only)
when: not stepca_config.stat.exists
block:
- name: Run step-ca initialization
community.docker.docker_container:
name: "{{ stepca_container_name }}-init"
image: "{{ stepca_image }}:{{ stepca_image_tag }}"
command: >-
step ca init
--name "{{ stepca_ca_name }}"
--dns "{{ stepca_dns_names }}"
--address ":{{ stepca_listen_port }}"
--provisioner "admin"
--password-file /home/step/secrets/password
--ssh
--remote-management
volumes:
- "{{ stepca_data_dir }}:/home/step"
state: started
detach: false
cleanup: true
register: stepca_init
- name: Display CA fingerprint
ansible.builtin.debug:
msg: "{{ stepca_init.container.Output }}"
when: stepca_init is changed
- name: Start step-ca
community.docker.docker_compose_v2:
project_src: "{{ stepca_base_dir }}"
state: present
- name: Template OIDC provisioner patch script
ansible.builtin.template:
src: patch_oidc_provisioner.py.j2
dest: "{{ stepca_base_dir }}/patch_oidc_provisioner.py"
owner: root
group: root
mode: "0700"
no_log: true
- name: Patch OIDC provisioner in ca.json
ansible.builtin.command:
cmd: python3 {{ stepca_base_dir }}/patch_oidc_provisioner.py
register: oidc_patch_result
changed_when: "'CHANGED' in oidc_patch_result.stdout"
- name: Restart step-ca if OIDC provisioner changed
community.docker.docker_compose_v2:
project_src: "{{ stepca_base_dir }}"
state: restarted
when: oidc_patch_result is changed
- name: Clean up patch script
ansible.builtin.file:
path: "{{ stepca_base_dir }}/patch_oidc_provisioner.py"
state: absent

View File

@@ -1,24 +0,0 @@
---
# Managed by Ansible — do not edit manually
# Smallstep step-ca — SSH & X.509 Certificate Authority
services:
step-ca:
image: {{ stepca_image }}:{{ stepca_image_tag }}
container_name: {{ stepca_container_name }}
restart: unless-stopped
ports:
- "{{ stepca_listen_port }}:{{ stepca_listen_port }}"
volumes:
- {{ stepca_data_dir }}:/home/step
environment:
- DOCKER_STEPCA_INIT_NAME={{ stepca_ca_name }}
- DOCKER_STEPCA_INIT_DNS_NAMES={{ stepca_dns_names }}
- DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT={{ stepca_remote_management | lower }}
- DOCKER_STEPCA_INIT_SSH={{ stepca_ssh_enabled | lower }}
networks:
- {{ stepca_docker_network }}
networks:
{{ stepca_docker_network }}:
external: true

View File

@@ -1,4 +0,0 @@
# Managed by Ansible — do not edit manually
STEPCA_DNS_NAMES={{ stepca_dns_names }}
STEPCA_SSH_ENABLED={{ stepca_ssh_enabled | lower }}
STEPCA_REMOTE_MANAGEMENT={{ stepca_remote_management | lower }}

View File

@@ -1,57 +0,0 @@
#!/usr/bin/env python3
# Managed by Ansible — do not edit manually
# Patches the OIDC provisioner in step-ca's ca.json with values from Ansible vars
import json
import sys
ca_json_path = "{{ stepca_data_dir }}/config/ca.json"
provisioner_name = "{{ stepca_oidc_provisioner_name }}"
desired_domains = {{ stepca_oidc_domains | to_json }}
desired_client_id = "{{ stepca_oidc_client_id }}"
desired_client_secret = "{{ stepca_oidc_client_secret }}"
desired_config_endpoint = "{{ stepca_oidc_configuration_endpoint }}"
desired_listen_address = "{{ stepca_oidc_listen_address }}"
with open(ca_json_path) as f:
cfg = json.load(f)
changed = False
found = False
for p in cfg["authority"]["provisioners"]:
if p.get("name") == provisioner_name and p.get("type") == "OIDC":
found = True
updates = {
"domains": desired_domains,
"clientID": desired_client_id,
"clientSecret": desired_client_secret,
"configurationEndpoint": desired_config_endpoint,
"listenAddress": desired_listen_address,
}
for key, value in updates.items():
if p.get(key) != value:
p[key] = value
changed = True
break
if not found:
# Add the OIDC provisioner if it doesn't exist
cfg["authority"]["provisioners"].append({
"type": "OIDC",
"name": provisioner_name,
"clientID": desired_client_id,
"clientSecret": desired_client_secret,
"configurationEndpoint": desired_config_endpoint,
"domains": desired_domains,
"listenAddress": desired_listen_address,
"claims": {"enableSSHCA": True},
"options": {"x509": {}, "ssh": {}},
})
changed = True
if changed:
with open(ca_json_path, "w") as f:
json.dump(cfg, f, indent=8)
print("CHANGED")
else:
print("OK")

View File

@@ -1,15 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/talosctl/defaults/main.yml
# DESCRIPTION: Default variables for talosctl installation.
# ------------------------------------------------------------------------------
# Pin to a specific version for reproducibility.
# Update this when upgrading the fastpass cluster.
talosctl_version: "v1.12.4"
# Architecture of the target host (amd64 or arm64)
talosctl_arch: "amd64"
# Where to install the binary
talosctl_install_dir: "/usr/local/bin"

View File

@@ -1,51 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/talosctl/tasks/main.yml
# DESCRIPTION: Install talosctl binary from GitHub releases.
# Downloads a pinned version, installs to /usr/local/bin,
# and verifies the installation.
# ------------------------------------------------------------------------------
- name: Check if talosctl is already installed
stat:
path: "{{ talosctl_install_dir }}/talosctl"
register: talosctl_binary
- name: Get installed talosctl version
command: "{{ talosctl_install_dir }}/talosctl version --client --short"
register: talosctl_installed_version
changed_when: false
failed_when: false
when: talosctl_binary.stat.exists
- name: Install talosctl
when: >
not talosctl_binary.stat.exists or
talosctl_version not in (talosctl_installed_version.stdout | default(''))
block:
- name: Download talosctl {{ talosctl_version }}
get_url:
url: "https://github.com/siderolabs/talos/releases/download/{{ talosctl_version }}/talosctl-linux-{{ talosctl_arch }}"
dest: "/tmp/talosctl"
mode: "0755"
- name: Install talosctl to {{ talosctl_install_dir }}
copy:
src: "/tmp/talosctl"
dest: "{{ talosctl_install_dir }}/talosctl"
mode: "0755"
remote_src: true
- name: Clean up downloaded binary
file:
path: "/tmp/talosctl"
state: absent
- name: Verify talosctl installation
command: "{{ talosctl_install_dir }}/talosctl version --client --short"
register: talosctl_verify
changed_when: false
- name: Display installed talosctl version
debug:
msg: "talosctl installed: {{ talosctl_verify.stdout }}"

View File

@@ -1,17 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/talosctl/meta/main.yml
# ------------------------------------------------------------------------------
galaxy_info:
author: rblundon
description: Install talosctl CLI for managing TalosOS clusters
license: MIT
min_ansible_version: "2.14"
platforms:
- name: Ubuntu
versions:
- noble
dependencies:
- role: common

View File

@@ -1,3 +0,0 @@
---
node_exporter_version: v1.4.0
cadvisor_version: v0.46.0

View File

@@ -1,29 +0,0 @@
---
- name: Create NodeExporter
docker_container:
name: node-exporter
restart_policy: always
image: prom/node-exporter:{{ node_exporter_version }}
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command: >
--path.procfs=/host/proc
--path.rootfs=/rootfs
--path.sysfs=/host/sys
--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)
published_ports: "9100:9100"
- name: Create cAdvisor
docker_container:
name: cadvisor
restart_policy: always
image: gcr.io/cadvisor/cadvisor:{{ cadvisor_version }}
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
published_ports: "9101:8080"

View File

@@ -1,38 +0,0 @@
Role Name
=========
A brief description of the role goes here.
Requirements
------------
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
Role Variables
--------------
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
Dependencies
------------
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
Example Playbook
----------------
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
- hosts: servers
roles:
- { role: username.rolename, x: 42 }
License
-------
BSD
Author Information
------------------
An optional section for the role authors to include contact information, or a website (HTML is not allowed).

View File

@@ -1,3 +0,0 @@
#SPDX-License-Identifier: MIT-0
---
# defaults file for time-sync

View File

@@ -1,3 +0,0 @@
#SPDX-License-Identifier: MIT-0
---
# handlers file for time-sync

View File

@@ -1,35 +0,0 @@
#SPDX-License-Identifier: MIT-0
galaxy_info:
author: your name
description: your role description
company: your company (optional)
# If the issue tracker for your role is not on github, uncomment the
# next line and provide a value
# issue_tracker_url: http://example.com/issue/tracker
# Choose a valid license ID from https://spdx.org - some suggested licenses:
# - BSD-3-Clause (default)
# - MIT
# - GPL-2.0-or-later
# - GPL-3.0-only
# - Apache-2.0
# - CC-BY-4.0
license: license (GPL-2.0-or-later, MIT, etc)
min_ansible_version: 2.1
# If this a Container Enabled role, provide the minimum Ansible Container version.
# min_ansible_container_version:
galaxy_tags: []
# List tags for your role here, one per line. A tag is a keyword that describes
# and categorizes the role. Users find roles by searching for tags. Be sure to
# remove the '[]' above, if you add tags to this list.
#
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
# Maximum 20 tags per role.
dependencies: []
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
# if you add dependencies to this list.

View File

@@ -1,41 +0,0 @@
---
# tasks file for time-sync
- name: Manage Time Synchronization
become: true
block:
- name: Remove the ntp package
ansible.builtin.package:
name: ntp
state: absent
- name: Install the chrony package
ansible.builtin.package:
name: chrony
state: present
- name: Remove pool entries from Chrony config file
ansible.builtin.lineinfile:
path: /etc/chrony/chrony.conf
regexp: '^pool\s*'
state: absent
- name: Add local NTP servers to Chrony config file
ansible.builtin.lineinfile:
path: /etc/chrony/chrony.conf
line: "server 10.1.71.21 iburst"
state: present
- name: Start and enable chronyd
ansible.builtin.systemd:
name: chronyd
state: started
enabled: true
- name: Force time synchronization
become: true
ansible.builtin.command: chronyc makestep
register: chronyc_status
- name: Display chronyc status
ansible.builtin.debug:
var: chronyc_status.stdout

View File

@@ -1,3 +0,0 @@
#SPDX-License-Identifier: MIT-0
localhost

View File

@@ -1,6 +0,0 @@
#SPDX-License-Identifier: MIT-0
---
- hosts: localhost
remote_user: root
roles:
- time-sync

View File

@@ -1,3 +0,0 @@
#SPDX-License-Identifier: MIT-0
---
# vars file for time-sync

View File

@@ -1,10 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/traefik/defaults/main.yml
# ------------------------------------------------------------------------------
# Directory on the host where Traefik files are deployed
traefik_base_dir: /opt/docker/traefik
# Cloudflare API token — MUST be set in vault
traefik_cloudflare_api_token: ""

View File

@@ -1,9 +0,0 @@
# ------------------------------------------------------------------------------
# FILE: roles/traefik/handlers/main.yml
# ------------------------------------------------------------------------------
- name: restart traefik
community.docker.docker_compose_v2:
project_src: "{{ traefik_base_dir }}"
state: present
recreate: always

View File

@@ -1,55 +0,0 @@
# ------------------------------------------------------------------------------
# FILE: roles/traefik/tasks/main.yml
# DESCRIPTION: Deploys Traefik from boilerplate-generated files in the repo.
# The compose.yaml and dynamic config live in boilerplates/traefik/
# and are copied to the host. Only the .env file is templated
# to inject secrets from Ansible vault.
# ------------------------------------------------------------------------------
- name: Create Traefik directory structure
file:
path: "{{ item }}"
state: directory
owner: "{{ ansible_user }}"
group: docker
mode: '0775'
loop:
- "{{ traefik_base_dir }}"
- "{{ traefik_base_dir }}/dynamic"
- name: Copy compose.yaml from boilerplate
copy:
src: "{{ playbook_dir }}/../../boilerplates/traefik/compose.yaml"
dest: "{{ traefik_base_dir }}/compose.yaml"
owner: "{{ ansible_user }}"
group: docker
mode: '0644'
notify: restart traefik
- name: Copy dynamic configuration from boilerplate
copy:
src: "{{ playbook_dir }}/../../boilerplates/traefik/dynamic/"
dest: "{{ traefik_base_dir }}/dynamic/"
owner: "{{ ansible_user }}"
group: docker
mode: '0644'
notify: restart traefik
- name: Deploy .env with secrets from vault
template:
src: env.j2
dest: "{{ traefik_base_dir }}/.env"
owner: "{{ ansible_user }}"
group: docker
mode: '0600'
notify: restart traefik
- name: Create Traefik Docker network
docker_network:
name: proxy
state: present
- name: Start Traefik
community.docker.docker_compose_v2:
project_src: "{{ traefik_base_dir }}"
state: present

View File

@@ -1,3 +0,0 @@
# Managed by Ansible — do not edit manually
CF_DNS_API_TOKEN={{ traefik_cloudflare_api_token }}
CLOUDFLARE_TUNNEL_TOKEN={{ vault_traefik_tunnel_token }}