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:
- Incorrect variable reference syntax for groups
- Missing proper loop context
- 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