Ctrl K
ComunidadGuías…

playbook.yml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
---
- 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