Infrastructure-as-code for my homelab. Uses playbook layering, component roles, and orchestrator roles for flexible targeting (e.g., deploy only Alloy on host A; Alloy+Loki on hosts in lokihosts).
- Ansible >= 2.19
- Collections
ansible-galaxy install -r collections/requirements.yml- Recommended tree:
.
├── ansible.cfg
├── collections/requirements.yml
├── inventories/
│ └── prod/
│ ├── hosts.yml
│ ├── group_vars/
│ └── host_vars/
├── playbooks/
│ ├── site.yml
│ ├── base.yml
│ ├── services.yml
│ ├── docker.yml
│ └── keepalived.yml
└── roles/
├── apt_cache/
├── grafana_common/ # repo + shared prereqs
├── grafana_alloy/
├── grafana_loki/
├── grafana_stack/ # orchestrator (select components)
├── docker_common/ # engine + compose
├── docker_app_dns/ # AdGuard + Unbound compose app
├── docker_stack/ # orchestrator (select apps)
└── keepalived/
Ensure
ansible.cfgincludesroles_path = ./roles:./playbooks/roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles.
all:
children:
rpis:
hosts:
adguard01: { ansible_host: 192.168.10.253 }
adguard02: { ansible_host: 192.168.10.254 }
grafana_hosts:
hosts:
adguard01:
lokihosts:
hosts:
log01:
vip_dns:
hosts:
adguard01:
adguard02:inventories/lab/group_vars/grafana_hosts.yml
grafana_components: ["alloy"] # picked up by grafana_stackinventories/lab/group_vars/lokihosts.yml
grafana_components: ["loki"] # picked up by grafana_stackinventories/lab/group_vars/docker_hosts.yml
docker_apps: ["dns"] # picked up by docker_stackinventories/lab/group_vars/vip_dns.yml
keepalived_group: "vip_dns"
keepalived_vrid: 52
keepalived_vip: "192.168.10.252/24"
keepalived_auth_pass: "supersecret"playbooks/site.yml: Baseline (e.g., apt cache)playbooks/docker.yml: Docker engine + selected compose appsplaybooks/services.yml: Grafana stack (Alloy/Loki/etc.)playbooks/keepalived.yml: VRRP VIP for DNS
Run baseline on a single host:
ansible-playbook -i inventories/lab/hosts.yml playbooks/base.yml --limit adguard02Deploy Grafana layer to its group (repo + selected components):
ansible-playbook -i inventories/lab/hosts.yml playbooks/services.yml --tags grafanaOnly Alloy configure step (no Loki) on current host:
ansible-playbook -i inventories/lab/hosts.yml playbooks/services.yml --tags alloy,configure --limit adguard01Deploy Docker engine + DNS compose app:
ansible-playbook -i inventories/lab/hosts.yml playbooks/docker.yml --tags dockerRe-render DNS compose after Certbot (adds LE mount; restarts AdGuard only):
ansible-playbook -i inventories/lab/hosts.yml -l adguard01 -t configure \
-e 'ansible_python_interpreter=/usr/bin/python3' \
-e 'docker_app_dir=/opt/containers/dns' \
-e 'docker_compose_file=/opt/containers/dns/compose.yml' \
-e 'adguard_root=/opt/containers/dns/adguardhome' \
-e 'unbound_root=/opt/containers/dns/unbound' \
-e 'use_host_network_for_adguard=true' \
-e 'adguard_listen_web_port=3000' \
-e 'unbound_host_port=5335' \
roles/docker_app_dns/tasks/render.ymlDeploy keepalived to VIP members:
ansible-playbook -i inventories/lab/hosts.yml playbooks/keepalived.yml --tags keepalived- Layer:
base,docker,grafana,keepalived - Component:
alloy,loki,dns, etc. - Step:
install,configure,start,stop, `remove
Examples:
# Install phase for all Grafana components on target hosts
--tags grafana,install
# Only Loki install
--tags loki,install
# Only Docker repo setup
--tags docker:repo- Orchestrators (
grafana_stack,docker_stack) do nothing by default. Enable components/apps per host/group with:grafana_components: ["alloy", "loki"]docker_apps: ["dns"]
- Handlers restart only affected services (e.g., Alloy or AdGuard) when templates change.
- Prefer
ansible_hostin inventory so peer IPs (keepalived unicast) resolve to addresses while still honoring SSHHostaliases.