Manage PiHole Custom DNS entries with Ansible (Update 2)

Hi everyone, the old ansible role does not work anymore with the latest version of PiHole. I will leave it, just in case, but below you can find the new role I am currently using.

I cannot take any credit for this playbook, unfortunately I cannot find the source anymore.

Here my current ansible role / playbook for the new version.

New Ansible role

Here the new folder structure for the ansible role.

  • ansible-role-pihole
    • files
      • config
        • pihole
          • (file) custom.list
    • tasks
      • main.yml
      • (file) api-dns-configuration-pihole-01.yml
      • (file) api-dns-configuration-pihole-02.yml
    • vars
      • main.yml

Alright. And here is the file content. If you add additional PiHole servers, just copy the api-dns-configuration-pihole-01.yml file and replace the variable “api_url_pihole_server_01” with the new variable. Also adjust the vars file “main.yml” and add the new variable in there.

If you want to add new DNS entries, just edit the custom.list file.

custom.list

10.10.0.100 pihole-01.example.local
10.10.0.101 pihole-02.example.local
10.10.0.102 other-server.example.local

api-dns-configuration-pihole-01.yml
- name: Authenticate with Pi-hole API
  shell: |
    curl -X POST "{{ api_url_pihole_server_01 }}/auth" \
      -H "accept: application/json" \
      -H "content-type: application/json" \
      -d '{"password":"{{ webpassword }}"}'
  register: api_auth_response

- name: Show API auth response
  debug:
    var: api_auth_response

- name: Parse API auth response
  set_fact:
    api_sid: "{{ api_auth_response.stdout | from_json | json_query('session.sid') }}"
    api_csrf: "{{ api_auth_response.stdout | from_json | json_query('session.csrf') }}"

- name: Show extracted sid and csrf for debugging
  debug:
    msg:
      - "sid: {{ api_sid }}"
      - "csrf: {{ api_csrf }}"

- name: Read custom.list and extract dns hosts
  set_fact:
    dns_hosts: "{{ lookup('file', '../files/config/pihole/custom.list') }}"

- name: Prepare dns hosts for the payload
  set_fact:
    dns_hosts_converted: "{{ dns_hosts.split('\n') | map('regex_replace', '^(.*)$', '\"\\1\"') | join(',\n') }}"

- name: Ouput dns hosts with quotes
  debug:
    var: dns_hosts_converted

- name: Send the updated DNS hosts to the API
  shell: |
    curl -X PATCH "{{ api_url_docker_router }}/config" \
      -H "accept: application/json" \
      -H "content-type: application/json" \
      -H "X-FTL-SID: {{ api_sid }}" \
      -d '{
        "config": {
          "dns": {
            "hosts": [
              {{ dns_hosts_converted }}
            ]
          }
        }
      }'

- name: Delete the sid to logout
  shell: |
    curl -X DELETE "{{ api_url_docker_router }}/auth" \
      -H "accept: application/json" \
      -H "content-type: application/json" \
      -H "X-FTL-SID: {{ api_sid }}"


api-dns-configuration-pihole-02.yml
- name: Authenticate with Pi-hole API
  shell: |
    curl -X POST "{{ api_url_pihole_server_02 }}/auth" \
      -H "accept: application/json" \
      -H "content-type: application/json" \
      -d '{"password":"{{ webpassword }}"}'
  register: api_auth_response

- name: Show API auth response
  debug:
    var: api_auth_response

- name: Parse API auth response
  set_fact:
    api_sid: "{{ api_auth_response.stdout | from_json | json_query('session.sid') }}"
    api_csrf: "{{ api_auth_response.stdout | from_json | json_query('session.csrf') }}"

- name: Show extracted sid and csrf for debugging
  debug:
    msg:
      - "sid: {{ api_sid }}"
      - "csrf: {{ api_csrf }}"

- name: Read custom.list and extract dns hosts
  set_fact:
    dns_hosts: "{{ lookup('file', '../files/config/pihole/custom.list') }}"

- name: Prepare dns hosts for the payload
  set_fact:
    dns_hosts_converted: "{{ dns_hosts.split('\n') | map('regex_replace', '^(.*)$', '\"\\1\"') | join(',\n') }}"

- name: Ouput dns hosts with quotes
  debug:
    var: dns_hosts_converted

- name: Send the updated DNS hosts to the API
  shell: |
    curl -X PATCH "{{ api_url_docker_router }}/config" \
      -H "accept: application/json" \
      -H "content-type: application/json" \
      -H "X-FTL-SID: {{ api_sid }}" \
      -d '{
        "config": {
          "dns": {
            "hosts": [
              {{ dns_hosts_converted }}
            ]
          }
        }
      }'

- name: Delete the sid to logout
  shell: |
    curl -X DELETE "{{ api_url_docker_router }}/auth" \
      -H "accept: application/json" \
      -H "content-type: application/json" \
      -H "X-FTL-SID: {{ api_sid }}"


tasks – main.yml
- import_tasks: api-dns-configuration-pihole.yml
  when: inventory_hostname == "pihole"
