How to Dynamically Populate /etc/hosts with Ansible Inventory IPs and Hostnames


2 views

When setting up a multi-node cluster, maintaining consistent /etc/hosts entries across all nodes is crucial for proper hostname resolution. The manual approach becomes impractical as your infrastructure grows.

Here's the problematic playbook snippet:

- name: Add IP address of all hosts to all hosts
  lineinfile: 
    dest: /etc/hosts
    line: '{{ hostvars[item]["ansible_host"] }} {{ hostvars[item]["ansible_hostname"] }} {{ hostvars[item]["ansible_nodename"] }}'
    state: present
  with_items: groups['all']

The key issues in the original approach are:

  1. Incorrect variable reference syntax for groups
  2. Missing proper loop context
  3. Potential undefined variables

Here's the corrected playbook implementation:

- name: Add all cluster nodes to /etc/hosts
  lineinfile:
    path: /etc/hosts
    line: "{{ hostvars[item].ansible_host }} {{ item }} {{ hostvars[item].ansible_nodename | default(item) }}"
    state: present
    create: yes
  loop: "{{ groups['all'] }}"
  when: hostvars[item].ansible_host is defined

For production environments, consider this more robust version:

- name: Ensure /etc/hosts contains all cluster nodes
  block:
    - name: Create backup of current /etc/hosts
      copy:
        src: /etc/hosts
        dest: /etc/hosts.bak
        remote_src: yes

    - name: Add host entries
      lineinfile:
        path: /etc/hosts
        line: "{{ hostvars[item].ansible_host }} {{ item.split('.')[0] }} {{ item }}"
        regexp: "^{{ hostvars[item].ansible_host }}.*{{ item.split('.')[0] }}"
        state: present
      loop: "{{ groups['cluster_nodes'] | default(groups['all']) }}"
      when: hostvars[item].ansible_host is defined

For complex scenarios, a template might be more maintainable:

- name: Generate /etc/hosts from template
  template:
    src: templates/hosts.j2
    dest: /etc/hosts
    owner: root
    group: root
    mode: '0644'

With corresponding template (hosts.j2):

127.0.0.1   localhost localhost.localdomain
::1         localhost localhost.localdomain

# Cluster nodes
{% for host in groups['all'] if hostvars[host].ansible_host is defined %}
{{ hostvars[host].ansible_host }} {{ hostvars[host].ansible_hostname | default(host) }} {{ hostvars[host].ansible_nodename | default(host.split('.')[0]) }}
{% endfor %}
  • Always back up /etc/hosts before modifications
  • Use dedicated inventory groups (like 'cluster_nodes') instead of 'all'
  • Include proper variable existence checks
  • Consider using DNS for larger clusters
  • Test changes in a staging environment first

If you encounter issues:

- name: Debug host variables
  debug:
    var: hostvars[item]
  loop: "{{ groups['all'] }}"

- name: Verify inventory structure
  debug:
    var: groups

When setting up multi-node clusters, maintaining consistent hostname resolution across all servers is crucial. The typical approach of manually editing /etc/hosts becomes impractical at scale. Here's a robust Ansible solution to automate this process.

The initial approach had several issues:


# Problematic code:
- name: Add IP address of all hosts to all hosts
  lineinfile: 
    dest: /etc/hosts
    line: '{{ hostvars[item]["ansible_host"] }} {{ hostvars[item]["ansible_hostname"] }} {{ hostvars[item]["ansible_nodename"] }}'
    state: present
  with_items: groups['all']

The main errors were:

  • Incorrect variable reference syntax for groups
  • Missing proper iteration through host variables
  • No handling of duplicate entries

Here's the corrected and enhanced version:


- name: Update /etc/hosts with cluster nodes
  blockinfile:
    path: /etc/hosts
    block: |
      {% for host in groups['all'] %}
      {{ hostvars[host].ansible_host }} {{ hostvars[host].ansible_hostname }} {{ hostvars[host].ansible_nodename | default(hostvars[host].ansible_hostname) }}
      {% endfor %}
    marker: "# {mark} ANSIBLE MANAGED BLOCK - CLUSTER HOSTS"
    insertafter: EOF

1. Using blockinfile instead of lineinfile:

  • Maintains all entries in a single managed block
  • Easier to update and maintain
  • Prevents duplicate entries

2. Proper variable handling:


# Fallback to hostname if nodename isn't defined
{{ hostvars[host].ansible_nodename | default(hostvars[host].ansible_hostname) }}

For more complex inventories, consider these variations:

Basic Inventory Example (inventory.ini):


[webservers]
web1 ansible_host=192.168.1.10 ansible_hostname=web1
web2 ansible_host=192.168.1.11 ansible_hostname=web2

[dbservers]
db1 ansible_host=192.168.1.20 ansible_hostname=db1

Alternative Implementation with Comments:


- name: Create comprehensive hosts entries
  blockinfile:
    path: /etc/hosts
    block: |
      # Cluster nodes - generated by Ansible
      {% for host in groups['all'] %}
      {{ hostvars[host].ansible_host }} {{ hostvars[host].ansible_hostname }} 
        {% if hostvars[host].ansible_nodename is defined %}
        {{ hostvars[host].ansible_nodename }}
        {% endif %}
      {% endfor %}
    marker: "# {mark} ANSIBLE MANAGED BLOCK"

After running the playbook, verify the results:


- name: Verify /etc/hosts updates
  command: cat /etc/hosts
  register: hosts_content
  changed_when: false

- debug:
    var: hosts_content.stdout_lines

For production environments, consider these enhancements:

  • Add validation to ensure IP addresses are properly formatted
  • Include backup functionality before modifying /etc/hosts
  • Implement idempotency checks to prevent unnecessary changes
  • Add tags for selective execution