Shore Up
A single small address card being copied and slotted into a whole row of identical filing drawers, with one drawer pulled out for a test fit first.
Linux

Roll Out an /etc/hosts or resolv.conf Change Across Servers with Ansible

Ketan Aagja8 min read
No ratings yet

Before you run this

This guide pushes a name-resolution change — either a static entry in /etc/hosts or an upstream DNS server change — to a group of servers at once using Ansible. Getting name resolution wrong across a fleet is one of the fastest ways to take down a whole environment: a bad /etc/hosts line or a dead DNS server means services can't find each other, monitoring goes dark, and package mirrors and time sync stop working.

  • Privileges: the tasks edit files under /etc and restart a service, so they run with become: true (sudo) on the managed hosts. Your SSH user needs working passwordless sudo, or you supply --ask-become-pass. On the control node, Ansible itself runs unprivileged.
  • Test first, on one host. Read the playbook, then run it against a single non-production server using --limit and --check --diff before you touch the real group. Confirm the diff is exactly what you expect. Never run it fleet-wide on the first attempt.
  • What it changes, and reversibility. The /etc/hosts task writes a marked block you can cleanly remove later. The resolv.conf task changes upstream DNS. Both use backup: true, which leaves a timestamped .bak copy on each host — but the safe undo is the playbook itself (shown at the end), not hand-editing dozens of backups.
  • Keep a second way in. If these servers are remote, keep a separate console / out-of-band session open (IPMI, cloud serial console, or a second SSH session you do not close) while you run this. If resolution breaks and your SSH depends on DNS, that second session is how you fix it.
  • The resolv.conf caveat — read this. On modern Debian/Ubuntu, /etc/resolv.conf is usually a symlink managed by systemd-resolved (or by NetworkManager / cloud-init). Writing the file directly will be silently overwritten on reboot or on a network event. So the correct, durable way to change DNS is to configure the resolver manager, not clobber the file. This guide does that.

Assumptions

  • Control node: Ansible (ansible-core 2.15+) installed on a Debian/Ubuntu admin box.
  • Managed hosts: Debian 12 or Ubuntu 22.04, with systemd-resolved active (the default). If your hosts use NetworkManager or a plain static /etc/resolv.conf, I note the difference where it matters.
  • RHEL/Alma: the file paths are the same; the resolver is often NetworkManager rather than systemd-resolved, so the DNS section below applies differently — check your distro's docs before using it there.

All commands use only stock modules from ansible.builtin, so there's nothing extra to install.

Inventory

Group the target servers so you can scope changes. A minimal inventory.ini:

[webservers]
web01.example.com
web02.example.com
web03.example.com

[webservers:vars]
ansible_user=deploy   # your SSH/sudo user; adjust to your fleet

Replace the hostnames and ansible_user with your own.

Part 1 — Push an /etc/hosts entry

I use blockinfile rather than lineinfile here because it wraps the managed lines in a labelled # BEGIN / # END block. That makes the change idempotent, easy to spot in the file, and trivial to remove later.

hosts_entry.yml:

---
- name: Manage internal /etc/hosts entries
  hosts: webservers
  become: true
  tasks:
    - name: Ensure managed block in /etc/hosts
      ansible.builtin.blockinfile:
        path: /etc/hosts
        # {mark} is replaced by BEGIN/END automatically
        marker: "# {mark} ANSIBLE MANAGED: internal hosts"
        block: |
          10.10.0.10   db01.example.com   db01
          10.10.0.11   cache01.example.com cache01
        backup: true          # leaves /etc/hosts.<timestamp>.bak on each host
        state: present

Replace the IPs and names with your real internal records. Keep them inside the block: — anything outside the marker is left untouched.

Dry-run against one host first:

ansible-playbook -i inventory.ini hosts_entry.yml --limit web01.example.com --check --diff

--check makes no changes; --diff prints exactly what would be written. Read it. When it's right, apply to that one host for real:

ansible-playbook -i inventory.ini hosts_entry.yml --limit web01.example.com --diff

Verify on that host (see verification below), then drop --limit to roll out to the whole group:

ansible-playbook -i inventory.ini hosts_entry.yml --diff

Part 2 — Change upstream DNS (the durable way)

Because /etc/resolv.conf is managed by systemd-resolved on these hosts, the change goes into /etc/systemd/resolved.conf under the [Resolve] section, and we restart the service so the managed resolv.conf symlink reflects it.

dns_servers.yml:

---
- name: Set upstream DNS via systemd-resolved
  hosts: webservers
  become: true
  tasks:
    - name: Set DNS= in resolved.conf
      ansible.builtin.lineinfile:
        path: /etc/systemd/resolved.conf
        regexp: '^#?DNS='        # matches the default commented "#DNS=" line too
        line: 'DNS=10.10.0.53 10.10.0.54'
        backup: true
      notify: Restart systemd-resolved

    - name: Set search domain in resolved.conf
      ansible.builtin.lineinfile:
        path: /etc/systemd/resolved.conf
        regexp: '^#?Domains='
        line: 'Domains=example.com'
        backup: true
      notify: Restart systemd-resolved

  handlers:
    - name: Restart systemd-resolved
      ansible.builtin.systemd:
        name: systemd-resolved
        state: restarted

Replace 10.10.0.53 10.10.0.54 with your resolvers and example.com with your search domain. The DNS= and Domains= keys and the [Resolve] section are documented in man resolved.conf — check it if you need FallbackDNS= or DNSSEC options, which I've deliberately left out to keep this to the common case.

Same rollout discipline — dry run, one host, then the group:

ansible-playbook -i inventory.ini dns_servers.yml --limit web01.example.com --check --diff
ansible-playbook -i inventory.ini dns_servers.yml --limit web01.example.com --diff
ansible-playbook -i inventory.ini dns_servers.yml --diff

If your hosts don't run systemd-resolved: for a genuinely static, unmanaged /etc/resolv.conf, you'd manage the file directly (with copy/template and backup: true). For NetworkManager-managed hosts, the DNS setting belongs on the connection profile, not in resolved.conf. Confirm which manager owns resolv.conf on your fleet before choosing — ls -l /etc/resolv.conf tells you what it points at.

Verification

Run these as ad-hoc commands so you check every host at once.

Confirm the hosts entry resolves via the file:

ansible webservers -i inventory.ini -m command -a 'getent hosts db01.example.com'

Check the DNS servers systemd-resolved is actually using:

ansible webservers -i inventory.ini -b -m command -a 'resolvectl status'

Look for your DNS Servers: values in the output, and confirm a real lookup works:

ansible webservers -i inventory.ini -m command -a 'resolvectl query db01.example.com'

If any host reports a failure, that's what your out-of-band session is for.

Undo / rollback

Remove the hosts block — flip the same task to state: absent. Ansible finds the markers and deletes the block cleanly. Change state: present to state: absent in hosts_entry.yml and re-run:

ansible-playbook -i inventory.ini hosts_entry.yml --check --diff   # confirm it removes the block
ansible-playbook -i inventory.ini hosts_entry.yml

Revert the DNS change — edit line: in dns_servers.yml back to your previous values (or to the default #DNS= if you want it unset) and re-run the playbook. Because the regexp matches the existing line, it's replaced in place, and the handler restarts systemd-resolved. The backup: true .bak files on each host are your reference for what the previous values were:

ansible webservers -i inventory.ini -b -m command -a 'ls -t /etc/systemd/resolved.conf*'

Roll back one host, verify with resolvectl status, then roll back the rest — the same order you rolled it out. For the module options and any parameter I didn't cover, the ansible.builtin.blockinfile, lineinfile, and systemd module pages on the official Ansible documentation are the reference.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.

Bulk-Update BIND Zone Files and Reload Safely with a Script

This script applies one literal find-and-replace across every zone file in a directory (for example, retiring an old IP or NS name), bumps each changed zone's SOA serial so slaves pick up the change, validates every touched zone, and reloads BIND only if all of them pass . If any zone fails validation it restores the backups and aborts before touching the running server.

9 min read

Deploy a Postfix Relay with Ansible

This playbook installs Postfix on one or more target hosts and configures it as a send-only relay (a "smarthost" client) : local mail is handed to an upstream provider over an authenticated, TLS-encrypted SMTP submission connection, and the host does not accept mail from the network. Its purpose is to give servers a reliable way to send notifications, cron output, and application mail without each app talking to your provider directly.

8 min read

Harden SSH Across a Fleet with Ansible

This playbook drops a single sshd configuration file ( /etc/ssh/sshd_config.d/99-hardening.conf ) onto every host in your inventory and reloads the SSH service. It disables root login, disables password authentication, requires public-key auth, and tightens a handful of session and auth limits.

8 min read

A Bash Script to Test Mail Server Deliverability End to End

This script runs a series of read-only and send checks against a mail domain you control: it looks up MX, SPF, DKIM, and DMARC records with dig , tests the STARTTLS handshake on the submission port with openssl , and then sends one real test message through your server with swaks . Its purpose is to confirm, in one pass, that mail for your domain is configured to leave and arrive correctly.

10 min read