vars – main.yml
api_url_pihole_server_01: "https://pihole-01.example.local/api"
api_url_pihole_server_02: "https://pihole-02.example.local/api"
webpassword: "<your-web-login-password"

Hope this helps.
Sorry that I cannot find the original source.

(Update 2) Anything below does not work anymore, with the latest version of PiHole.

Hi there,

Today, I’d like to share a simple way to manage custom DNS entries on multiple Pi-hole servers using Ansible. I’m currently running three Pi-hole servers as Docker containers – two in the cloud and one on-premises – to provide redundant DNS for my Netbird network.

However, having to update all three servers manually can get tedious very quickly.

So, I will be using Ansible to make this way more efficient.

The setup is straightforward: we’ll replace the “custom.list” file in the Pi-hole config folder.

I assume you have already set up SSH keys and hosts configuration for your servers.

Creating the Ansible role

For this, I’ll create a role, since I prefer the flexibility and I am more used to this kind of set up.

We create a folder and the required subfolders.

ansible :: ~ » cd Ansible/roles

# Create folders
ansible :: roles » mkdir -p ansible-role-pihole/tasks/copy-config ansible-role-pihole/files/config/pihole

# List folders
ansible :: roles » tree ansible-role-pihole
ansible-role-pihole
├── files
│   └── config
│   └── pihole
├── tasks
│   ├── copy-config

Next, we create the ansible role yml file we will use to execute the playbooks.

ansible :: roles » vim ansible-role-pihole.yml
---
- hosts: pihole
gather_facts: true
become: true
roles:
- ansible-role-pihole

Create a task to copy the “custom.list” file to the Pi-hole servers and place this task in the “tasks/copy-config” folder.

Update the path to match your Pi-hole configuration. In my case it would be “/etc/pihole/pihole”, but that’s not the default.

ansible :: roles » cd ansible-role-pihole/tasks/copy-config
ansible :: copy-config » vim copy-config-pihole-custom-dns.yml
---
- name: pihole custom dns configuration
copy:
src: files/config/pihole/custom.list
dest: <path-to-pihole-folder>/custom.list
owner: root
mode: 0644
backup: yes

Create a “main.yml” file in the tasks folder “ansible-role-pihole”.

ansible :: roles » cd ansible-role-pihole/tasks
ansible :: tasks » vim main.yml
- import_tasks: copy-config/copy-config-pihole-custom-dns.yml
tags: custom-dns

The last file we need, is the “custom.list”. Create it in the “files/config/pihole” folder. The content is a simple list of IP address and hostname.

ansible :: roles » cd ansible-role-pihole/files/config/pihole/
ansible :: pihole » vim custom.list
10.10.0.103 ipa.example.local
10.10.0.108 file.exmaple.local

Alright. Just a quick look into the ansible hosts file.

ansible :: Ansible » vim hosts
all:
children:
pihole:
hosts:
pihole-01:
ansible_host: <ip-address>
pihole-02:
ansible_host: <ip-address>
pihole-03:
ansible_host: <ip-address>

Let’s take another look at the file structure.

ansible :: Ansible » tree roles/ansible-role-pihole
roles/ansible-role-pihole
├── files
│   └── config
│   └── pihole
│   └── custom.list
├── tasks
│   ├── copy-config
│   │   └── copy-config-pihole-custom-dns.yml
│   └── main.yml

Ok. Now execute the playbook.

ansible :: Ansible » ansible-playbook roles/ansible-role-pihole.yml -i hosts
PLAY [pihole] ***********************************************************************************************************

TASK [Gathering Facts] **************************************************************************************************
ok: [pihole-01]
ok: [pihole-02]
ok: [pihole-03]


TASK [ansible-role-pihole : pihole custom dns configuration] ************************************************************
changed: [pihole-01]
changed: [pihole-02]
changed: [pihole-03]


PLAY RECAP **************************************************************************************************************
pihole-01 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
pihole-02 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
pihole-03 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

And that is it.

(Update)

I am going to restart the DNS resolver service after the custom.list has been replaced, since it takes a while until the resolver picks it up.

Add the following (marked in orange) to the “copy-config-pihole-custom-dns.yml” file.

---
- name: pihole custom dns configuration
copy:
src: files/config/pihole/custom.list
dest: <path-to-pihole-folder>/custom.list
owner: root
mode: 0644
backup: yes
register: pihole_custom_dns

Now create a new yml file and type in the following. This will execute the “pihole restartdns” command within the docker container. Make sure the container name fits your environment.

- name: Restart PiHole DNS Resolver
community.docker.docker_container_exec:
container: <container-name>
command: /usr/local/bin/pihole restartdns
chdir: /
when: pihole_custom_dns.changed

Once that’s done, add the task to the “main.yml” file.

- import_tasks: copy-config/copy-config-pihole-custom-dns.yml
tags: custom-dns
- import_tasks: docker-exec.yml
tags: pihole-docker-exec

(Update)

Of course, we could manage much more than that. For instance, we could update the upstream DNS servers or modify specific settings like DNSSEC.

Thanks. Till next time.

Leave a Reply