Friday, June 28, 2013

[Ansible] Dynamicaly update /etc/hosts files on target servers based on Ansible inventory, respecting idempotency


Ansible is just a great peace of work.

# START Blha Blha Blha

We are using it more and more every day at ${myDayJob}, and it helps us in every aspects of configuration management, deployment, and orchestration

If you still don't know it : Just GO check it out -> http://www.ansibleworks.com/docs/

One important aspect with making good Ansible playbooks is to respect idempotency

Wikipedia Source : Idempotence is the property of certain operations in mathematics and computer science, that can be applied multiple times without changing the result beyond

That means we want to be able to use a playbook to deploy, configure or update our full solution stack, but also make sure the already deployed blocks are conformed to what Ansible knows to be right about the topology and configuration, BUT without modifying any of those configurations if they comply with Ansible inventory/desired configuration.

# END Blha Blha Blha

When dealing with /etc/hosts files, we had a solution based on dynamically creating fragments from a template looking like bellow, and then using the assemble module to apply changes to the target servers :

        {% for k, v in hostvars.iteritems() -%}
        {{ hostvars[k]['ansible_default_ipv4']['address'] }}  {{ k }} {{ v['ansible_hostname'] }}
        {% endfor %}

The drawback with this method was that the already existing file on the target was deleted and copied over by the newly created one, (discarding any changes made locally on the target, but this is not the point here, and local changes are a bad habit any way), marking the task step as "changed" and the Ansible "PLAY RECAP" displaying this change.

We wanted to respect a true idempotence and get rid of those undesired/unnecessary modification.

After a few miss-steps we came up with the following solution :
 - lineinfile:  dest=/etc/hosts regexp='.*{{item}}$'
                line='${hostvars.${item}.ansible_default_ipv4.address} ${item}'
                state=present
 only_if: is_set('${hostvars.${item}.ansible_default_ipv4.address}')
 with_items: '{{groups.all}}'     
What it does is pretty straight forward :

  for all target hosts defined by the playbook
    for all hosts defined in the groups, set {{item}} value to its corresponding hostname
      find any line matching the regexp  '.*{{item}}$'  in /etc/hosts -> eg : '192.168.0.1 appServer1'
      if found check it matches the desired couple "hostname" "ip" (with "ip" being '${hostvars.${item}.ansible_default_ipv4.address})
          if it matches, don't change any thing
          else change it to the desired "hostname ip"
        else (if not found), add the desired "hostname ip" at the end of file

The "only_if" statement is used to skip hosts we couldn't get facts from (host couldn't be reached, etc ...), thus preventing from adding garbage info into the target /etc/hosts

Use it / Share it / Comment it / Improve it

---

Juts writing this quick post make me realize (once again) how power full is Ansible :
  - 3 lines of statements to described the desired configuration state and apply eventual changes
  - 7 lines to explain it in plain english (and not even sure I am cristal clear ...)

---

[EDIT] : Thanks to @rothgar

As  Justin mentioned in his comment, this syntax is now deprecated

Please use this one :

# Idempotent way to build a /etc/hosts file with Ansible using your Ansible hosts inventory for a source.
# Will include all hosts the playbook is run on.
# Inspired from http://xmeblog.blogspot.com/2013/06/ansible-dynamicaly-update-etchosts.html

- name: "Build hosts file"
  lineinfile:   dest=/etc/hosts 
                regexp='.*{{ item }}$
                line="{{ hostvars[item].ansible_default_ipv4.address }} {{item}}"
                state=present
  when: hostvars[item].ansible_default_ipv4.address is defined
  with_items: groups['all']

11 comments:

Richard said...

Very nice. Thank you.

Rothgar said...
This comment has been removed by the author.
Rothgar said...

This syntax is old and shouldn't be used anymore. Here's new syntax for using lineinfile to iterate over your hosts

https://gist.github.com/rothgar/8793800

- lineinfile: dest=/etc/hosts regexp='.*{{ item }}$' line="{{ hostvars[item]['ansible_default_ipv4']['address'] }} {{item}}" state=present
when: hostvars[item]['ansible_default_ipv4']['address'] is defined
with_items: groups['all']

Rowinson Gallego said...

I had to change it in Ansible 2.7.6:

- name: Add the inventory into /etc/hosts
lineinfile:
dest: /etc/hosts
regexp: '.*{{ item }}$'
line: "{{ hostvars[item]['ansible_default_ipv4']['address'] }} {{item}}"
state: present
when: hostvars[item]['ansible_facts']['default_ipv4'] is defined
with_items:
- "{{ groups['all'] }}"

thatsk said...

not working any one of above for ansible 2.7.4

hosts file
[local_host]
localhost

[local_host:vars]
ssh_key_filename="id_rsa"
remote_machine_username="root"
remote_machine_password="jas"
ansible_user=root
ansible_password=J@ckp0t5
ansible_port= "22"

[ansible_setup_passwordless_setup_group]
test12

[dbservers]
test1
test2


---
- hosts: localhost
gather_facts: false
connection: local
tasks:
- name: "Build hosts file"
lineinfile:
dest=/etc/hosts
regexp='.*{{ item }}$'
line="{{ hostvars[item]['ansible_default_ipv4']}}"
state=present
with_items: groups['dbservers']

ERROR:-
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: u\"hostvars['groups['dbservers']']\" is undefined\n\nThe error appears to have been in '/root/test.yaml': line 6, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n tasks:\n - name: \"Build hosts file\"\n ^ here\n"}

Azure DevOps said...

update more different ideas with us.
DevOps Online Training

Unknown said...

This syntax works with Ansible 2.8

- hosts: all
become: yes
tasks:
- name: "Build hosts file"
lineinfile: dest=/etc/hosts regexp='.*{{ item }}$' line="{{ hostvars[item].ansible_default_ipv4.address }} {{item}}" state=present
when: hostvars[item].ansible_default_ipv4.address is defined
with_items: "{{ groups['all'] }}"

Balajee Nanduri said...

Nice Post. Well Written. Keep sharing more and more DevOps Online Training
DevOps Online Training India
DevOps Online Training hyderabad

Sowmiya R said...

Good job! Fruitful article. I like this very much. It is very useful for my research. It shows your interest in this topic very well. I hope you will post some more information about the software. Please keep sharing!!

Oracle Training in Chennai | Certification | Online Training Course | Oracle Training in Bangalore | Certification | Online Training Course | Oracle Training in Hyderabad | Certification | Online Training Course | Oracle Training in Online | Oracle Certification Online Training Course | Hadoop Training in Chennai | Certification | Big Data Online Training Course

Anu said...


Wow, this is quite amazing. I think it would be that many people still find these stories.
DevOps Training in Bangalore | Certification | Online Training Course institute | DevOps Training in Hyderabad | Certification | Online Training Course institute | DevOps Training in Coimbatore | Certification | Online Training Course institute | DevOps Online Training | Certification | Devops Training Online

aaryan said...

aws solution architect training
azure solution architect certification
azure data engineer certification
openshift certification
oracle cloud integration training