Introduction

Dnsmasq provides lightweight DHCP, DNS, and TFTP services in a single daemon — perfect for lab environments, edge networks, and PXE boot infrastructure. Ansible automates the full setup: DHCP ranges with MAC-based reservations, DNS forwarding with local overrides, and PXE boot for automated OS installation.

Deploy Dnsmasq

---
- name: Deploy Dnsmasq
  hosts: network_servers
  become: true
  vars:
    dnsmasq_interface: eth0
    dnsmasq_domain: lab.example.com
    dnsmasq_dhcp_range_start: 10.0.0.100
    dnsmasq_dhcp_range_end: 10.0.0.200
    dnsmasq_dhcp_lease_time: 12h
    dnsmasq_gateway: 10.0.0.1
    dnsmasq_dns_servers:
      - 1.1.1.1
      - 8.8.8.8
  tasks:
    - name: Install Dnsmasq
      ansible.builtin.package:
        name: dnsmasq
        state: present

    - name: Deploy dnsmasq.conf
      ansible.builtin.template:
        src: dnsmasq.conf.j2
        dest: /etc/dnsmasq.conf
        mode: '0644'
      notify: restart dnsmasq

    - name: Deploy DHCP reservations
      ansible.builtin.template:
        src: dhcp-reservations.conf.j2
        dest: /etc/dnsmasq.d/reservations.conf
        mode: '0644'
      notify: restart dnsmasq

    - name: Deploy local DNS entries
      ansible.builtin.template:
        src: local-dns.conf.j2
        dest: /etc/dnsmasq.d/local-dns.conf
        mode: '0644'
      notify: restart dnsmasq

    - name: Allow DHCP/DNS through firewall
      ansible.posix.firewalld:
        service: "{{ item }}"
        permanent: true
        state: enabled
        immediate: true
      loop: [dhcp, dns]

    - name: Start Dnsmasq
      ansible.builtin.service:
        name: dnsmasq
        state: started
        enabled: true

  handlers:
    - name: restart dnsmasq
      ansible.builtin.service:
        name: dnsmasq
        state: restarted

Main Config

# templates/dnsmasq.conf.j2
# Managed by Ansible

# Interface binding
interface={{ dnsmasq_interface }}
bind-interfaces

# Domain
domain={{ dnsmasq_domain }}
local=/{{ dnsmasq_domain }}/

# DHCP
dhcp-range={{ dnsmasq_dhcp_range_start }},{{ dnsmasq_dhcp_range_end }},{{ dnsmasq_dhcp_lease_time }}
dhcp-option=option:router,{{ dnsmasq_gateway }}
dhcp-option=option:dns-server,{{ ansible_default_ipv4.address }}
dhcp-option=option:domain-name,{{ dnsmasq_domain }}
dhcp-option=option:ntp-server,{{ ansible_default_ipv4.address }}
dhcp-authoritative
dhcp-leasefile=/var/lib/dnsmasq/dnsmasq.leases

# DNS
{% for dns in dnsmasq_dns_servers %}
server={{ dns }}
{% endfor %}

# Logging
log-dhcp
log-queries
log-facility=/var/log/dnsmasq.log

# Security
bogus-priv
domain-needed
no-resolv
no-poll

# Cache
cache-size=1000

{% if dnsmasq_pxe_enabled | default(false) %}
# PXE/TFTP
enable-tftp
tftp-root=/var/lib/tftpboot
dhcp-boot=pxelinux.0
{% endif %}

# Include additional configs
conf-dir=/etc/dnsmasq.d/,*.conf

DHCP Reservations

# templates/dhcp-reservations.conf.j2
# Static DHCP reservations
{% for host in dnsmasq_reservations | default([]) %}
dhcp-host={{ host.mac }},{{ host.ip }},{{ host.hostname }}{% if host.lease is defined %},{{ host.lease }}{% endif %}

