Remove dups

This commit is contained in:
Mike McFetridge
2026-07-24 15:42:46 -04:00
parent 84e1ead50c
commit b78378565a
345 changed files with 0 additions and 40650 deletions
@@ -1,20 +0,0 @@
---
- name: "Activate Checkmk changes"
hosts: localhost
gather_facts: false
vars_files:
- secrets.yaml
vars:
server_url: "http://192.168.2.4:5000"
site: "cmk"
tasks:
- name: "Start activation on a specific site"
checkmk.general.activation:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
force_foreign_changes: 'true'
sites:
- "{{ site }}"
@@ -1,16 +0,0 @@
---
- name: Install Checkmk agent on all hosts
hosts: all
become: true
roles:
- checkmk.general.agent
vars:
checkmk_agent_version: "2.4.0p15"
checkmk_agent_server: << checkmk_server >>
checkmk_agent_server_protocol: << checkmk_protocol >>
checkmk_agent_site: << checkmk_site >>
checkmk_agent_auto_activate: << checkmk_auto_activate >>
checkmk_agent_tls: << checkmk_tls >>
checkmk_agent_user: << checkmk_user >>
checkmk_agent_pass: << checkmk_pass >>
checkmk_agent_host_name: << checkmk_host >>
@@ -1,82 +0,0 @@
{
"slug": "checkmk-install-agent",
"kind": "ansible",
"metadata": {
"name": "Install Checkmk Agent",
"description": "Installs and registers the Checkmk monitoring agent on target hosts using the checkmk.general.agent Ansible role.",
"tags": [],
"icon": {
"provider": "selfhst",
"id": "checkmk"
},
"draft": false,
"version": {
"name": "2.4.0",
"source_dep_name": "manual/checkmk-install-agent"
}
},
"variables": [
{
"title": "Checkmk Configuration",
"name": "checkmk",
"items": [
{
"name": "checkmk_server",
"type": "str",
"title": "Checkmk Server",
"required": true
},
{
"name": "checkmk_protocol",
"type": "str",
"title": "Protocol",
"required": true,
"default": "https",
"config": {
"placeholder": "https"
}
},
{
"name": "checkmk_site",
"type": "str",
"title": "Checkmk Site",
"required": true,
"default": "cmk",
"config": {
"placeholder": "cmk"
}
},
{
"name": "checkmk_auto_activate",
"type": "bool",
"title": "Auto Activate Agent",
"required": false
},
{
"name": "checkmk_tls",
"type": "bool",
"title": "TLS",
"required": false
},
{
"name": "checkmk_user",
"type": "str",
"title": "Checkmk Automation User",
"required": true
},
{
"name": "checkmk_pass",
"type": "secret",
"title": "Automation Password",
"required": true
},
{
"name": "checkmk_host",
"type": "str",
"title": "Checkmk Host Name",
"required": true
}
]
}
]
}
@@ -1,18 +0,0 @@
---
- name: Manage Checkmk host
hosts: all
gather_facts: false
tasks:
- name: "Create or update host in Checkmk"
checkmk.general.host:
server_url: "<< checkmk_protocol >>://<< checkmk_server >>"
site: << checkmk_site >>
automation_user: << checkmk_user >>
automation_secret: << checkmk_pass >>
name: << host_name >>
attributes:
ipaddress: << host_ip >>
folder: << host_folder >>
state: "present"
delegate_to: localhost
run_once: true
@@ -1,94 +0,0 @@
{
"slug": "checkmk-manage-host",
"kind": "ansible",
"metadata": {
"name": "Manage Checkmk Host",
"description": "Manages host entries in Checkmk monitoring using the checkmk.general.host Ansible module. Creates or updates host configuration in your Checkmk instance.",
"tags": [],
"icon": {
"provider": "selfhst",
"id": "checkmk"
},
"draft": false,
"version": {
"name": "2.4.0",
"source_dep_name": "manual/checkmk-manage-host"
}
},
"variables": [
{
"title": "Checkmk Configuration",
"name": "checkmk",
"items": [
{
"name": "checkmk_server",
"type": "str",
"title": "Checkmk Server",
"required": true
},
{
"name": "checkmk_protocol",
"type": "str",
"title": "Protocol",
"required": true,
"default": "https",
"config": {
"placeholder": "https"
}
},
{
"name": "checkmk_site",
"type": "str",
"title": "Checkmk Site",
"required": true,
"default": "cmk",
"config": {
"placeholder": "cmk"
}
},
{
"name": "checkmk_user",
"type": "str",
"title": "Checkmk Automation User",
"required": true
},
{
"name": "checkmk_pass",
"type": "secret",
"title": "Automation Password",
"required": true
}
]
},
{
"title": "Host Configuration",
"name": "host",
"items": [
{
"name": "host_name",
"type": "str",
"title": "Host Name",
"required": true,
"description": "Hostname to add to Checkmk"
},
{
"name": "host_ip",
"type": "str",
"title": "Host IP",
"required": true,
"description": "IP address of the host"
},
{
"name": "host_folder",
"type": "str",
"title": "Folder Path",
"required": true,
"default": "/",
"config": {
"placeholder": "/"
}
}
]
}
]
}
@@ -1,20 +0,0 @@
# to run: ansible-playbook -i ./inventory.ini install-agent.yaml --ask-become-pass
---
- name: "Install Checkmk agent on all hosts"
hosts: all
become: yes
roles:
- checkmk.general.agent
vars:
checkmk_agent_version: "2.3.0p7"
#checkmk_agent_server: "192.168.2.4:5000"
checkmk_agent_server: "checkmk.mikemcfetridge.com"
checkmk_agent_server_protocol: https
checkmk_agent_site: "cmk"
checkmk_agent_auto_activate: true
checkmk_agent_tls: "true" # NOTE: Register Agent to enable TLS
checkmk_agent_user: "{{ automation_user }}"
checkmk_agent_pass: "{{ automation_secret }}"
checkmk_agent_host_name: "{{ ansible_hostname }}" # NOTE: Required to replace FQDN with hostname only
vars_files:
- secrets.yaml
-23
View File
@@ -1,23 +0,0 @@
[servers]
192.168.2.1
192.168.2.2
192.168.2.3
192.168.2.4
192.168.2.5
192.168.2.6
192.168.2.7
192.168.2.8
192.168.2.9
192.168.2.10
192.168.2.11
192.168.2.12
192.168.2.13
192.168.2.14
192.168.2.16
192.168.2.17
192.168.2.18
192.168.2.19
192.168.2.20
192.168.2.22
192.168.2.23
192.168.2.24
-21
View File
@@ -1,21 +0,0 @@
[servers]
ansible-server.home.mikemcfetridge.com
arrsserver.home.mikemcfetridge.com
beszel-server.home.mikemcfetridge.com
checkmk.home.mikemcfetridge.com
dockerapps.home.mikemcfetridge.com
gitea-server.home.mikemcfetridge.com
glance-server.home.mikemcfetridge.com
immich-wallabag.home.mikemcfetridge.com
linkwarden.home.mikemcfetridge.com
netbird-1.home.mikemcfetridge.com
netbird-2.home.mikemcfetridge.com
npm-server.home.mikemcfetridge.com
pihole-server.home.mikemcfetridge.com
pve.home.mikemcfetridge.com
pve2.home.mikemcfetridge.com
retrogaming-server.home.mikemcfetridge.com
searxng-server.home.mikemcfetridge.com
technitiumdns-server.home.mikemcfetridge.com
vaultwarden-server.home.mikemcfetridge.com
wireguard-server.home.mikemcfetridge.com
@@ -1,25 +0,0 @@
---
- name: "Manage Checkmk rules"
hosts: localhost
gather_facts: false
vars_files:
- secrets.yaml
vars:
server_url: "http://192.168.2.4:5000"
site: "cmk"
tasks:
- name: Get a rule with a particular rule id
ansible.builtin.debug:
msg: "Rule: {{ extensions | to_nice_yaml }}"
vars:
extensions: "{{
lookup('checkmk.general.rule',
rule_id='checkmk-rule-id',
server_url=server_url,
site=site,
automation_user=automation_user,
automation_secret=automation_secret,
validate_certs=False
)
}}"
-286
View File
@@ -1,286 +0,0 @@
---
- name: "Manage Checkmk hosts"
hosts: localhost
gather_facts: false
vars_files:
- secrets.yaml
vars:
server_url: "http://192.168.2.4:5000"
site: "cmk"
tasks:
- name: "Create host - PVE01"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "pve01"
attributes:
ipaddress: "192.168.2.1"
folder: "/"
state: "present"
- name: "Create host - PVE02"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "pve02"
attributes:
ipaddress: "192.168.2.2"
folder: "/"
state: "present"
- name: "Create host - beszel"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "Beszel"
attributes:
ipaddress: "192.168.2.3"
folder: "/"
state: "present"
- name: "Create host - Checkmk"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "Checkmk"
attributes:
ipaddress: "192.168.2.4"
folder: "/"
state: "present"
- name: "Create host - komodo"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "komodo"
attributes:
ipaddress: "192.168.2.5"
folder: "/"
state: "present"
- name: "Create host - Datacenter"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "Datacenter"
attributes:
ipaddress: "192.168.2.6"
folder: "/"
state: "present"
- name: "Create host - DockerApps"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "DockerApps"
attributes:
ipaddress: "192.168.2.7"
folder: "/"
state: "present"
- name: "Create host - PIHOLE"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "PiHole"
attributes:
ipaddress: "192.168.2.8"
folder: "/"
state: "present"
- name: "Create host - TECHNITIUMDNS"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "TechnitiumDNS"
attributes:
ipaddress: "192.168.2.9"
folder: "/"
state: "present"
- name: "Create host - Wireguard"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "Wireguard"
attributes:
ipaddress: "192.168.2.10"
folder: "/"
state: "present"
- name: "Create host - Ansible"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "Ansible"
attributes:
ipaddress: "192.168.2.11"
folder: "/"
state: "present"
- name: "Create host - LinkWarden"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "LinkWarden"
attributes:
ipaddress: "192.168.2.12"
folder: "/"
state: "present"
- name: "Create host - Immich"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "Immich"
attributes:
ipaddress: "192.168.2.13"
folder: "/"
state: "present"
- name: "Create host - RetroGaming"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "RetroGaming"
attributes:
ipaddress: "192.168.2.14"
folder: "/"
state: "present"
- name: "Create host - MediaServer"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "MediaServer"
attributes:
ipaddress: "192.168.2.16"
folder: "/"
state: "present"
- name: "Create host - NetBird1"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "NetBird1"
attributes:
ipaddress: "192.168.2.17"
folder: "/"
state: "present"
- name: "Create host - NetBird2"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "NetBird2"
attributes:
ipaddress: "192.168.2.18"
folder: "/"
state: "present"
- name: "Create host - VaultWarden"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "VaultWarden"
attributes:
ipaddress: "192.168.2.19"
folder: "/"
state: "present"
- name: "Create host - Glance"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "Glance"
attributes:
ipaddress: "192.168.2.20"
folder: "/"
state: "present"
- name: "Create host - NPM"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "Npm"
attributes:
ipaddress: "192.168.2.22"
folder: "/"
state: "present"
- name: "Create host - Searxng"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "Searxng"
attributes:
ipaddress: "192.168.2.23"
folder: "/"
state: "present"
- name: "Create host - Gitea"
checkmk.general.host:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
name: "Gitea"
attributes:
ipaddress: "192.168.2.24"
folder: "/"
state: "present"
- name: "Start activation on a specific site"
checkmk.general.activation:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
force_foreign_changes: 'true'
sites:
- "{{ site }}"
@@ -1,71 +0,0 @@
---
- name: "Manage Checkmk rules"
hosts: localhost
gather_facts: false
vars_files:
- secrets.yaml
vars:
server_url: "checkmk.home.arpa"
site: "cmk"
tasks:
- name: Create DNS Check Rule
checkmk.general.rule:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
ruleset: "active_checks:dns"
rule:
properties: {
"comment": "Ansible managed",
"description": "DNS DNS Monitoring",
"disabled": false,
}
conditions: {
"host_label_groups": [],
"host_name": {
"match_on": [
"your-dns-container-host"
],
"operator": "one_of"
},
"host_tags": [],
"service_label_groups": []
}
"value_raw": {
"hostname": "hostname-to-query",
"server": "dns-server-ip",
"expected_addresses_list": [
"expected-ip-address"
]
}
location:
folder: "/"
position: "top"
state: "present"
- name: Create NVME Temperature override rule
checkmk.general.rule:
server_url: "{{ server_url }}"
site: "{{ site }}"
automation_user: "{{ automation_user }}"
automation_secret: "{{ automation_secret }}"
ruleset: "checkgroup_parameters:temperature"
rule:
conditions:
host_label_groups: []
host_tags: []
service_description:
match_on:
- "DRIVE MODEL NAME*"
operator: "one_of"
service_label_groups: []
location:
folder: "/"
position: "top"
properties:
description: "NVME Temperature override"
disabled: false
value_raw: "{'levels': (60.0, 80.0)}"
state: present
-3
View File
@@ -1,3 +0,0 @@
---
automation_user: "ansible"
automation_secret: "Px40If4VUvvA35"
@@ -1,52 +0,0 @@
---
- name: Enable Docker TLS
hosts: all
become: true
vars:
certs_path: "<< certs_path >>"
tasks:
- name: Check if docker certs are existing
ansible.builtin.stat:
path: {{ certs_path }}
register: certs_dir
- name: Fail if docker certs are not existing
ansible.builtin.fail:
msg: "Docker certificates are not existing in /root/docker-certs."
when: not certs_dir.stat.exists
- name: Get machine's primary internal ip address from eth0 interface
ansible.builtin.setup:
register: ip_address
- name: Set machine's primary internal ip address
ansible.builtin.set_fact:
ip_address: {{ ip_address.ansible_facts.ansible_default_ipv4.address }}
- name: Check if ip_address is a valid ip address
ansible.builtin.assert:
that:
- ip_address is match("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$")
fail_msg: "ip_address is not a valid ip address."
success_msg: "ip_address is a valid ip address."
- name: Change docker daemon to use certs
ansible.builtin.lineinfile:
path: /lib/systemd/system/docker.service
line: >
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
-H tcp://{{ ip_address }}:2376 --tlsverify --tlscacert={{ certs_path }}/ca.pem
--tlscert={{ certs_path }}/server-cert.pem --tlskey={{ certs_path }}/server-key.pem
regexp: '^ExecStart='
state: present
- name: Reload systemd daemon
ansible.builtin.systemd:
daemon_reload: true
- name: Restart docker daemon
ansible.builtin.systemd:
name: docker
state: restarted
enabled: true
@@ -1,37 +0,0 @@
{
"slug": "docker-certs-enable",
"kind": "ansible",
"metadata": {
"name": "Enable Docker TLS",
"description": "Enables TLS on the Docker daemon using existing certificates. Configures Docker for secure remote API access over an encrypted connection.",
"tags": [],
"icon": {
"provider": "simple-icons",
"id": "docker"
},
"draft": true,
"version": {
"name": "1.0.0",
"source_dep_name": "manual/docker-certs-enable"
}
},
"variables": [
{
"title": "Certificate Configuration",
"name": "certificates",
"items": [
{
"name": "certs_path",
"type": "str",
"title": "Certs Path",
"required": false,
"default": "/root/docker-certs",
"description": "Path where certificates are stored",
"config": {
"placeholder": "/root/docker-certs"
}
}
]
}
]
}
@@ -1,158 +0,0 @@
---
- name: Generate Docker TLS certificates
hosts: all
become: true
vars:
certs_path: "<< certs_path >>"
cert_validity_days: << cert_validity_days >>
cn_domain: "<< cn_domain >>"
tasks:
- name: Check if docker certs are existing
ansible.builtin.stat:
path: {{ certs_path }}
register: certs_dir
- name: Create docker certs directory (if needed)
ansible.builtin.file:
path: {{ certs_path }}
state: directory
mode: '0700'
when: not certs_dir.stat.exists
- name: Check if docker certs directory is empty
ansible.builtin.command: ls -A {{ certs_path }}
register: certs_list
when: certs_dir.stat.exists
changed_when: false
ignore_errors: true
- name: Fail if docker certs already exist
ansible.builtin.fail:
msg: "Docker certificates already exist in /root/docker-certs."
when: certs_list.stdout | default('') != ''
- name: Get machine's primary internal ip address from eth0 interface
ansible.builtin.setup:
register: ip_address
- name: Set machine's primary internal ip address
ansible.builtin.set_fact:
ip_address: {{ ip_address.ansible_facts.ansible_default_ipv4.address }}
- name: Check if ip_address is a valid ip address
ansible.builtin.assert:
that:
- ip_address is match("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$")
fail_msg: "ip_address is not a valid ip address."
success_msg: "ip_address is a valid ip address."
- name: Generate CA private key
ansible.builtin.command:
cmd: >
openssl genrsa -out {{ certs_path }}/ca-key.pem 4096
args:
creates: {{ certs_path }}/ca-key.pem
- name: Generate CA certificate
ansible.builtin.command:
cmd: >
openssl req -sha256 -new -x509
-subj "/CN={{ cn_domain }}"
-days {{ cert_validity_days }}
-key {{ certs_path }}/ca-key.pem
-out {{ certs_path }}/ca.pem
args:
creates: {{ certs_path }}/ca.pem
- name: Generate server private key
ansible.builtin.command:
cmd: >
openssl genrsa -out {{ certs_path }}/server-key.pem 4096
creates: {{ certs_path }}/server-key.pem
- name: Generate server certificate signing request
ansible.builtin.command:
cmd: >
openssl req -sha256 -new
-subj "/CN={{ inventory_hostname }}"
-key {{ certs_path }}/server-key.pem
-out {{ certs_path }}/server.csr
creates: {{ certs_path }}/server.csr
- name: Generate server certificate extension file
ansible.builtin.shell: |
echo "subjectAltName = DNS:{{ inventory_hostname }},IP:{{ ip_address }},IP:127.0.0.1" >> {{ certs_path }}/extfile.cnf
echo "extendedKeyUsage = serverAuth" >> {{ certs_path }}/extfile.cnf
args:
creates: {{ certs_path }}/extfile.cnf
- name: Generate server certificate
ansible.builtin.command:
cmd: >
openssl x509 -req -days {{ cert_validity_days }} -sha256
-in {{ certs_path }}/server.csr
-CA {{ certs_path }}/ca.pem
-CAkey {{ certs_path }}/ca-key.pem
-CAcreateserial -out {{ certs_path }}/server-cert.pem
-extfile {{ certs_path }}/extfile.cnf
creates: {{ certs_path }}/server-cert.pem
- name: Generate client private key
ansible.builtin.command:
cmd: >
openssl genrsa -out {{ certs_path }}/key.pem 4096
creates: {{ certs_path }}/key.pem
- name: Generate client certificate signing request
ansible.builtin.command:
cmd: >
openssl req -sha256 -new
-subj "/CN=client"
-key {{ certs_path }}/key.pem
-out {{ certs_path }}/client.csr
creates: {{ certs_path }}/client.csr
- name: Generate client certificate extension file
ansible.builtin.shell: |
echo "extendedKeyUsage = clientAuth" >> {{ certs_path }}/client-extfile.cnf
args:
creates: {{ certs_path }}/client-extfile.cnf
- name: Generate client certificate
ansible.builtin.command:
cmd: >
openssl x509 -req -days {{ cert_validity_days }}
-sha256 -in {{ certs_path }}/client.csr
-CA {{ certs_path }}/ca.pem
-CAkey {{ certs_path }}/ca-key.pem
-CAcreateserial -out {{ certs_path }}/cert.pem
-extfile {{ certs_path }}/client-extfile.cnf
creates: {{ certs_path }}/cert.pem
- name: Remove client certificate signing request
ansible.builtin.file:
path: {{ certs_path }}/server.csr
state: absent
- name: Remove client certificate signing request
ansible.builtin.file:
path: {{ certs_path }}/client.csr
state: absent
- name: Remove server certificate extension file
ansible.builtin.file:
path: {{ certs_path }}/extfile.cnf
state: absent
- name: Remove client certificate extension file
ansible.builtin.file:
path: {{ certs_path }}/client-extfile.cnf
state: absent
- name: Set permissions for docker certs
ansible.builtin.file:
path: {{ certs_path }}
mode: '0700'
recurse: true
follow: true
@@ -1,63 +0,0 @@
{
"slug": "docker-certs",
"kind": "ansible",
"metadata": {
"name": "Generate Docker TLS Certificates",
"description": "Generates TLS certificates for the Docker daemon, including CA, server, and client certificates. Used to secure Docker remote API access.",
"tags": [],
"icon": {
"provider": "simple-icons",
"id": "docker"
},
"draft": true,
"version": {
"name": "1.0.0",
"source_dep_name": "manual/docker-certs"
}
},
"variables": [
{
"title": "Certificate Configuration",
"name": "certificates",
"items": [
{
"name": "certs_path",
"type": "str",
"title": "Certs Path",
"required": false,
"default": "/root/docker-certs",
"description": "Path where certificates will be stored",
"config": {
"placeholder": "/root/docker-certs"
}
},
{
"name": "cert_validity_days",
"type": "int",
"title": "Validity Days",
"required": false,
"default": 3630,
"description": "Certificate validity period in days",
"config": {
"slider": true,
"min": 30,
"max": 3650,
"step": 30,
"placeholder": "3650",
"unit": "days"
}
},
{
"name": "cn_domain",
"type": "str",
"title": "CA Common Name",
"required": false,
"description": "Common Name (CN) for the CA certificate",
"config": {
"placeholder": "home.arpa.tld"
}
}
]
}
]
}
@@ -1,18 +0,0 @@
---
- name: Docker prune safe
hosts: all
become: true
gather_facts: false
tasks:
- name: Prune Docker resources with safe defaults
community.docker.docker_prune:
containers: << prune_stopped_containers | default(false) >>
images: true
images_filters:
dangling: << not (prune_include_unused_tagged_images | default(false)) >>
networks: << prune_unused_networks | default(false) >>
volumes: << prune_unused_volumes | default(false) >>
builder_cache: << prune_builder_cache | default(false) >>
builder_cache_keep_storage: "<< prune_keep_storage_mb | default(1024) >>M"
timeout: 300
@@ -1,2 +0,0 @@
collections:
- name: community.docker
@@ -1,83 +0,0 @@
{
"slug": "docker-cleanup",
"kind": "ansible",
"metadata": {
"name": "Docker Cleanup",
"description": "Safely prunes Docker resources on target hosts. Only removes resources that are explicitly enabled in the configuration, keeping a conservative default.",
"tags": [
"docker",
"cleanup",
"prune",
"maintenance"
],
"icon": {
"provider": "mdi",
"id": "broom",
"color": "fuchsia"
},
"draft": false,
"version": {
"name": "1",
"source_dep_name": "manual/docker-cleanup"
}
},
"variables": [
{
"title": "Prune Options",
"name": "prune",
"items": [
{
"name": "prune_include_unused_tagged_images",
"type": "bool",
"title": "Tagged Images",
"required": false,
"default": false
},
{
"name": "prune_stopped_containers",
"type": "bool",
"title": "Prune Stopped Containers",
"required": false,
"default": false
},
{
"name": "prune_unused_volumes",
"type": "bool",
"title": "Prune Unused Volumes",
"required": false,
"default": false
},
{
"name": "prune_unused_networks",
"type": "bool",
"title": "Prune Unused Networks",
"required": false,
"default": false
},
{
"name": "prune_builder_cache",
"type": "bool",
"title": "Prune Builder Cache",
"required": false,
"default": false
},
{
"name": "prune_keep_storage_mb",
"type": "int",
"title": "Keep Storage",
"required": false,
"default": 1024,
"description": "Minimum amount of cache/storage to keep when pruning (where supported)",
"config": {
"slider": true,
"min": 256,
"max": 102400,
"step": 256,
"placeholder": "1024",
"unit": "MB"
}
}
]
}
]
}
@@ -1,72 +0,0 @@
---
- name: Docker disk usage report
hosts: all
become: true
gather_facts: false
vars:
report_root: "/opt/docker-reports"
report_timestamp: "{{ lookup('pipe', 'date +%Y%m%d-%H%M%S') }}"
report_file_name: "docker_disk_usage_<< inventory_hostname >>_<< report_timestamp >>.txt"
tasks:
- name: Ensure report root exists
ansible.builtin.file:
path: "<< report_root >>"
state: directory
mode: "0750"
- name: Collect docker system df summary
ansible.builtin.command: docker system df
register: docker_df_summary
changed_when: false
- name: Collect docker system df verbose report
ansible.builtin.command: docker system df -v
register: docker_df_verbose
changed_when: false
when: include_verbose_report | default(false)
- name: Build report body
ansible.builtin.set_fact:
docker_disk_usage_report_body: |
Host: << inventory_hostname >>
Timestamp: {{ lookup('pipe', 'date -u +%Y-%m-%dT%H:%M:%SZ') }}
Docker system df:
<< docker_df_summary.stdout >>
<%- if include_verbose_report | default(false) %>
Docker system df -v:
<< docker_df_verbose.stdout >>
<%- endif %>
- name: Save local disk usage report
ansible.builtin.copy:
dest: "<< report_root >>/<< report_file_name >>"
content: "<< docker_disk_usage_report_body >>"
mode: "0640"
- name: Build Discord report content
ansible.builtin.set_fact:
discord_disk_report_content: |
<< discord_message_prefix | default('Docker Disk Usage Report') >>
```text
<< docker_disk_usage_report_body | truncate(1700, true, '...') >>
```
when:
- discord_enabled | default(false)
- (discord_webhook_url | default('')) | length > 0
- name: Send disk usage report to Discord webhook
ansible.builtin.uri:
url: "<< discord_webhook_url | default('') >>"
method: POST
body_format: json
body:
username: "<< discord_username | default('Docker Reporter') >>"
content: "<< discord_disk_report_content >>"
status_code: 204
when:
- discord_enabled | default(false)
- (discord_webhook_url | default('')) | length > 0
@@ -1,148 +0,0 @@
{
"slug": "docker-disk-usage-report",
"kind": "ansible",
"metadata": {
"name": "Docker Disk Usage Report",
"description": "Generates a Docker disk usage report on target hosts and optionally sends a summary to Discord via webhook.",
"tags": [
"docker",
"report",
"disk",
"monitoring"
],
"icon": {
"provider": "mdi",
"id": "chart-box",
"color": "blue"
},
"draft": true,
"version": {
"name": "1",
"source_dep_name": "manual/docker-disk-usage-report"
}
},
"variables": [
{
"title": "Report",
"name": "report",
"items": [
{
"name": "include_verbose_report",
"type": "bool",
"title": "Verbose Report",
"required": false,
"default": false,
"description": "Include docker system df -v output in local report"
}
]
},
{
"title": "Discord Webhook",
"name": "discord",
"items": [
{
"name": "discord_enabled",
"type": "bool",
"title": "Discord",
"required": false,
"default": false,
"description": "Send a short report to Discord webhook"
},
{
"name": "discord_webhook_url",
"type": "str",
"title": "Discord Webhook URL",
"required": false,
"default": ""
},
{
"name": "discord_username",
"type": "str",
"title": "Discord Username",
"required": false,
"default": "Docker Reporter",
"description": "Sender name for webhook message",
"config": {
"placeholder": "Docker Reporter"
}
},
{
"name": "discord_message_prefix",
"type": "str",
"title": "Discord Message Prefix",
"required": false,
"default": "Docker Disk Usage Report",
"description": "Prefix line sent before report content",
"config": {
"placeholder": "Docker Disk Usage Report"
}
}
]
},
{
"title": "Internal",
"name": "internal",
"items": [
{
"name": "docker_df_verbose",
"type": "str",
"title": "Docker DF Verbose",
"required": false,
"default": ""
},
{
"name": "report_file_name",
"type": "str",
"title": "Report File Name",
"required": false,
"default": ""
},
{
"name": "report_root",
"type": "str",
"title": "Report Root",
"required": false,
"default": "/reports",
"config": {
"placeholder": "/reports"
}
},
{
"name": "docker_disk_usage_report_body",
"type": "str",
"title": "Report Body",
"required": false,
"default": ""
},
{
"name": "discord_disk_report_content",
"type": "str",
"title": "Discord Report",
"required": false,
"default": ""
},
{
"name": "inventory_hostname",
"type": "str",
"title": "Inventory Hostname",
"required": false,
"default": ""
},
{
"name": "report_timestamp",
"type": "str",
"title": "Report Timestamp",
"required": false,
"default": ""
},
{
"name": "docker_df_summary",
"type": "str",
"title": "Docker DF Summary",
"required": false,
"default": ""
}
]
}
]
}
@@ -1,51 +0,0 @@
---
- name: Docker healthcheck audit
hosts: all
become: true
gather_facts: false
tasks:
- name: Collect Docker containers
community.docker.docker_host_info:
containers: true
register: docker_host_info
- name: Build list of containers missing healthcheck
ansible.builtin.set_fact:
containers_missing_healthcheck: "{{ (containers_missing_healthcheck | default([])) + [item.Names[0] | default(item.Id)] }}"
when:
- item.Config is defined
- item.Config.Healthcheck is not defined
loop: "{{ docker_host_info.containers | default([]) }}"
- name: Ensure missing-healthcheck list exists
ansible.builtin.set_fact:
containers_missing_healthcheck: []
when: containers_missing_healthcheck is not defined
- name: Build audit report
ansible.builtin.set_fact:
docker_healthcheck_audit_report:
checked_containers: "{{ docker_host_info.containers | default([]) | length }}"
missing_healthcheck_count: "{{ containers_missing_healthcheck | length }}"
missing_healthcheck_containers: "{{ containers_missing_healthcheck }}"
- name: Save healthcheck audit report
ansible.builtin.copy:
dest: "<< audit_report_path | default('/tmp/docker-healthcheck-audit.json') >>"
content: "{{ docker_healthcheck_audit_report | to_nice_json }}"
mode: "0640"
when: audit_save_report | default(true)
- name: Print audit summary
ansible.builtin.debug:
msg: >-
Checked {{ docker_healthcheck_audit_report.checked_containers }} containers,
found {{ docker_healthcheck_audit_report.missing_healthcheck_count }} without healthcheck.
- name: Fail when missing healthchecks are found
ansible.builtin.fail:
msg: "Containers missing healthcheck: {{ containers_missing_healthcheck | join(', ') }}"
when:
- audit_fail_on_missing | default(false)
- containers_missing_healthcheck | length > 0
@@ -1,2 +0,0 @@
collections:
- name: community.docker
@@ -1,86 +0,0 @@
{
"slug": "docker-healthcheck-audit",
"kind": "ansible",
"metadata": {
"name": "Docker Healthcheck Audit",
"description": "Audits Docker containers for missing healthchecks and optionally fails the playbook when violations are found. Can save a JSON audit report to the target host.",
"tags": [
"docker",
"audit",
"healthcheck",
"compliance"
],
"icon": {
"provider": "mdi",
"id": "stethoscope",
"color": "blue"
},
"draft": false,
"version": {
"name": "1",
"source_dep_name": "manual/docker-healthcheck-audit"
}
},
"variables": [
{
"title": "Audit",
"name": "audit",
"items": [
{
"name": "audit_fail_on_missing",
"type": "bool",
"title": "Fail Missing Checks",
"required": false,
"default": false,
"description": "Fail the play if one or more containers are missing a healthcheck"
},
{
"name": "audit_save_report",
"type": "bool",
"title": "Save JSON Report",
"required": false,
"default": true,
"description": "Save audit report to a JSON file on the target host"
},
{
"name": "audit_report_path",
"type": "str",
"title": "Audit Report Path",
"required": false,
"default": "/tmp/docker-healthcheck-audit.json",
"description": "Path to store the generated JSON report",
"config": {
"placeholder": "/tmp/docker-healthcheck-audit.json"
}
}
]
},
{
"title": "Internal",
"name": "internal",
"items": [
{
"name": "containers_missing_healthcheck",
"type": "str",
"title": "Containers Missing Healthcheck",
"required": false,
"default": ""
},
{
"name": "docker_healthcheck_audit_report",
"type": "str",
"title": "Healthcheck Audit Report",
"required": false,
"default": ""
},
{
"name": "docker_host_info",
"type": "str",
"title": "Docker Host Info",
"required": false,
"default": ""
}
]
}
]
}
@@ -1,84 +0,0 @@
---
- name: Install Docker on Ubuntu
hosts: all
become: true
gather_facts: true
vars:
docker_apt_arch_map:
x86_64: amd64
aarch64: arm64
armv7l: armhf
ppc64le: ppc64el
s390x: s390x
docker_apt_arch: "{{ docker_apt_arch_map[ansible_architecture] | default('amd64') }}"
tasks:
- name: Remove conflicting packages
ansible.builtin.apt:
name:
- docker.io
- docker-compose
- docker-compose-v2
- docker-doc
- podman-docker
- containerd
- runc
state: absent
- name: Install prerequisite packages
ansible.builtin.apt:
name:
- ca-certificates
- curl
update_cache: true
state: present
- name: Create apt keyrings directory
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
mode: "0755"
- name: Download Docker GPG key
ansible.builtin.get_url:
url: https://download.docker.com/linux/ubuntu/gpg
dest: /etc/apt/keyrings/docker.asc
mode: "0644"
force: true
- name: Add Docker apt repository list file
ansible.builtin.copy:
dest: /etc/apt/sources.list.d/docker.list
mode: "0644"
content: |
deb [arch={{ docker_apt_arch }} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable
- name: Install Docker Engine packages
ansible.builtin.apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
update_cache: true
state: present
- name: Ensure Docker service is enabled and started
ansible.builtin.systemd:
name: docker
enabled: true
state: started
<%- if add_user_to_docker == true %>
- name: Add user to docker group
ansible.builtin.user:
name: "<< user >>"
groups: docker
append: true
<%- endif %>
- name: Verify Docker CLI is available
ansible.builtin.command: docker --version
changed_when: false
@@ -1,46 +0,0 @@
{
"slug": "docker-install-ubuntu",
"kind": "ansible",
"metadata": {
"name": "Install Docker on Ubuntu",
"description": "Installs Docker Engine on Ubuntu systems using the official Docker APT repository.",
"tags": [],
"icon": {
"provider": "simple-icons",
"id": "docker",
"color": "blue"
},
"draft": false,
"version": {
"name": "1",
"source_dep_name": "manual/docker-install-ubuntu"
}
},
"variables": [
{
"title": "User Access",
"name": "postinstall",
"items": [
{
"name": "add_user_to_docker",
"type": "bool",
"title": "Docker Group",
"required": false,
"default": false,
"description": "Add a user to the docker group"
},
{
"name": "user",
"type": "str",
"title": "User",
"required": false,
"default": "",
"description": "Username to add to docker group",
"needs": [
"add_user_to_docker=true"
]
}
]
}
]
}
@@ -1,158 +0,0 @@
---
- name: Docker mysql backup
hosts: all
become: true
gather_facts: false
vars:
backup_root: "/opt/docker-db-backups/mysql"
backup_timestamp: "{{ lookup('pipe', 'date +%Y%m%d-%H%M%S') }}"
backup_file_name: "backup_mysql_<< container_name >>_<< backup_timestamp >>.sql.gz"
backup_file_path: "<< backup_root >>/<< backup_file_name >>"
tasks:
- name: Ensure backup root exists
ansible.builtin.file:
path: "<< backup_root >>"
state: directory
mode: "0750"
- name: Get container information
community.docker.docker_container_info:
name: "<< container_name >>"
register: container_info
- name: Fail if container does not exist
ansible.builtin.fail:
msg: "Container '<< container_name >>' does not exist"
when: not container_info.exists
- name: Run backup and validation
block:
- name: Dump all databases with password
ansible.builtin.shell: |
set -euo pipefail
docker exec -e MYSQL_PWD=<< mysql_password | quote >> "<< container_name >>" \
mysqldump --single-transaction --routines --events --triggers \
--all-databases -u "<< mysql_user | default('root') >>" \
| gzip > "<< backup_file_path >>"
args:
executable: /bin/bash
changed_when: true
when:
- mysql_database | default('all') == 'all'
- (mysql_password | default('')) | length > 0
- name: Dump all databases without password
ansible.builtin.shell: |
set -euo pipefail
docker exec "<< container_name >>" \
mysqldump --single-transaction --routines --events --triggers \
--all-databases -u "<< mysql_user | default('root') >>" \
| gzip > "<< backup_file_path >>"
args:
executable: /bin/bash
changed_when: true
when:
- mysql_database | default('all') == 'all'
- (mysql_password | default('')) | length == 0
- name: Dump selected database with password
ansible.builtin.shell: |
set -euo pipefail
docker exec -e MYSQL_PWD=<< mysql_password | quote >> "<< container_name >>" \
mysqldump --single-transaction --routines --events --triggers \
-u "<< mysql_user | default('root') >>" "<< mysql_database >>" \
| gzip > "<< backup_file_path >>"
args:
executable: /bin/bash
changed_when: true
when:
- mysql_database | default('all') != 'all'
- (mysql_password | default('')) | length > 0
- name: Dump selected database without password
ansible.builtin.shell: |
set -euo pipefail
docker exec "<< container_name >>" \
mysqldump --single-transaction --routines --events --triggers \
-u "<< mysql_user | default('root') >>" "<< mysql_database >>" \
| gzip > "<< backup_file_path >>"
args:
executable: /bin/bash
changed_when: true
when:
- mysql_database | default('all') != 'all'
- (mysql_password | default('')) | length == 0
- name: Validate backup file exists
ansible.builtin.stat:
path: "<< backup_file_path >>"
register: backup_archive_stat
- name: Fail if backup file is missing or empty
ansible.builtin.fail:
msg: "Backup file missing or empty: << backup_file_path >>"
when:
- not (backup_archive_stat.stat.exists | default(false))
or (backup_archive_stat.stat.size | default(0) | int) == 0
- name: Validate gzip archive integrity
ansible.builtin.command: gzip -t "<< backup_file_path >>"
changed_when: false
- name: Validate mysql dump signature
ansible.builtin.shell: |
set -euo pipefail
gzip -dc "<< backup_file_path >>" \
| head -n 50 \
| grep -Eq 'MySQL dump'
args:
executable: /bin/bash
changed_when: false
- name: Remove old mysql backup archives
ansible.builtin.find:
paths: "<< backup_root >>"
patterns: "backup_mysql_<< container_name >>_*.sql.gz"
age: "<< backup_retention_days | default(14) >>d"
recurse: true
register: old_backups
- name: Delete old mysql backup archives
ansible.builtin.file:
path: "<< item.path >>"
state: absent
loop: "<< old_backups.files >>"
- name: Mark backup status success
ansible.builtin.set_fact:
backup_status: "success"
backup_status_message: "Backup and validation succeeded"
rescue:
- name: Mark backup status failed
ansible.builtin.set_fact:
backup_status: "failed"
backup_status_message: "<< ansible_failed_result.msg | default('Backup or validation failed') >>"
always:
- name: Send mysql backup status to Discord
ansible.builtin.uri:
url: "<< discord_webhook | default('') >>"
method: POST
body_format: json
body:
content: |
MySQL Backup << backup_status | default('failed') | upper >>
Host: << inventory_hostname >>
Container: << container_name >>
File: << backup_file_path >>
Message: << backup_status_message | default('n/a') >>
status_code: 204
when:
- send_discord_notification | default(false)
- (discord_webhook | default('')) | length > 0
- name: Fail play if backup failed
ansible.builtin.fail:
msg: "<< backup_status_message | default('Backup failed') >>"
when: backup_status | default('failed') == 'failed'
@@ -1,2 +0,0 @@
collections:
- name: community.docker
@@ -1,186 +0,0 @@
{
"slug": "docker-mysql-backup",
"kind": "ansible",
"metadata": {
"name": "Docker MySQL Backup",
"description": "Connects to a named MySQL container and creates compressed SQL dumps with configurable retention. Supports optional Discord notifications on success or failure.",
"tags": [
"docker",
"backup",
"mysql",
"database"
],
"icon": {
"provider": "simple-icons",
"id": "mysql",
"color": "emerald"
},
"draft": true,
"version": {
"name": "1",
"source_dep_name": "manual/docker-mysql-backup"
}
},
"variables": [
{
"title": "Backup",
"name": "backup",
"items": [
{
"name": "container_name",
"type": "str",
"title": "Container Name",
"required": true
},
{
"name": "mysql_user",
"type": "str",
"title": "MySQL User",
"required": false,
"default": "root",
"config": {
"placeholder": "root"
}
},
{
"name": "mysql_password",
"type": "str",
"title": "MySQL Password",
"required": false,
"default": ""
},
{
"name": "mysql_database",
"type": "str",
"title": "Database Name",
"required": false,
"default": "all",
"description": "Database to dump, or 'all' for all databases",
"config": {
"placeholder": "all"
}
},
{
"name": "backup_retention_days",
"type": "int",
"title": "Retention Days",
"required": false,
"default": 14,
"description": "Delete backup archives older than this many days",
"config": {
"slider": true,
"min": 1,
"max": 365,
"step": 1,
"placeholder": "14",
"unit": "days"
}
}
]
},
{
"title": "Notification",
"name": "notification",
"items": [
{
"name": "send_discord_notification",
"type": "bool",
"title": "Send Discord Notification",
"required": false,
"default": false,
"description": "Send success/failure status to Discord webhook"
},
{
"name": "discord_webhook",
"type": "secret",
"title": "Discord Webhook",
"required": false,
"default": "",
"description": "Discord webhook URL for notifications"
}
]
},
{
"title": "Internal",
"name": "internal",
"items": [
{
"name": "backup_file_name",
"type": "str",
"title": "Backup File Name",
"required": false,
"default": ""
},
{
"name": "backup_root",
"type": "str",
"title": "Backup Root",
"required": false,
"default": "/backups/mysql",
"config": {
"placeholder": "/backups/mysql"
}
},
{
"name": "ansible_failed_result",
"type": "str",
"title": "Failed Result",
"required": false,
"default": ""
},
{
"name": "backup_timestamp",
"type": "str",
"title": "Backup Timestamp",
"required": false,
"default": ""
},
{
"name": "old_backups",
"type": "str",
"title": "Old Backups",
"required": false,
"default": ""
},
{
"name": "backup_status",
"type": "str",
"title": "Backup Status",
"required": false,
"default": ""
},
{
"name": "inventory_hostname",
"type": "str",
"title": "Inventory Hostname",
"required": false,
"default": ""
},
{
"name": "backup_file_path",
"type": "str",
"title": "Backup File Path",
"required": false,
"default": ""
},
{
"name": "item",
"type": "str",
"title": "Loop Item",
"required": false,
"default": ""
},
{
"name": "backup_status_message",
"type": "str",
"title": "Backup Status Message",
"required": false,
"default": "",
"config": {
"textarea": true
}
}
]
}
]
}
@@ -1,93 +0,0 @@
---
- name: Docker orphan detection
hosts: all
become: true
gather_facts: false
vars:
report_root: "/opt/docker-reports"
report_timestamp: "{{ lookup('pipe', 'date +%Y%m%d-%H%M%S') }}"
report_file_name: "docker_orphan_detection_<< inventory_hostname >>_<< report_timestamp >>.txt"
tasks:
- name: Ensure report root exists
ansible.builtin.file:
path: "<< report_root >>"
state: directory
mode: "0750"
- name: List exited containers
ansible.builtin.command: docker ps -a --filter status=exited --format "{{.Names}}"
register: orphan_exited_containers
changed_when: false
- name: List dead containers
ansible.builtin.command: docker ps -a --filter status=dead --format "{{.Names}}"
register: orphan_dead_containers
changed_when: false
- name: List dangling images
ansible.builtin.command: docker images -f dangling=true -q
register: orphan_dangling_images
changed_when: false
- name: List dangling volumes
ansible.builtin.command: docker volume ls -qf dangling=true
register: orphan_dangling_volumes
changed_when: false
- name: List dangling networks
ansible.builtin.command: docker network ls -qf dangling=true
register: orphan_dangling_networks
changed_when: false
- name: Build orphan report body
ansible.builtin.set_fact:
docker_orphan_report_body: |
Host: << inventory_hostname >>
Timestamp: {{ lookup('pipe', 'date -u +%Y-%m-%dT%H:%M:%SZ') }}
Exited containers (<< orphan_exited_containers.stdout_lines | length >>):
<< (orphan_exited_containers.stdout_lines | default([])) | join('\n') if (orphan_exited_containers.stdout_lines | length > 0) else 'none' >>
Dead containers (<< orphan_dead_containers.stdout_lines | length >>):
<< (orphan_dead_containers.stdout_lines | default([])) | join('\n') if (orphan_dead_containers.stdout_lines | length > 0) else 'none' >>
Dangling images (<< orphan_dangling_images.stdout_lines | length >>):
<< (orphan_dangling_images.stdout_lines | default([])) | join('\n') if (orphan_dangling_images.stdout_lines | length > 0) else 'none' >>
Dangling volumes (<< orphan_dangling_volumes.stdout_lines | length >>):
<< (orphan_dangling_volumes.stdout_lines | default([])) | join('\n') if (orphan_dangling_volumes.stdout_lines | length > 0) else 'none' >>
Dangling networks (<< orphan_dangling_networks.stdout_lines | length >>):
<< (orphan_dangling_networks.stdout_lines | default([])) | join('\n') if (orphan_dangling_networks.stdout_lines | length > 0) else 'none' >>
- name: Save local orphan detection report
ansible.builtin.copy:
dest: "<< report_root >>/<< report_file_name >>"
content: "<< docker_orphan_report_body >>"
mode: "0640"
- name: Build Discord report content
ansible.builtin.set_fact:
discord_orphan_report_content: |
<< discord_message_prefix | default('Docker Orphan Detection Report') >>
```text
<< docker_orphan_report_body | truncate(1700, true, '...') >>
```
when:
- discord_enabled | default(false)
- (discord_webhook_url | default('')) | length > 0
- name: Send orphan report to Discord webhook
ansible.builtin.uri:
url: "<< discord_webhook_url | default('') >>"
method: POST
body_format: json
body:
username: "<< discord_username | default('Docker Reporter') >>"
content: "<< discord_orphan_report_content >>"
status_code: 204
when:
- discord_enabled | default(false)
- (discord_webhook_url | default('')) | length > 0
@@ -1,155 +0,0 @@
{
"slug": "docker-orphan-detection",
"kind": "ansible",
"metadata": {
"name": "Docker Orphan Detection",
"description": "Detects orphaned Docker resources (dead/exited containers, dangling images, volumes, and networks) and optionally sends a report to Discord via webhook.",
"tags": [
"docker",
"audit",
"orphan",
"report"
],
"icon": {
"provider": "mdi",
"id": "magnify",
"color": "fuchsia"
},
"draft": true,
"version": {
"name": "1",
"source_dep_name": "manual/docker-orphan-detection"
}
},
"variables": [
{
"title": "Discord Webhook",
"name": "discord",
"items": [
{
"name": "discord_enabled",
"type": "bool",
"title": "Discord",
"required": false,
"default": false,
"description": "Send orphan detection summary to Discord webhook"
},
{
"name": "discord_webhook_url",
"type": "str",
"title": "Discord Webhook URL",
"required": false,
"default": ""
},
{
"name": "discord_username",
"type": "str",
"title": "Discord Username",
"required": false,
"default": "Docker Reporter",
"description": "Sender name for webhook message",
"config": {
"placeholder": "Docker Reporter"
}
},
{
"name": "discord_message_prefix",
"type": "str",
"title": "Discord Message Prefix",
"required": false,
"default": "Docker Orphan Detection Report",
"description": "Prefix line sent before report content",
"config": {
"placeholder": "Docker Orphan Detection Report"
}
}
]
},
{
"title": "Internal",
"name": "internal",
"items": [
{
"name": "report_root",
"type": "str",
"title": "Report Root",
"required": false,
"default": "/reports",
"config": {
"placeholder": "/reports"
}
},
{
"name": "orphan_dead_containers",
"type": "str",
"title": "Orphan Dead Containers",
"required": false,
"default": ""
},
{
"name": "orphan_dangling_images",
"type": "str",
"title": "Orphan Dangling Images",
"required": false,
"default": ""
},
{
"name": "orphan_dangling_volumes",
"type": "str",
"title": "Orphan Dangling Volumes",
"required": false,
"default": ""
},
{
"name": "docker_orphan_report_body",
"type": "str",
"title": "Orphan Report Body",
"required": false,
"default": ""
},
{
"name": "report_timestamp",
"type": "str",
"title": "Report Timestamp",
"required": false,
"default": ""
},
{
"name": "orphan_exited_containers",
"type": "str",
"title": "Orphan Exited Containers",
"required": false,
"default": ""
},
{
"name": "orphan_dangling_networks",
"type": "str",
"title": "Orphan Dangling Networks",
"required": false,
"default": ""
},
{
"name": "report_file_name",
"type": "str",
"title": "Report File Name",
"required": false,
"default": ""
},
{
"name": "discord_orphan_report_content",
"type": "str",
"title": "Orphan Report",
"required": false,
"default": ""
},
{
"name": "inventory_hostname",
"type": "str",
"title": "Inventory Hostname",
"required": false,
"default": ""
}
]
}
]
}
@@ -1,156 +0,0 @@
---
- name: Docker postgres backup
hosts: all
become: true
gather_facts: false
vars:
backup_root: "/opt/docker-db-backups/postgres"
backup_timestamp: "{{ lookup('pipe', 'date +%Y%m%d-%H%M%S') }}"
backup_file_name: "backup_postgres_<< container_name >>_<< backup_timestamp >>.sql.gz"
backup_file_path: "<< backup_root >>/<< backup_file_name >>"
tasks:
- name: Ensure backup root exists
ansible.builtin.file:
path: "<< backup_root >>"
state: directory
mode: "0750"
- name: Get container information
community.docker.docker_container_info:
name: "<< container_name >>"
register: container_info
- name: Fail if container does not exist
ansible.builtin.fail:
msg: "Container '<< container_name >>' does not exist"
when: not container_info.exists
- name: Run backup and validation
block:
- name: Dump all databases with password
ansible.builtin.shell: |
set -euo pipefail
docker exec -e PGPASSWORD=<< postgres_password | quote >> "<< container_name >>" \
pg_dumpall -U "<< postgres_user | default('postgres') >>" \
| gzip > "<< backup_file_path >>"
args:
executable: /bin/bash
changed_when: true
when:
- postgres_database | default('all') == 'all'
- (postgres_password | default('')) | length > 0
- name: Dump all databases without password
ansible.builtin.shell: |
set -euo pipefail
docker exec "<< container_name >>" \
pg_dumpall -U "<< postgres_user | default('postgres') >>" \
| gzip > "<< backup_file_path >>"
args:
executable: /bin/bash
changed_when: true
when:
- postgres_database | default('all') == 'all'
- (postgres_password | default('')) | length == 0
- name: Dump selected database with password
ansible.builtin.shell: |
set -euo pipefail
docker exec -e PGPASSWORD=<< postgres_password | quote >> "<< container_name >>" \
pg_dump -U "<< postgres_user | default('postgres') >>" \
"<< postgres_database >>" \
| gzip > "<< backup_file_path >>"
args:
executable: /bin/bash
changed_when: true
when:
- postgres_database | default('all') != 'all'
- (postgres_password | default('')) | length > 0
- name: Dump selected database without password
ansible.builtin.shell: |
set -euo pipefail
docker exec "<< container_name >>" \
pg_dump -U "<< postgres_user | default('postgres') >>" \
"<< postgres_database >>" \
| gzip > "<< backup_file_path >>"
args:
executable: /bin/bash
changed_when: true
when:
- postgres_database | default('all') != 'all'
- (postgres_password | default('')) | length == 0
- name: Validate backup file exists
ansible.builtin.stat:
path: "<< backup_file_path >>"
register: backup_archive_stat
- name: Fail if backup file is missing or empty
ansible.builtin.fail:
msg: "Backup file missing or empty: << backup_file_path >>"
when:
- not (backup_archive_stat.stat.exists | default(false))
or (backup_archive_stat.stat.size | default(0) | int) == 0
- name: Validate gzip archive integrity
ansible.builtin.command: gzip -t "<< backup_file_path >>"
changed_when: false
- name: Validate postgres dump signature
ansible.builtin.shell: |
set -euo pipefail
gzip -dc "<< backup_file_path >>" \
| head -n 50 \
| grep -Eq 'PostgreSQL database dump'
args:
executable: /bin/bash
changed_when: false
- name: Remove old postgres backup archives
ansible.builtin.find:
paths: "<< backup_root >>"
patterns: "backup_postgres_<< container_name >>_*.sql.gz"
age: "<< backup_retention_days | default(14) >>d"
recurse: true
register: old_backups
- name: Delete old postgres backup archives
ansible.builtin.file:
path: "<< item.path >>"
state: absent
loop: "<< old_backups.files >>"
- name: Mark backup status success
ansible.builtin.set_fact:
backup_status: "success"
backup_status_message: "Backup and validation succeeded"
rescue:
- name: Mark backup status failed
ansible.builtin.set_fact:
backup_status: "failed"
backup_status_message: "<< ansible_failed_result.msg | default('Backup or validation failed') >>"
always:
- name: Send postgres backup status to Discord
ansible.builtin.uri:
url: "<< discord_webhook | default('') >>"
method: POST
body_format: json
body:
content: |
PostgreSQL Backup << backup_status | default('failed') | upper >>
Host: << inventory_hostname >>
Container: << container_name >>
File: << backup_file_path >>
Message: << backup_status_message | default('n/a') >>
status_code: 204
when:
- send_discord_notification | default(false)
- (discord_webhook | default('')) | length > 0
- name: Fail play if backup failed
ansible.builtin.fail:
msg: "<< backup_status_message | default('Backup failed') >>"
when: backup_status | default('failed') == 'failed'
@@ -1,2 +0,0 @@
collections:
- name: community.docker
@@ -1,187 +0,0 @@
{
"slug": "docker-postgres-backup",
"kind": "ansible",
"metadata": {
"name": "Docker Postgres Backup",
"description": "Connects to a named PostgreSQL container and creates compressed SQL dumps with configurable retention. Supports optional Discord notifications on success or failure.",
"tags": [
"docker",
"backup",
"postgres",
"database"
],
"icon": {
"provider": "simple-icons",
"id": "postgresql",
"color": "emerald"
},
"draft": true,
"version": {
"name": "1",
"source_dep_name": "manual/docker-postgres-backup"
}
},
"variables": [
{
"title": "Backup",
"name": "backup",
"items": [
{
"name": "container_name",
"type": "str",
"title": "Container Name",
"required": true
},
{
"name": "postgres_user",
"type": "str",
"title": "Postgres User",
"required": false,
"default": "postgres",
"description": "Database user for pg_dump/pg_dumpall",
"config": {
"placeholder": "postgres"
}
},
{
"name": "postgres_password",
"type": "str",
"title": "Postgres Password",
"required": false,
"default": ""
},
{
"name": "postgres_database",
"type": "str",
"title": "Database Name",
"required": false,
"default": "all",
"description": "Database to dump, or 'all' for all databases",
"config": {
"placeholder": "all"
}
},
{
"name": "backup_retention_days",
"type": "int",
"title": "Retention Days",
"required": false,
"default": 14,
"description": "Delete backup archives older than this many days",
"config": {
"slider": true,
"min": 1,
"max": 365,
"step": 1,
"placeholder": "14",
"unit": "days"
}
}
]
},
{
"title": "Notification",
"name": "notification",
"items": [
{
"name": "send_discord_notification",
"type": "bool",
"title": "Send Discord Notification",
"required": false,
"default": false,
"description": "Send success/failure status to Discord webhook"
},
{
"name": "discord_webhook",
"type": "secret",
"title": "Discord Webhook",
"required": false,
"default": "",
"description": "Discord webhook URL for notifications"
}
]
},
{
"title": "Internal",
"name": "internal",
"items": [
{
"name": "backup_status",
"type": "str",
"title": "Backup Status",
"required": false,
"default": ""
},
{
"name": "inventory_hostname",
"type": "str",
"title": "Inventory Hostname",
"required": false,
"default": ""
},
{
"name": "backup_timestamp",
"type": "str",
"title": "Backup Timestamp",
"required": false,
"default": ""
},
{
"name": "item",
"type": "str",
"title": "Loop Item",
"required": false,
"default": ""
},
{
"name": "backup_file_path",
"type": "str",
"title": "Backup File Path",
"required": false,
"default": ""
},
{
"name": "old_backups",
"type": "str",
"title": "Old Backups",
"required": false,
"default": ""
},
{
"name": "backup_root",
"type": "str",
"title": "Backup Root",
"required": false,
"default": "/backups/postgres",
"config": {
"placeholder": "/backups/postgres"
}
},
{
"name": "backup_file_name",
"type": "str",
"title": "Backup File Name",
"required": false,
"default": ""
},
{
"name": "ansible_failed_result",
"type": "str",
"title": "Failed Result",
"required": false,
"default": ""
},
{
"name": "backup_status_message",
"type": "str",
"title": "Backup Status Message",
"required": false,
"default": "",
"config": {
"textarea": true
}
}
]
}
]
}
@@ -1,77 +0,0 @@
---
- name: Docker volume backup
hosts: all
become: true
gather_facts: false
vars:
backup_root: "/opt/docker-volume-backups"
backup_timestamp: "{{ lookup('pipe', 'date +%Y%m%d-%H%M%S') }}"
backup_archive_name: "backup_<< container_name >>_{{ backup_timestamp }}.tar.gz"
tasks:
- name: Ensure backup root exists
ansible.builtin.file:
path: "{{ backup_root }}"
state: directory
mode: "0750"
- name: Get container information
community.docker.docker_container_info:
name: "<< container_name >>"
register: container_info
- name: Fail if container does not exist
ansible.builtin.fail:
msg: "Container '<< container_name >>' does not exist"
when: not container_info.exists
- name: Record container running state
ansible.builtin.set_fact:
container_was_running: "{{ (container_info.container.State.Status | default('')) == 'running' }}"
- name: Stop container for consistent backup
community.docker.docker_container:
name: "<< container_name >>"
state: stopped
when:
- stop_containers_during_backup | default(false)
- container_was_running
- name: Back up container path using all attached volumes
ansible.builtin.shell: |
set -eu
docker run --rm \
--volumes-from "<< container_name >>" \
-v "{{ backup_root }}":/backup \
busybox sh -c '
test -e "<< backup_container_path >>"
tar -czf "/backup/{{ backup_archive_name }}" \
-C / "<< backup_container_path | regex_replace('^/', '') >>"
'
args:
executable: /bin/sh
changed_when: true
- name: Start container after backup
community.docker.docker_container:
name: "<< container_name >>"
state: started
when:
- stop_containers_during_backup | default(false)
- container_was_running
- name: Remove old backup archives
ansible.builtin.find:
paths: "{{ backup_root }}"
patterns: "backup_<< container_name >>_*.tar.gz"
age: "<< backup_retention_days | default(14) >>d"
recurse: true
register: old_backups
- name: Delete old backup archives
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ old_backups.files }}"
@@ -1,2 +0,0 @@
collections:
- name: community.docker
@@ -1,113 +0,0 @@
{
"slug": "docker-volume-backup",
"kind": "ansible",
"metadata": {
"name": "Docker Volume Backup",
"description": "Backs up data from a Docker container path by auto-attaching all container volumes into a temporary backup container. Creates timestamped tar.gz archives with configurable retention.",
"tags": [
"docker",
"backup",
"volume",
"container"
],
"icon": {
"provider": "mdi",
"id": "harddisk",
"color": "emerald"
},
"draft": true,
"version": {
"name": "1",
"source_dep_name": "manual/docker-volume-backup"
}
},
"variables": [
{
"title": "Backup",
"name": "backup",
"items": [
{
"name": "container_name",
"type": "str",
"title": "Container Name",
"required": true
},
{
"name": "backup_retention_days",
"type": "int",
"title": "Retention Days",
"required": false,
"default": 14,
"description": "Delete backup archives older than this many days",
"config": {
"slider": true,
"min": 1,
"max": 365,
"step": 1,
"placeholder": "14",
"unit": "days"
}
},
{
"name": "backup_container_path",
"type": "str",
"title": "Backup Path",
"required": true,
"description": "Path inside the container that should be archived"
},
{
"name": "stop_containers_during_backup",
"type": "bool",
"title": "Stop Backup",
"required": false,
"default": false,
"description": "If enabled, stop the container before backup and start it again afterwards if it was running"
}
]
},
{
"title": "Internal",
"name": "internal",
"items": [
{
"name": "backup_timestamp",
"type": "str",
"title": "Backup Timestamp",
"required": false,
"default": ""
},
{
"name": "backup_root",
"type": "str",
"title": "Backup Root",
"required": false,
"default": "/backups",
"config": {
"placeholder": "/backups"
}
},
{
"name": "backup_archive_name",
"type": "str",
"title": "Backup Archive Name",
"required": false,
"default": ""
},
{
"name": "item",
"type": "str",
"title": "Loop Item",
"required": false,
"default": ""
},
{
"name": "old_backups",
"type": "str",
"title": "Old Backups",
"required": false,
"default": ""
}
]
}
]
}
@@ -1,404 +0,0 @@
---
- name: Configure K3s post-install platform services - Fetch kubeconfig
hosts: all
gather_facts: false
become: true
<%- if secrets_file %>
vars_files:
- << secrets_file >>
<%- endif %>
vars:
kubeconfig_path: "<< kubeconfig_local_path >>"
kubeconfig_server_target: "<< kubeconfig_server_endpoint >>"
tasks:
- name: Validate that the kubeconfig source host exists in inventory
ansible.builtin.assert:
that:
- "'<< kubeconfig_source_host | default('k3s-server-01') >>' in hostvars"
fail_msg: "Inventory host '<< kubeconfig_source_host | default('k3s-server-01') >>' was not found."
run_once: true
- name: Read kubeconfig from the primary K3s server
ansible.builtin.slurp:
src: "<< kubeconfig_source_path | default('/etc/rancher/k3s/k3s.yaml') >>"
delegate_to: "<< kubeconfig_source_host | default('k3s-server-01') >>"
register: k3s_source_kubeconfig
when: fetch_kubeconfig | default(true)
run_once: true
- name: Ensure local kubeconfig directory exists
ansible.builtin.file:
path: "{{ kubeconfig_path | dirname }}"
state: directory
mode: "0700"
delegate_to: localhost
run_once: true
when: fetch_kubeconfig | default(true)
- name: Write kubeconfig to the control machine
ansible.builtin.copy:
dest: "{{ kubeconfig_path }}"
mode: "0600"
content: "{{ (k3s_source_kubeconfig.content | b64decode) | regex_replace('https://127\\.0\\.0\\.1:6443', 'https://' ~ kubeconfig_server_target ~ ':6443') }}"
delegate_to: localhost
run_once: true
when: fetch_kubeconfig | default(true)
- name: Configure K3s post-install platform services - Prepare Longhorn prerequisites
hosts: all
gather_facts: false
become: true
<%- if secrets_file %>
vars_files:
- << secrets_file >>
<%- endif %>
vars:
longhorn_target_hosts: "{{ query('inventory_hostnames', '<< longhorn_node_hosts >>') }}"
tasks:
- name: Validate Longhorn target host pattern
ansible.builtin.assert:
that:
- longhorn_target_hosts | length > 0
fail_msg: "No inventory hosts matched '<< longhorn_node_hosts >>'."
when: longhorn_prerequisites_enabled | default(false)
run_once: true
- name: Install Longhorn prerequisite packages
ansible.builtin.apt:
name:
- open-iscsi
- nfs-common
- cryptsetup
- util-linux
state: present
update_cache: true
when:
- longhorn_prerequisites_enabled | default(false)
- inventory_hostname in longhorn_target_hosts
- name: Ensure iscsid service is enabled and running
ansible.builtin.service:
name: iscsid
state: started
enabled: true
when:
- longhorn_prerequisites_enabled | default(false)
- inventory_hostname in longhorn_target_hosts
- name: Ensure open-iscsi service is enabled and running when present
ansible.builtin.service:
name: open-iscsi
state: started
enabled: true
failed_when: false
when:
- longhorn_prerequisites_enabled | default(false)
- inventory_hostname in longhorn_target_hosts
- name: Persist dm_crypt kernel module for Longhorn
ansible.builtin.copy:
dest: /etc/modules-load.d/longhorn.conf
mode: "0644"
content: |
dm_crypt
when:
- longhorn_prerequisites_enabled | default(false)
- inventory_hostname in longhorn_target_hosts
- name: Load dm_crypt kernel module immediately
community.general.modprobe:
name: dm_crypt
state: present
when:
- longhorn_prerequisites_enabled | default(false)
- inventory_hostname in longhorn_target_hosts
- name: Disable multipathd when the environment does not use multipath storage
ansible.builtin.service:
name: multipathd
state: stopped
enabled: false
failed_when: false
when:
- longhorn_prerequisites_enabled | default(false)
- longhorn_disable_multipathd | default(false)
- inventory_hostname in longhorn_target_hosts
- name: Configure K3s post-install platform services - Configure cluster add-ons
hosts: all
gather_facts: false
<%- if secrets_file %>
vars_files:
- << secrets_file >>
<%- endif %>
vars:
kubeconfig_path: "<< kubeconfig_local_path >>"
tasks:
- block:
- name: Ensure the kubeconfig file exists before applying cluster resources
ansible.builtin.stat:
path: "{{ kubeconfig_path }}"
register: kubeconfig_stat
- name: Fail when the kubeconfig file is missing
ansible.builtin.fail:
msg: "Kubeconfig not found at {{ kubeconfig_path }}. Enable fetch_kubeconfig or provide an existing kubeconfig path."
when: not (kubeconfig_stat.stat.exists | default(false))
- name: Verify Helm is available on the control machine when required
ansible.builtin.command:
cmd: helm version --short
changed_when: false
when: (cert_manager_install_enabled | default(false)) or (metrics_server_install_enabled | default(false)) or (longhorn_install_enabled | default(false))
- name: Validate ACME email when ClusterIssuer creation is enabled
ansible.builtin.assert:
that:
- (cert_manager_acme_email | default('')) | length > 0
fail_msg: "cert_manager_acme_email must be set when cert_manager_clusterissuer_enabled=true."
when: cert_manager_clusterissuer_enabled | default(false)
- name: Build namespace list for enabled platform components
ansible.builtin.set_fact:
managed_namespaces: >-
{{
(
(extra_namespaces_yaml | default('[]') | from_yaml)
+ ([cert_manager_namespace | default('cert-manager')] if cert_manager_install_enabled | default(false) or cert_manager_create_cloudflare_secret | default(false) or cert_manager_clusterissuer_enabled | default(false) else [])
+ ([longhorn_namespace | default('longhorn-system')] if longhorn_install_enabled | default(false) else [])
+ ([traefik_dashboard_namespace | default('traefik')] if traefik_dashboard_exposure_enabled | default(false) else [])
) | unique
}}
when: ensure_namespaces | default(true)
- name: Ensure selected namespaces exist
kubernetes.core.k8s:
kubeconfig: "{{ kubeconfig_path }}"
state: present
definition:
apiVersion: v1
kind: Namespace
metadata:
name: "{{ item }}"
loop: "{{ managed_namespaces | default([]) }}"
loop_control:
label: "{{ item }}"
when:
- ensure_namespaces | default(true)
- item | length > 0
- name: Add cert-manager Helm repository
kubernetes.core.helm_repository:
name: jetstack
repo_url: https://charts.jetstack.io
when: cert_manager_install_enabled | default(false)
- name: Install cert-manager
kubernetes.core.helm:
kubeconfig: "{{ kubeconfig_path }}"
name: cert-manager
chart_ref: jetstack/cert-manager
chart_version: "<< cert_manager_chart_version | default('1.18.2') >>"
release_namespace: "<< cert_manager_namespace | default('cert-manager') >>"
create_namespace: true
wait: true
values:
crds:
enabled: true
when: cert_manager_install_enabled | default(false)
- name: Create Cloudflare API token secret placeholder for cert-manager
kubernetes.core.k8s:
kubeconfig: "{{ kubeconfig_path }}"
state: present
definition:
apiVersion: v1
kind: Secret
metadata:
name: "<< cert_manager_cloudflare_secret_name | default('cloudflare-api-token-secret') >>"
namespace: "<< cert_manager_namespace | default('cert-manager') >>"
type: Opaque
stringData:
<< cert_manager_cloudflare_secret_key | default('api-token') >>: "<< cert_manager_cloudflare_api_token | default('replace-me-with-your-cloudflare-api-token') >>"
when: cert_manager_create_cloudflare_secret | default(false)
- name: Create cert-manager ClusterIssuer
kubernetes.core.k8s:
kubeconfig: "{{ kubeconfig_path }}"
state: present
definition:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: "<< cert_manager_clusterissuer_name | default('letsencrypt-production') >>"
spec:
acme:
email: "<< cert_manager_acme_email >>"
server: "<< cert_manager_acme_server | default('https://acme-v02.api.letsencrypt.org/directory') >>"
privateKeySecretRef:
name: "<< cert_manager_clusterissuer_name | default('letsencrypt-production') >>-account-key"
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: "<< cert_manager_cloudflare_secret_name | default('cloudflare-api-token-secret') >>"
key: "<< cert_manager_cloudflare_secret_key | default('api-token') >>"
when: cert_manager_clusterissuer_enabled | default(false)
- name: Add metrics-server Helm repository
kubernetes.core.helm_repository:
name: metrics-server
repo_url: https://kubernetes-sigs.github.io/metrics-server/
when: metrics_server_install_enabled | default(false)
- name: Install metrics-server
kubernetes.core.helm:
kubeconfig: "{{ kubeconfig_path }}"
name: metrics-server
chart_ref: metrics-server/metrics-server
chart_version: "<< metrics_server_chart_version | default('3.13.0') >>"
release_namespace: "<< metrics_server_namespace | default('kube-system') >>"
create_namespace: true
wait: true
<%- if metrics_server_kubelet_insecure_tls %>
values:
args:
- --kubelet-insecure-tls
<%- endif %>
when: metrics_server_install_enabled | default(false)
- name: Add Longhorn Helm repository
kubernetes.core.helm_repository:
name: longhorn
repo_url: https://charts.longhorn.io
when: longhorn_install_enabled | default(false)
- name: Install Longhorn
kubernetes.core.helm:
kubeconfig: "{{ kubeconfig_path }}"
name: longhorn
chart_ref: longhorn/longhorn
chart_version: "<< longhorn_chart_version | default('1.9.1') >>"
release_namespace: "<< longhorn_namespace | default('longhorn-system') >>"
create_namespace: true
wait: true
values:
defaultSettings:
defaultDataPath: "<< longhorn_default_data_path | default('/var/lib/longhorn') >>"
when: longhorn_install_enabled | default(false)
- name: Expose Traefik dashboard through an IngressRoute
kubernetes.core.k8s:
kubeconfig: "{{ kubeconfig_path }}"
state: present
definition:
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: traefik-dashboard
namespace: "<< traefik_dashboard_namespace | default('traefik') >>"
spec:
entryPoints:
- "<< traefik_dashboard_entrypoint | default('websecure') >>"
routes:
- match: "Host(`<< traefik_dashboard_host | default('traefik.example.internal') >>`)"
kind: Rule
<%- if traefik_dashboard_middleware_name %>
middlewares:
- name: "<< traefik_dashboard_middleware_name >>"
namespace: "<< traefik_dashboard_middleware_namespace | default('traefik') >>"
<%- endif %>
services:
- kind: TraefikService
name: api@internal
<%- if traefik_dashboard_tls_enabled %>
tls:
secretName: "<< traefik_dashboard_tls_secret_name | default('traefik-dashboard-tls') >>"
<%- endif %>
when: traefik_dashboard_exposure_enabled | default(false)
- name: Read kube-proxy ConfigMap before strictARP patch
kubernetes.core.k8s_info:
kubeconfig: "{{ kubeconfig_path }}"
api_version: v1
kind: ConfigMap
namespace: kube-system
name: kube-proxy
register: kube_proxy_configmap
when: kube_proxy_strict_arp_enabled | default(false)
- name: Build kube-proxy configuration with strictARP enabled
ansible.builtin.set_fact:
kube_proxy_rendered_config: >-
{{
(kube_proxy_configmap.resources[0].data['config.conf'] | from_yaml)
| combine(
{
'ipvs': (
(kube_proxy_configmap.resources[0].data['config.conf'] | from_yaml).ipvs
| default({})
| combine({'strictARP': true})
)
},
recursive=True
)
}}
when:
- kube_proxy_strict_arp_enabled | default(false)
- kube_proxy_configmap.resources | length > 0
- name: Patch kube-proxy ConfigMap with strictARP enabled
kubernetes.core.k8s:
kubeconfig: "{{ kubeconfig_path }}"
state: present
merge_type:
- merge
definition:
apiVersion: v1
kind: ConfigMap
metadata:
name: kube-proxy
namespace: kube-system
data:
config.conf: "{{ kube_proxy_rendered_config | to_nice_yaml(indent=2) }}"
when:
- kube_proxy_strict_arp_enabled | default(false)
- kube_proxy_configmap.resources | length > 0
- name: Read kube-proxy daemonset for restart
kubernetes.core.k8s_info:
kubeconfig: "{{ kubeconfig_path }}"
api_version: apps/v1
kind: DaemonSet
namespace: kube-system
name: kube-proxy
register: kube_proxy_daemonset
when: kube_proxy_strict_arp_enabled | default(false)
- name: Restart kube-proxy daemonset after strictARP change
kubernetes.core.k8s:
kubeconfig: "{{ kubeconfig_path }}"
state: present
merge_type:
- merge
definition:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kube-proxy
namespace: kube-system
spec:
template:
metadata:
annotations:
homelab.boilerplates.io/restarted-at: "{{ lookup('pipe', 'date -u +%Y-%m-%dT%H:%M:%SZ') }}"
when:
- kube_proxy_strict_arp_enabled | default(false)
- kube_proxy_daemonset.resources | length > 0
delegate_to: localhost
run_once: true
@@ -1,527 +0,0 @@
{
"slug": "k3s-postinstall",
"kind": "ansible",
"metadata": {
"name": "K3s Post-Install Platform Setup",
"description": "Configures common post-install building blocks for a fresh K3s cluster. Supports kubeconfig retrieval, namespace management, Traefik dashboard exposure, cert-manager bootstrap, metrics-server, Longhorn preparation, and optional kube-system tuning.",
"tags": [
"kubernetes",
"k3s",
"postinstall",
"helm",
"homelab"
],
"icon": {
"provider": "simple-icons",
"id": "kubernetes"
},
"draft": false,
"version": {
"name": "1.0.0",
"source_dep_name": "manual/k3s-postinstall"
}
},
"variables": [
{
"title": "Execution",
"name": "execution",
"items": [
{
"name": "secrets_file",
"type": "str",
"title": "Secrets File",
"required": false,
"default": "",
"description": "Optional vars file to load before running the playbook."
}
]
},
{
"title": "Kubeconfig",
"name": "kubeconfig",
"items": [
{
"name": "fetch_kubeconfig",
"type": "bool",
"title": "Fetch Kubeconfig",
"required": false,
"default": true,
"description": "Fetch the admin kubeconfig from the first K3s server before applying cluster resources"
},
{
"name": "kubeconfig_source_host",
"type": "str",
"title": "Source Host",
"required": false,
"default": "k3s-server-01",
"description": "Inventory host that stores the K3s admin kubeconfig",
"needs": [
"fetch_kubeconfig=true"
],
"config": {
"placeholder": "k3s-server-01"
}
},
{
"name": "kubeconfig_source_path",
"type": "str",
"title": "Source Path",
"required": false,
"default": "/etc/rancher/k3s/k3s.yaml",
"description": "Path to the kubeconfig file on the K3s server",
"needs": [
"fetch_kubeconfig=true"
],
"config": {
"placeholder": "/etc/rancher/k3s/k3s.yaml"
}
},
{
"name": "kubeconfig_server_endpoint",
"type": "str",
"title": "API Endpoint",
"required": false,
"default": "{{ hostvars['k3s-server-01'].ansible_host | default('k3s-server-01') }}",
"description": "Host or IP that should replace 127.0.0.1 in the fetched kubeconfig",
"needs": [
"fetch_kubeconfig=true"
],
"config": {
"placeholder": "192.168.1.10"
}
},
{
"name": "kubeconfig_local_path",
"type": "str",
"title": "Local Kubeconfig",
"required": false,
"default": "{{ lookup('env', 'HOME') }}/.kube/k3s.yaml",
"description": "Local kubeconfig path used by the control machine for all cluster operations",
"config": {
"placeholder": "{{ lookup('env', 'HOME') }}/.kube/k3s.yaml"
}
}
]
},
{
"title": "Namespaces",
"name": "namespaces",
"items": [
{
"name": "ensure_namespaces",
"type": "bool",
"title": "Manage Namespaces",
"required": false,
"default": true,
"description": "Ensure common namespaces exist before installing platform add-ons"
},
{
"name": "extra_namespaces_yaml",
"type": "str",
"title": "Extra Namespaces",
"required": false,
"default": "[]",
"description": "YAML list of extra namespaces to create",
"needs": [
"ensure_namespaces=true"
],
"config": {
"textarea": true,
"placeholder": "- apps\n- monitoring"
}
}
]
},
{
"title": "Traefik",
"name": "traefik",
"items": [
{
"name": "traefik_dashboard_exposure_enabled",
"type": "bool",
"title": "Expose Dashboard",
"required": false,
"default": false,
"description": "Create a Traefik IngressRoute that exposes the internal Traefik dashboard"
},
{
"name": "traefik_dashboard_namespace",
"type": "str",
"title": "Namespace",
"required": false,
"default": "traefik",
"description": "Namespace where the Traefik release is installed",
"needs": [
"traefik_dashboard_exposure_enabled=true"
],
"config": {
"placeholder": "traefik"
}
},
{
"name": "traefik_dashboard_host",
"type": "str",
"title": "Dashboard Host",
"required": false,
"default": "traefik.example.internal",
"description": "FQDN used to expose the Traefik dashboard",
"needs": [
"traefik_dashboard_exposure_enabled=true"
],
"config": {
"placeholder": "traefik.example.internal"
}
},
{
"name": "traefik_dashboard_entrypoint",
"type": "str",
"title": "Entrypoint",
"required": false,
"default": "websecure",
"description": "Traefik entrypoint for the dashboard route",
"needs": [
"traefik_dashboard_exposure_enabled=true"
],
"config": {
"placeholder": "websecure"
}
},
{
"name": "traefik_dashboard_tls_enabled",
"type": "bool",
"title": "TLS",
"required": false,
"default": true,
"needs": [
"traefik_dashboard_exposure_enabled=true"
]
},
{
"name": "traefik_dashboard_tls_secret_name",
"type": "str",
"title": "TLS Secret",
"required": false,
"default": "traefik-dashboard-tls",
"description": "TLS secret used by the dashboard route when TLS is enabled",
"needs": [
"traefik_dashboard_exposure_enabled=true",
"traefik_dashboard_tls_enabled=true"
],
"config": {
"placeholder": "traefik-dashboard-tls"
}
},
{
"name": "traefik_dashboard_middleware_name",
"type": "str",
"title": "Middleware",
"required": false,
"default": "",
"description": "Optional Traefik middleware name attached to the dashboard route",
"needs": [
"traefik_dashboard_exposure_enabled=true"
],
"config": {
"placeholder": "traefik-dashboard-auth"
}
},
{
"name": "traefik_dashboard_middleware_namespace",
"type": "str",
"title": "Middleware NS",
"required": false,
"default": "traefik",
"needs": [
"traefik_dashboard_exposure_enabled=true"
],
"config": {
"placeholder": "traefik"
}
}
]
},
{
"title": "Cert-Manager",
"name": "cert-manager",
"items": [
{
"name": "cert_manager_install_enabled",
"type": "bool",
"title": "Install cert-manager",
"required": false,
"default": false
},
{
"name": "cert_manager_chart_version",
"type": "str",
"title": "Chart Version",
"required": false,
"default": "1.18.2",
"needs": [
"cert_manager_install_enabled=true"
],
"config": {
"placeholder": "1.18.2"
}
},
{
"name": "cert_manager_namespace",
"type": "str",
"title": "Namespace",
"required": false,
"default": "cert-manager",
"config": {
"placeholder": "cert-manager"
}
},
{
"name": "cert_manager_create_cloudflare_secret",
"type": "bool",
"title": "Cloudflare Secret",
"required": false,
"default": false,
"description": "Create a placeholder secret for a Cloudflare DNS API token"
},
{
"name": "cert_manager_cloudflare_secret_name",
"type": "str",
"title": "Secret Name",
"required": false,
"default": "cloudflare-api-token-secret",
"description": "Secret name for the Cloudflare DNS API token",
"needs": [
"cert_manager_create_cloudflare_secret=true"
],
"config": {
"placeholder": "cloudflare-api-token-secret"
}
},
{
"name": "cert_manager_cloudflare_secret_key",
"type": "str",
"title": "Secret Key",
"required": false,
"default": "api-token",
"description": "Secret key that stores the Cloudflare API token",
"needs": [
"cert_manager_create_cloudflare_secret=true"
],
"config": {
"placeholder": "api-token"
}
},
{
"name": "cert_manager_cloudflare_api_token",
"type": "secret",
"title": "Cloudflare Token",
"required": false,
"default": "",
"description": "Optional Cloudflare API token value used to populate the placeholder secret",
"needs": [
"cert_manager_create_cloudflare_secret=true"
]
},
{
"name": "cert_manager_clusterissuer_enabled",
"type": "bool",
"title": "ClusterIssuer",
"required": false,
"default": false,
"description": "Create a production Let's Encrypt ClusterIssuer that uses Cloudflare DNS-01"
},
{
"name": "cert_manager_clusterissuer_name",
"type": "str",
"title": "ClusterIssuer Name",
"required": false,
"default": "letsencrypt-production",
"needs": [
"cert_manager_clusterissuer_enabled=true"
],
"config": {
"placeholder": "letsencrypt-production"
}
},
{
"name": "cert_manager_acme_email",
"type": "str",
"title": "ACME Email",
"required": false,
"default": "",
"description": "Email address used for ACME registration",
"needs": [
"cert_manager_clusterissuer_enabled=true"
],
"config": {
"placeholder": "me@example.com"
}
},
{
"name": "cert_manager_acme_server",
"type": "str",
"title": "ACME Server",
"required": false,
"default": "https://acme-v02.api.letsencrypt.org/directory",
"description": "ACME directory URL used by the ClusterIssuer",
"needs": [
"cert_manager_clusterissuer_enabled=true"
],
"config": {
"placeholder": "https://acme-v02.api.letsencrypt.org/directory"
}
}
]
},
{
"title": "Metrics Server",
"name": "metrics-server",
"items": [
{
"name": "metrics_server_install_enabled",
"type": "bool",
"title": "Install Metrics Server",
"required": false,
"default": false
},
{
"name": "metrics_server_chart_version",
"type": "str",
"title": "Chart Version",
"required": false,
"default": "3.13.0",
"needs": [
"metrics_server_install_enabled=true"
],
"config": {
"placeholder": "3.13.0"
}
},
{
"name": "metrics_server_namespace",
"type": "str",
"title": "Namespace",
"required": false,
"default": "kube-system",
"needs": [
"metrics_server_install_enabled=true"
],
"config": {
"placeholder": "kube-system"
}
},
{
"name": "metrics_server_kubelet_insecure_tls",
"type": "bool",
"title": "Insecure TLS",
"required": false,
"default": true,
"description": "Add --kubelet-insecure-tls for clusters that use self-signed kubelet certificates",
"needs": [
"metrics_server_install_enabled=true"
]
}
]
},
{
"title": "Longhorn",
"name": "longhorn",
"items": [
{
"name": "longhorn_prerequisites_enabled",
"type": "bool",
"title": "Prepare Nodes",
"required": false,
"default": false,
"description": "Install host packages and kernel module configuration needed by Longhorn"
},
{
"name": "longhorn_node_hosts",
"type": "str",
"title": "Node Hosts",
"required": false,
"default": "k3s_cluster",
"description": "Inventory host pattern for the nodes that should be prepared for Longhorn",
"needs": [
"longhorn_prerequisites_enabled=true"
],
"config": {
"placeholder": "k3s_cluster"
}
},
{
"name": "longhorn_disable_multipathd",
"type": "bool",
"title": "Disable multipathd",
"required": false,
"default": false,
"description": "Stop and disable multipathd if your nodes do not use multipath storage",
"needs": [
"longhorn_prerequisites_enabled=true"
]
},
{
"name": "longhorn_install_enabled",
"type": "bool",
"title": "Install Longhorn",
"required": false,
"default": false,
"description": "Install Longhorn with Helm after prerequisites are in place"
},
{
"name": "longhorn_chart_version",
"type": "str",
"title": "Chart Version",
"required": false,
"default": "1.9.1",
"needs": [
"longhorn_install_enabled=true"
],
"config": {
"placeholder": "1.9.1"
}
},
{
"name": "longhorn_namespace",
"type": "str",
"title": "Namespace",
"required": false,
"default": "longhorn-system",
"needs": [
"longhorn_install_enabled=true"
],
"config": {
"placeholder": "longhorn-system"
}
},
{
"name": "longhorn_default_data_path",
"type": "str",
"title": "Data Path",
"required": false,
"default": "/var/lib/longhorn",
"description": "Default host path used by Longhorn for replica data",
"needs": [
"longhorn_install_enabled=true"
],
"config": {
"placeholder": "/var/lib/longhorn"
}
}
]
},
{
"title": "Kube-System",
"name": "kube-system",
"items": [
{
"name": "kube_proxy_strict_arp_enabled",
"type": "bool",
"title": "Enable strictARP",
"required": false,
"default": false,
"description": "Patch the kube-proxy ConfigMap and restart the daemonset with strictARP enabled"
}
]
}
]
}
@@ -1,200 +0,0 @@
---
- name: Bootstrap K3s cluster with k3sup
hosts: all
gather_facts: false
<%- if secrets_file %>
vars_files:
- << secrets_file >>
<%- endif %>
vars:
k3s_server_target: "{{ hostvars['<< k3s_server_host >>'].ansible_host | default('<< k3s_server_host >>') }}"
<%- if kubeconfig_destination_mode == 'remote' %>
k3s_generated_kubeconfig_path: "/tmp/<< k3s_context_name | default('homelab-k3s') >>-kubeconfig"
<%- else %>
k3s_generated_kubeconfig_path: "<< local_kubeconfig_path >>"
<%- endif %>
tasks:
- name: Gather control machine facts
ansible.builtin.setup:
delegate_to: localhost
run_once: true
register: localhost_facts
- name: Validate that the first server host exists in inventory
ansible.builtin.assert:
that:
- "'<< k3s_server_host >>' in hostvars"
fail_msg: "Inventory host '<< k3s_server_host >>' was not found."
run_once: true
- name: Resolve k3sup release asset
ansible.builtin.set_fact:
k3sup_release_asset: >-
{%- if localhost_facts.ansible_facts.ansible_system == 'Linux' and localhost_facts.ansible_facts.ansible_architecture in ['x86_64', 'amd64'] -%}
k3sup
{%- elif localhost_facts.ansible_facts.ansible_system == 'Linux' and localhost_facts.ansible_facts.ansible_architecture in ['aarch64', 'arm64'] -%}
k3sup-arm64
{%- elif localhost_facts.ansible_facts.ansible_system == 'Linux' and localhost_facts.ansible_facts.ansible_architecture in ['armv7l', 'armv6l'] -%}
k3sup-arm
{%- elif localhost_facts.ansible_facts.ansible_system == 'Darwin' and localhost_facts.ansible_facts.ansible_architecture in ['x86_64', 'amd64'] -%}
k3sup-darwin
{%- elif localhost_facts.ansible_facts.ansible_system == 'Darwin' and localhost_facts.ansible_facts.ansible_architecture in ['arm64', 'aarch64'] -%}
k3sup-darwin-arm64
{%- else -%}
unsupported
{%- endif -%}
when: install_k3sup_binary | default(false)
run_once: true
- name: Fail when k3sup binary installation is not supported for this control machine
ansible.builtin.fail:
msg: "Automatic k3sup installation is not supported for {{ localhost_facts.ansible_facts.ansible_system }}/{{ localhost_facts.ansible_facts.ansible_architecture }}. Set install_k3sup_binary=false and install k3sup manually."
when:
- install_k3sup_binary | default(false)
- k3sup_release_asset == "unsupported"
run_once: true
- name: Ensure local directory for k3sup exists
ansible.builtin.file:
path: "{{ '<< k3sup_install_path >>' | dirname }}"
state: directory
mode: "0755"
delegate_to: localhost
become: true
when: install_k3sup_binary | default(false)
run_once: true
- name: Install k3sup binary on the control machine
ansible.builtin.get_url:
url: "https://github.com/alexellis/k3sup/releases/download/<< k3sup_version >>/{{ k3sup_release_asset }}"
dest: "<< k3sup_install_path >>"
mode: "0755"
delegate_to: localhost
become: true
when: install_k3sup_binary | default(false)
run_once: true
- name: Verify k3sup is available
ansible.builtin.command:
cmd: "<< k3sup_install_path >> version"
changed_when: false
delegate_to: localhost
run_once: true
- name: Ensure local kubeconfig directory exists when using local destination mode
ansible.builtin.file:
path: "{{ k3s_generated_kubeconfig_path | dirname }}"
state: directory
mode: "0700"
delegate_to: localhost
when: "<< kubeconfig_destination_mode >>" == "local"
run_once: true
- name: Check whether K3s is already present on the first server
ansible.builtin.stat:
path: /etc/rancher/k3s/k3s.yaml
delegate_to: "<< k3s_server_host >>"
register: k3s_server_install_state
run_once: true
- name: Bootstrap the first K3s server with k3sup
ansible.builtin.command:
cmd: >-
<< k3sup_install_path >> install
--ip {{ k3s_server_target }}
--user << k3s_server_user | default('ubuntu') >>
--ssh-port << k3s_server_ssh_port | default(22) >>
--local-path {{ k3s_generated_kubeconfig_path }}
--merge=false
--context << k3s_context_name | default('homelab-k3s') >>
<%- if k3sup_use_sudo %>
--sudo
<%- endif %>
<%- if k3s_server_ssh_key_path %>
--ssh-key << k3s_server_ssh_key_path | quote >>
<%- endif %>
<%- if extra_server_args %>
--k3s-extra-args << extra_server_args | quote >>
<%- endif %>
delegate_to: localhost
when:
- bootstrap_control_plane | default(true)
- not (k3s_server_install_state.stat.exists | default(false))
run_once: true
- name: Resolve inventory hosts for K3s agents
ansible.builtin.set_fact:
k3s_agent_inventory_hosts: "{{ query('inventory_hostnames', '<< k3s_agent_hosts_pattern >>') }}"
when: join_agent_nodes | default(true)
run_once: true
- name: Fail when agent join is enabled but no hosts match the inventory pattern
ansible.builtin.assert:
that:
- k3s_agent_inventory_hosts | length > 0
fail_msg: "No inventory hosts matched '<< k3s_agent_hosts_pattern >>'."
when: join_agent_nodes | default(true)
run_once: true
- name: Check whether K3s agent is already present on each node
ansible.builtin.stat:
path: /etc/systemd/system/k3s-agent.service
delegate_to: "{{ item }}"
loop: "{{ k3s_agent_inventory_hosts | default([]) }}"
loop_control:
label: "{{ item }}"
register: k3s_agent_install_state
when: join_agent_nodes | default(true)
run_once: true
- name: Join agent nodes to the K3s cluster with k3sup
ansible.builtin.command:
cmd: >-
<< k3sup_install_path >> join
--ip {{ hostvars[item.item].ansible_host | default(item.item) }}
--server-ip {{ k3s_server_target }}
--user << k3s_agent_user | default('ubuntu') >>
--server-user << k3s_server_user | default('ubuntu') >>
--ssh-port << k3s_agent_ssh_port | default(22) >>
--server-ssh-port << k3s_server_ssh_port | default(22) >>
<%- if k3sup_use_sudo %>
--sudo
<%- endif %>
<%- if k3s_server_ssh_key_path %>
--ssh-key << k3s_server_ssh_key_path | quote >>
<%- endif %>
<%- if extra_agent_args %>
--k3s-extra-args << extra_agent_args | quote >>
<%- endif %>
delegate_to: localhost
loop: "{{ k3s_agent_install_state.results | default([]) }}"
loop_control:
label: "{{ item.item }}"
when:
- join_agent_nodes | default(true)
- not (item.stat.exists | default(false))
run_once: true
- name: Ensure remote kubeconfig directory exists
ansible.builtin.file:
path: "{{ '<< kubeconfig_remote_path >>' | dirname }}"
state: directory
mode: "0700"
delegate_to: "<< kubeconfig_remote_host | default('localhost') >>"
become: << kubeconfig_remote_become | default(false) >>
when: "<< kubeconfig_destination_mode >>" == "remote"
run_once: true
- name: Copy kubeconfig to the remote destination host
ansible.builtin.copy:
src: "{{ k3s_generated_kubeconfig_path }}"
dest: "<< kubeconfig_remote_path >>"
owner: "<< kubeconfig_remote_owner | default('ubuntu') >>"
group: "<< kubeconfig_remote_group | default('ubuntu') >>"
mode: "<< kubeconfig_remote_mode | default('0600') >>"
delegate_to: "<< kubeconfig_remote_host | default('localhost') >>"
become: << kubeconfig_remote_become | default(false) >>
when: "<< kubeconfig_destination_mode >>" == "remote"
run_once: true
@@ -1,353 +0,0 @@
{
"slug": "k3sup-k3s-cluster",
"kind": "ansible",
"metadata": {
"name": "Bootstrap K3s Cluster with k3sup",
"description": "Bootstraps a K3s cluster from the Ansible control machine by using k3sup over SSH. Supports first-server install, optional agent joins, and local or remote kubeconfig placement.",
"tags": [
"kubernetes",
"k3s",
"k3sup",
"bootstrap"
],
"icon": {
"provider": "simple-icons",
"id": "kubernetes"
},
"draft": true,
"version": {
"name": "1.0.0",
"source_dep_name": "manual/k3sup-k3s-cluster"
}
},
"variables": [
{
"title": "Execution",
"name": "execution",
"items": [
{
"name": "secrets_file",
"type": "str",
"title": "Secrets File",
"required": false,
"default": "",
"description": "Optional vars file to load before running the playbook."
},
{
"name": "install_k3sup_binary",
"type": "bool",
"title": "Install k3sup",
"required": false,
"default": false,
"description": "Download and install the k3sup binary on the Ansible control machine before bootstrapping"
},
{
"name": "k3sup_version",
"type": "str",
"title": "k3sup Version",
"required": false,
"default": "0.13.15",
"description": "Version of k3sup to install when binary installation is enabled",
"needs": [
"install_k3sup_binary=true"
],
"config": {
"placeholder": "0.13.15"
}
},
{
"name": "k3sup_install_path",
"type": "str",
"title": "k3sup Path",
"required": false,
"default": "/usr/local/bin/k3sup",
"description": "Local filesystem path for the k3sup binary",
"config": {
"placeholder": "/usr/local/bin/k3sup"
}
}
]
},
{
"title": "Control Plane",
"name": "control-plane",
"items": [
{
"name": "bootstrap_control_plane",
"type": "bool",
"title": "Bootstrap Server",
"required": false,
"default": true,
"description": "Run k3sup install against the first K3s server"
},
{
"name": "k3s_server_host",
"type": "str",
"title": "Server Host",
"required": true,
"default": "k3s-server-01",
"description": "Inventory host name for the first K3s server",
"config": {
"placeholder": "k3s-server-01"
}
},
{
"name": "k3s_server_user",
"type": "str",
"title": "Server SSH User",
"required": false,
"default": "ubuntu",
"description": "SSH user used by k3sup for the first server",
"config": {
"placeholder": "ubuntu"
}
},
{
"name": "k3s_server_ssh_port",
"type": "int",
"title": "Server SSH Port",
"required": false,
"default": 22,
"description": "SSH port used for the first server",
"config": {
"slider": true,
"min": 1,
"max": 65535,
"step": 1,
"placeholder": "22"
}
},
{
"name": "k3s_server_ssh_key_path",
"type": "str",
"title": "SSH Key Path",
"required": false,
"default": "",
"description": "Optional private SSH key path for k3sup",
"config": {
"placeholder": "~/.ssh/id_ed25519"
}
},
{
"name": "k3s_context_name",
"type": "str",
"title": "Cluster Context",
"required": false,
"default": "homelab-k3s",
"description": "kubectl context name written into the generated kubeconfig",
"config": {
"placeholder": "homelab-k3s"
}
},
{
"name": "k3sup_use_sudo",
"type": "bool",
"title": "Use sudo",
"required": false,
"default": true,
"description": "Run k3sup remote operations with sudo on the target hosts"
},
{
"name": "extra_server_args",
"type": "str",
"title": "Server Args",
"required": false,
"default": "--write-kubeconfig-mode 644 --disable servicelb",
"description": "Optional extra K3s server arguments passed through k3sup",
"config": {
"placeholder": "--write-kubeconfig-mode 644 --disable servicelb"
}
}
]
},
{
"title": "Agents",
"name": "agents",
"items": [
{
"name": "join_agent_nodes",
"type": "bool",
"title": "Join Agents",
"required": false,
"default": true,
"description": "Join matching inventory hosts to the cluster as K3s agents"
},
{
"name": "k3s_agent_hosts_pattern",
"type": "str",
"title": "Agent Hosts",
"required": false,
"default": "k3s_agents",
"description": "Inventory host pattern used to discover K3s agent nodes",
"needs": [
"join_agent_nodes=true"
],
"config": {
"placeholder": "k3s_agents"
}
},
{
"name": "k3s_agent_user",
"type": "str",
"title": "Agent SSH User",
"required": false,
"default": "ubuntu",
"description": "SSH user used by k3sup for agent nodes",
"needs": [
"join_agent_nodes=true"
],
"config": {
"placeholder": "ubuntu"
}
},
{
"name": "k3s_agent_ssh_port",
"type": "int",
"title": "Agent SSH Port",
"required": false,
"default": 22,
"description": "SSH port used for agent nodes",
"needs": [
"join_agent_nodes=true"
],
"config": {
"slider": true,
"min": 1,
"max": 65535,
"step": 1,
"placeholder": "22"
}
},
{
"name": "extra_agent_args",
"type": "str",
"title": "Agent Args",
"required": false,
"default": "",
"description": "Optional extra K3s agent arguments passed through k3sup",
"needs": [
"join_agent_nodes=true"
],
"config": {
"placeholder": "--node-label role=worker"
}
}
]
},
{
"title": "Kubeconfig",
"name": "kubeconfig",
"items": [
{
"name": "kubeconfig_destination_mode",
"type": "enum",
"title": "Destination Mode",
"required": false,
"default": "local",
"description": "Store the generated kubeconfig locally on the control machine or copy it to another managed host",
"config": {
"options": [
"local",
"remote"
]
}
},
{
"name": "local_kubeconfig_path",
"type": "str",
"title": "Local Kubeconfig",
"required": false,
"default": "{{ lookup('env', 'HOME') }}/.kube/config",
"description": "Local kubeconfig path used when destination mode is local",
"needs": [
"kubeconfig_destination_mode=local"
],
"config": {
"placeholder": "{{ lookup('env', 'HOME') }}/.kube/config"
}
},
{
"name": "kubeconfig_remote_host",
"type": "str",
"title": "Remote Host",
"required": false,
"default": "localhost",
"description": "Managed inventory host that should receive the kubeconfig copy",
"needs": [
"kubeconfig_destination_mode=remote"
],
"config": {
"placeholder": "bastion-01"
}
},
{
"name": "kubeconfig_remote_path",
"type": "str",
"title": "Remote Kubeconfig",
"required": false,
"default": "/home/ubuntu/.kube/config",
"description": "Destination path on the remote host for the kubeconfig file",
"needs": [
"kubeconfig_destination_mode=remote"
],
"config": {
"placeholder": "/home/ubuntu/.kube/config"
}
},
{
"name": "kubeconfig_remote_owner",
"type": "str",
"title": "Remote Owner",
"required": false,
"default": "ubuntu",
"description": "Owner for the remote kubeconfig file",
"needs": [
"kubeconfig_destination_mode=remote"
],
"config": {
"placeholder": "ubuntu"
}
},
{
"name": "kubeconfig_remote_group",
"type": "str",
"title": "Remote Group",
"required": false,
"default": "ubuntu",
"description": "Group for the remote kubeconfig file",
"needs": [
"kubeconfig_destination_mode=remote"
],
"config": {
"placeholder": "ubuntu"
}
},
{
"name": "kubeconfig_remote_mode",
"type": "str",
"title": "Remote Mode",
"required": false,
"default": "0600",
"description": "Filesystem mode for the remote kubeconfig file",
"needs": [
"kubeconfig_destination_mode=remote"
],
"config": {
"placeholder": "0600"
}
},
{
"name": "kubeconfig_remote_become",
"type": "bool",
"title": "Remote Become",
"required": false,
"default": false,
"description": "Use become when creating directories or copying the remote kubeconfig",
"needs": [
"kubeconfig_destination_mode=remote"
]
}
]
}
]
}
@@ -1,24 +0,0 @@
---
- name: Add SSH authorized keys
hosts: all
become: true
tasks:
- name: Install SSH public key
ansible.posix.authorized_key:
user: "<< target_user >>"
state: present
key: "<< ssh_public_key >>"
manage_dir: true
<%- if configure_passwordless_sudo %>
- name: Enable passwordless sudo for the sudo group
ansible.builtin.copy:
dest: /etc/sudoers.d/90-sudo-nopasswd
content: |
%sudo ALL=(ALL:ALL) NOPASSWD: ALL
owner: root
group: root
mode: "0440"
validate: /usr/sbin/visudo -cf %s
<%- endif %>
@@ -1,55 +0,0 @@
{
"slug": "ubuntu-add-sshkey",
"kind": "ansible",
"metadata": {
"name": "Add SSH Key and Configure Sudoers",
"description": "Installs a single SSH public key for a target user on the workload-selected hosts and can enable passwordless sudo for the Ubuntu sudo group.",
"tags": [],
"icon": {
"provider": "simple-icons",
"id": "ansible"
},
"draft": false,
"version": {
"name": "1.0.0",
"source_dep_name": "manual/ubuntu-add-sshkey"
}
},
"variables": [
{
"title": "Access",
"name": "access",
"items": [
{
"name": "target_user",
"type": "str",
"title": "Target User",
"required": false,
"default": "ubuntu",
"description": "Linux user account that should receive the authorized SSH keys.",
"config": {
"placeholder": "ubuntu"
}
},
{
"name": "ssh_public_key",
"type": "str",
"title": "SSH Public Key",
"required": true,
"description": "SSH public key to add to the target user's authorized_keys file.",
"config": {
"textarea": true,
"placeholder": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBExampleKey user@laptop"
}
},
{
"name": "configure_passwordless_sudo",
"type": "bool",
"title": "Passwordless Sudo",
"required": false,
"default": true
}
]
}
]
}
@@ -1,85 +0,0 @@
---
- name: Install curated Ubuntu packages
hosts: all
become: true
gather_facts: true
vars:
ubuntu_package_matrix:
- name: python3-venv
enabled: << install_python3_venv >>
- name: python3-pip
enabled: << install_python3_pip >>
- name: python3-apt
enabled: << install_python3_apt >>
- name: curl
enabled: << install_curl >>
- name: wget
enabled: << install_wget >>
- name: git
enabled: << install_git >>
- name: jq
enabled: << install_jq >>
- name: rsync
enabled: << install_rsync >>
- name: unzip
enabled: << install_unzip >>
- name: ca-certificates
enabled: << install_ca_certificates >>
- name: gnupg
enabled: << install_gnupg >>
- name: lsb-release
enabled: << install_lsb_release >>
- name: prometheus-node-exporter
enabled: << install_prometheus_node_exporter >>
- name: nfs-common
enabled: << install_nfs_common >>
- name: qemu-guest-agent
enabled: << install_qemu_guest_agent >>
tasks:
- name: Validate Ubuntu APT target
ansible.builtin.assert:
that:
- ansible_facts['distribution'] == 'Ubuntu'
- ansible_facts['pkg_mgr'] == 'apt'
fail_msg: "This playbook only supports Ubuntu hosts that use APT."
- name: Build selected package list
ansible.builtin.set_fact:
ubuntu_selected_packages: "{{ ubuntu_package_matrix | selectattr('enabled') | map(attribute='name') | list }}"
- name: Refresh APT package index
ansible.builtin.command:
cmd: apt-get update
environment:
DEBIAN_FRONTEND: noninteractive
register: ubuntu_packages_apt_update
changed_when: false
- name: Install selected Ubuntu packages
ansible.builtin.apt:
name: "{{ ubuntu_selected_packages }}"
state: present
update_cache: false
when: ubuntu_selected_packages | length > 0
- name: Enable and start Prometheus node exporter
ansible.builtin.service:
name: prometheus-node-exporter
state: started
enabled: true
when: << install_prometheus_node_exporter >>
- name: Enable and start QEMU guest agent
ansible.builtin.service:
name: qemu-guest-agent
state: started
enabled: true
when: << install_qemu_guest_agent >>
- name: Show selected package summary
ansible.builtin.debug:
msg:
- "Host: {{ inventory_hostname }}"
- "Selected packages: {{ ubuntu_selected_packages | join(', ') if (ubuntu_selected_packages | length > 0) else 'none' }}"
@@ -1,157 +0,0 @@
{
"slug": "ubuntu-packages",
"kind": "ansible",
"metadata": {
"name": "Install Ubuntu Packages",
"description": "Installs a curated set of useful Ubuntu packages with per-package switches for scripting, package management, monitoring, NFS, and VM guest integration.",
"tags": [
"ubuntu",
"apt",
"packages",
"ansible"
],
"icon": {
"provider": "simple-icons",
"id": "ansible"
},
"draft": false,
"version": {
"name": "1.0.0",
"source_dep_name": "manual/ubuntu-packages"
}
},
"variables": [
{
"title": "Base Packages",
"name": "base_packages",
"items": [
{
"name": "install_python3_venv",
"type": "bool",
"title": "python3-venv",
"required": false,
"default": true,
"description": "Install Python virtual environment support."
},
{
"name": "install_python3_pip",
"type": "bool",
"title": "python3-pip",
"required": false,
"default": true,
"description": "Install pip for Python 3."
},
{
"name": "install_python3_apt",
"type": "bool",
"title": "python3-apt",
"required": false,
"default": true,
"description": "Install Python bindings for APT."
},
{
"name": "install_curl",
"type": "bool",
"title": "curl",
"required": false,
"default": true,
"description": "Install curl for HTTP requests and downloads."
},
{
"name": "install_wget",
"type": "bool",
"title": "wget",
"required": false,
"default": true,
"description": "Install wget for scripted downloads."
},
{
"name": "install_git",
"type": "bool",
"title": "git",
"required": false,
"default": true,
"description": "Install Git for source control and repository operations."
},
{
"name": "install_jq",
"type": "bool",
"title": "jq",
"required": false,
"default": true,
"description": "Install jq for JSON parsing in shell scripts."
},
{
"name": "install_rsync",
"type": "bool",
"title": "rsync",
"required": false,
"default": true,
"description": "Install rsync for file synchronization and copy jobs."
},
{
"name": "install_unzip",
"type": "bool",
"title": "unzip",
"required": false,
"default": true,
"description": "Install unzip for archive extraction."
},
{
"name": "install_ca_certificates",
"type": "bool",
"title": "ca-certificates",
"required": false,
"default": true,
"description": "Install CA certificates for HTTPS trust."
},
{
"name": "install_gnupg",
"type": "bool",
"title": "gnupg",
"required": false,
"default": true,
"description": "Install GnuPG for repository keys and signing workflows."
},
{
"name": "install_lsb_release",
"type": "bool",
"title": "lsb-release",
"required": false,
"default": true,
"description": "Install lsb-release for distro detection in scripts."
}
]
},
{
"title": "Optional Packages",
"name": "optional_packages",
"items": [
{
"name": "install_prometheus_node_exporter",
"type": "bool",
"title": "prometheus-node-exporter",
"required": false,
"default": false,
"description": "Install Prometheus node exporter and enable its service."
},
{
"name": "install_nfs_common",
"type": "bool",
"title": "nfs-common",
"required": false,
"default": false,
"description": "Install NFS client utilities."
},
{
"name": "install_qemu_guest_agent",
"type": "bool",
"title": "qemu-guest-agent",
"required": false,
"default": false,
"description": "Install the QEMU guest agent and enable its service."
}
]
}
]
}
@@ -1,19 +0,0 @@
---
- name: Install Ubuntu VM core packages
hosts: all
become: true
tasks:
- name: Install packages
ansible.builtin.apt:
name:
- prometheus-node-exporter
- nfs-common
- qemu-guest-agent
update_cache: true
- name: Start guest qemu-guest-agent
ansible.builtin.service:
name: qemu-guest-agent
state: started
enabled: true
@@ -1,19 +0,0 @@
{
"slug": "ubuntu-vm-core",
"kind": "ansible",
"metadata": {
"name": "Install Ubuntu VM Core Packages",
"description": "Installs essential packages for Ubuntu virtual machines, including Prometheus node exporter, NFS client utilities, and QEMU guest agent.",
"tags": [],
"icon": {
"provider": "simple-icons",
"id": "ansible"
},
"draft": false,
"version": {
"name": "1.0.0",
"source_dep_name": "manual/ubuntu-vm-core"
}
},
"variables": []
}