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
Related Articles
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.