playbook.yml
Rama main · réplica de lectura del repositorio original.
---
- name: Configure a Raspberry Pi as the LAN's DNS server
hosts: all
vars:
# Where your config repo is cloned on the Pi.
app_dir: "/home/{{ ansible_user }}/{{ config_repo.split('/')[1] }}"
timezone: Etc/UTC
# If an unattended security upgrade needs a reboot, it happens at this
# local time. Pick a quiet hour: the Pi is the network's only DNS server.
reboot_time: "04:00"
ntp_enabled: true
ntp_timezone: "{{ timezone }}"
ntp_servers:
- time.cloudflare.com
docker_users:
- "{{ ansible_user }}"
docker_install_compose_plugin: true
security_ssh_allowed_users:
- "{{ ansible_user }}"
security_sudoers_passwordless:
- "{{ ansible_user }}"
security_autoupdate_reboot: true
security_autoupdate_reboot_time: "{{ reboot_time }}"
pip_package: python3-pip
pip_install_packages:
- name: docker
pre_tasks:
# Set github_user and config_repo in your inventory. There's deliberately
# no default: a default username would authorize someone else's SSH keys.
- name: Ensure required variables are set
ansible.builtin.assert:
that:
- github_user is defined and github_user | length > 0
- config_repo is defined and config_repo | length > 0
fail_msg: >-
Set github_user (whose public SSH keys to authorize) and config_repo
(your private config repo, as OWNER/NAME) in hosts.yml. See hosts.example.yml.
quiet: true
# Secrets arrive as environment variables (see playbook.env.example).
# lookup('env') turns a missing one into "", which would write an empty
# token or password, so stop before touching the Pi instead.
- name: Ensure every secret in playbook.env.example is set
ansible.builtin.assert:
that: lookup('env', item) | length > 0
fail_msg: "{{ item }} is empty. See playbook.env.example."
quiet: true
# The names come from playbook.env.example itself, so the two can't drift.
loop: "{{ lookup('file', 'playbook.env.example').splitlines() | select('match', '^[A-Z_]+=') | map('regex_replace', '=.*', '') | list }}"
delegate_to: localhost
run_once: true # noqa: run-once[task]
# Before the security role below turns off SSH password login, so a
# first run over a password can't lock you out halfway through.
- name: Ensure SSH keys are authorized
ansible.posix.authorized_key:
user: "{{ ansible_user }}"
state: present
key: "https://github.com/{{ github_user }}.keys"
roles:
# Disables SSH password auth, sets up unattended upgrades and fail2ban.
- role: geerlingguy.security
become: true
- role: geerlingguy.ntp
become: true
# The Docker SDK for Python, which the community.docker modules need.
- role: geerlingguy.pip
become: true
# Docker from Docker's own apt repo for this Debian release and CPU,
# with the compose plugin.
- role: geerlingguy.docker
become: true
tasks:
# A deploy key lets the Pi pull your private config repo.
- name: Ensure deploy key is present
community.crypto.openssh_keypair:
path: ~/.ssh/id_github
type: ed25519
register: deploy_key
- name: Ensure deploy key is authorized
community.general.github_deploy_key:
key: "{{ deploy_key.public_key }}"
name: "{{ inventory_hostname }}"
state: present
owner: "{{ config_repo.split('/')[0] }}"
repo: "{{ config_repo.split('/')[1] }}"
token: "{{ lookup('env', 'PI_GITHUB_TOKEN') }}"
- name: Clone config repo
ansible.builtin.git:
repo: "git@github.com:{{ config_repo }}.git"
dest: "{{ app_dir }}"
version: main
key_file: ~/.ssh/id_github
accept_hostkey: true
- name: Install ufw
become: true
ansible.builtin.apt:
name: ufw
state: present
# Docker-published ports (53, 80, 443) bypass ufw, so the limits that
# matter for those are AdGuard's allowed_clients and the Caddyfile's
# remote_ip matcher. ufw still covers SSH and anything else on the host.
- name: Rate-limit SSH
become: true
community.general.ufw:
rule: limit
port: ssh
proto: tcp
- name: Allow SSH
become: true
community.general.ufw:
rule: allow
app: OpenSSH
- name: Enable ufw and default to deny
become: true
community.general.ufw:
state: enabled
default: deny
- name: Set Caddy secret
no_log: true
ansible.builtin.copy:
dest: "{{ app_dir }}/.caddy.env"
content: "CLOUDFLARE_API_TOKEN={{ lookup('env', 'PI_CLOUDFLARE_TOKEN') }}\n"
mode: "0600"
# First run only (force: false), so settings changed in the AdGuard UI stick.
- name: Create AdGuard config from the example
become: true
ansible.builtin.copy:
src: "{{ app_dir }}/adguard-conf/AdGuardHome.example.yaml"
dest: "{{ app_dir }}/adguard-conf/AdGuardHome.yaml"
remote_src: true
force: false
mode: "0600"
# Only fills the placeholder, so after the first run, change the password
# in AdGuardHome.yaml or the UI, not in PI_ADGUARD_ADMIN_HASH.
- name: Set AdGuard admin password hash
no_log: true
become: true
ansible.builtin.replace:
path: "{{ app_dir }}/adguard-conf/AdGuardHome.yaml"
regexp: "ADGUARD_ADMIN_PASSWORD_HASH"
replace: "{{ lookup('env', 'PI_ADGUARD_ADMIN_HASH') }}"
- name: Create and start docker compose services
community.docker.docker_compose_v2:
project_src: "{{ app_dir }}"
pull: always
build: always
remove_orphans: true
# The Pi is the network's DNS server, so a deploy that leaves AdGuard
# unhealthy (bad config, failed image) should fail loudly.
- name: Wait for AdGuard Home to report healthy
community.docker.docker_container_info:
name: adguardhome
register: adguard_info
until: adguard_info.exists and (adguard_info.container.State.Health.Status | default('') == 'healthy')
retries: 30
delay: 2
- name: Schedule weekly Docker cleanup
ansible.builtin.cron:
name: Docker system prune
weekday: "0"
hour: "3"
minute: "0"
job: docker system prune -af --filter 'until=72h' >/dev/null 2>&1