{% endfor %}
# Variables
dnsmasq_reservations:
  - { mac: "aa:bb:cc:dd:ee:01", ip: 10.0.0.10, hostname: server1 }
  - { mac: "aa:bb:cc:dd:ee:02", ip: 10.0.0.11, hostname: server2 }
  - { mac: "aa:bb:cc:dd:ee:03", ip: 10.0.0.12, hostname: server3 }
  - { mac: "aa:bb:cc:dd:ee:10", ip: 10.0.0.20, hostname: printer, lease: infinite }

Local DNS

# templates/local-dns.conf.j2
# Local DNS entries from inventory
{% for host in groups['all'] %}
{% if hostvars[host].ansible_host is defined %}
address=/{{ host }}.{{ dnsmasq_domain }}/{{ hostvars[host].ansible_host }}
{% endif %}
{% endfor %}

# Additional DNS entries
{% for entry in dnsmasq_dns_entries | default([]) %}
address=/{{ entry.name }}/{{ entry.ip }}
{% endfor %}

# CNAME aliases
{% for cname in dnsmasq_cnames | default([]) %}
cname={{ cname.alias }},{{ cname.target }}
{% endfor %}
dnsmasq_dns_entries:
  - { name: git.lab.example.com, ip: 10.0.0.30 }
  - { name: registry.lab.example.com, ip: 10.0.0.31 }

dnsmasq_cnames:
  - { alias: www.lab.example.com, target: server1.lab.example.com }

PXE Boot Setup

- name: Configure PXE boot
  hosts: network_servers
  become: true
  vars:
    dnsmasq_pxe_enabled: true
  tasks:
    - name: Create TFTP directory
      ansible.builtin.file:
        path: /var/lib/tftpboot
        state: directory
        mode: '0755'

    - name: Install Syslinux for PXE
      ansible.builtin.package:
        name: syslinux-common
        state: present

    - name: Copy PXE bootloader
      ansible.builtin.copy:
        src: /usr/lib/syslinux/modules/bios/{{ item }}
        dest: /var/lib/tftpboot/{{ item }}
        remote_src: true
        mode: '0644'
      loop:
        - pxelinux.0
        - ldlinux.c32
        - menu.c32
        - libutil.c32

    - name: Create PXE menu
      ansible.builtin.copy:
        dest: /var/lib/tftpboot/pxelinux.cfg/default
        content: |
          DEFAULT menu.c32
          PROMPT 0
          TIMEOUT 100
          MENU TITLE PXE Boot Menu

          LABEL ubuntu
            MENU LABEL Install Ubuntu 24.04
            KERNEL ubuntu/vmlinuz
            APPEND initrd=ubuntu/initrd root=/dev/ram0 ramdisk_size=1500000 ip=dhcp url=http://{{ ansible_default_ipv4.address }}/ubuntu.iso

          LABEL local
            MENU LABEL Boot from local disk
            LOCALBOOT 0
        mode: '0644'

Health Check

- name: Check DHCP leases
  ansible.builtin.command: cat /var/lib/dnsmasq/dnsmasq.leases
  register: leases
  changed_when: false

- name: Test DNS resolution
  ansible.builtin.command: "dig @localhost {{ item }} +short"
  loop:
    - server1.{{ dnsmasq_domain }}
    - google.com
  register: dns_test
  changed_when: false

- name: Check Dnsmasq status
  ansible.builtin.command: systemctl status dnsmasq
  register: dnsmasq_status
  changed_when: false

Troubleshooting

DHCP Not Assigning

- name: Check Dnsmasq logs
  ansible.builtin.command: "grep -i dhcp /var/log/dnsmasq.log | tail -20"
  register: dhcp_logs
  changed_when: false

Config Validation

- name: Test config syntax
  ansible.builtin.command: dnsmasq --test
  register: config_test
  changed_when: false

Conclusion

Dnsmasq combines DHCP, DNS, and TFTP in a single lightweight daemon — Ansible templates the config from YAML variables for DHCP ranges, MAC reservations, local DNS entries, and PXE boot menus. Generate DNS records directly from Ansible inventory so every managed host is automatically resolvable. Perfect for labs, edge sites, and bootstrap infrastructure.