Ansible is a very powerful configuration management tool. It can be used to deploy settings and software to one or more (virtual) machines. Using a CM-tool ensures that every deployment is done in exactly the same way. This doesn’t only make your job as a system engineer a whole lot easier, it also results in a more stable environment since every machine is consistent and more predictable. I’m using Ansible intensively for quite some time now and found that it was time to write a post about some small, but usually hard to find or annoying to solve, problems or challenges.
Handlers
Handles will execute a certain action when a task ends with a status changed. Handlers are very powerful and useful since they aren’t executed every time they are called. Instead, the action required by the handler will be executed only once at the end of the play.
Keep all handlers in one file
Every role which you define in Ansible can contain a certain set of handlers. In a lot of cases, you end up with defining the same handler (for example: restart iptables or reload systemd) multiple times, for every role. Personally, I find it easier to have one file containing all my handlers and include that file from a specific role.
Create new roles with a main.yml in handlers/ containing the following:
[jendep@be-plw-ans-0001 roles]$ cat /etc/ansible/playbooks/roles/apache/handlers/main.yml - include: ../../handlers.yml
Define all handlers in /etc/playbooks/roles/handlers.yml:
[jendep@be-plw-ans-0001 roles]$ cat /etc/ansible/playbooks/roles/handlers.yml - name: reload postfix service: name=postfix state=restarted - name: reload aliases command: newaliases - name: reload exports command: exportfs -ra - name: restart proftpd action: service name=proftpd state=restarted - name: restart xinetd action: service name=xinetd state=restarted ...
Define a handler which suits different environments
When you’re creating roles and deploy them to different types of systems or different operating system versions, soon or later you’ll run into the problem that, for example, a name of a service is different on another distribution (for example: Apache on RHEL-based distributions is called http or the change to mariaDB in CentOS 7 from MySQL in CentOS 6). Theoretically, you could define each task which requires such kind of handler multiple times but that’s not really helpful for creating clean and clear roles.
To overcome this problem: call two handlers, one for each possible scenario, from where you would normally call your handler in your task:
- name: copy .my.cnf for root template: src=my.cnf.j2 dest=/root/.my.cnf owner=root mode=0600 notify: - restart mysqld - restart mariadb
Define two handlers but put the condition to determine the action inside the handlers-file:
- name: restart mysqld service: name=mysqld state=restarted when: ansible_distribution_major_version =='6' - name: restart mariadb service: name=mariadb state=restarted when: ansible_distribution_major_version =='7'
Variables
Variables are key in Ansible and when you know how to flexibly use variables, a whole lot of possibilities become available.
Set variables dynamically in a task
When you want to let a variable depend on another on or just on a fact, you can set the variable on the fly in the task using the variable. This can be done with set_fact:
- name: use Spacewalk in DMZ when needed set_fact: spacewalk_server={{dmz_spacewalk_server}} when: dmz
When you want to set a default for a variable, you can do this as follows:
- set_fact: ssh_permittunnel_value: 'yes' when: ssh_permittunnel - name: sshd - permit tunnel lineinfile: dest=/etc/ssh/sshd_config regexp="^PermitTunnel.*" state=present line="PermitTunnel {{ssh_permittunnel_value|default('no')}}" notify: restart sshd
Specify multiple parameters in with_items
With_items for a task allows you to execute the task multiple times with different values. In some cases, you want to execute a task multiple times but use several different (but connected) values.
You can use the following example to specify multiple parameters in with_items:
- name: create users user: name={{ item.name }} group={{ item.group }} state=present shell={{ item.shell }} with_items: - { name: 'user1', group: 'group1', shell: '/bin/bash' } - { name: 'daemon-user', group: 'group2', shell: '/sbin/nologin' }
Define empty lists
As with the previous example, you could also have cases where you use with_items combined with a list defined in the inventory. If the list isn’t defined, you will receive an error when executing the playbook. To overcome this problem but still not execute the task, you can define an empty list in the inventory when needed.
Imagine the following task:
- name: create users user: name={{ item }} state=present with_items: user_list
When in a certain case, you do not need to create users, you can define the list as follows in the inventory
user_list: []
An alternative to this, is to check if the variable used in with_items is defined but that requires more work and makes the playbook less clear:
- name: create users user: name={{ item }} state=present with_items: user_list when: user_list is defined
Substring a variabele in Ansible
It happens often that you only require a part of the value of a variable. In such case you can substring the value to get only parts of it. A use case could be version numbering. Sometimes a version is written as x.y and in another place as xy (for example package postgresql94-server install service postgresql-9.4.
The following task:
- debug: var="full variable {{ansible_os_family}}" - debug: var="using substring the {{ansible_os_family[3:7]}} is {{ansible_os_family[:3]}} "
Gets you the following result:
full variable RedHat using substring the Hat is Red
Calculate variables
Another manipulation with variables that’s often used is calculating variable values. As with most of the examples here, it’s quite easy to use but not always easy to find the correct syntax:
- name: configure memlock limits lineinfile: dest=/etc/security/limits.conf line="soft memlock {{ansible_memtotal_mb * 950000 }}" state=present
Often, you get a result in a different format than you require. For example a float after dividing where you need a integer. You can round variables using round or convert them to int as follows:
- name: set number of hugepages sysctl: name=vm.nr_hugepages value="{{(ansible_memtotal_mb/4)|int}}" state=present reload=no
Get the IP of a hostname inside a playbook
While this trick isn’t very Ansible-related it is something I use quite often. For some components, you require the IP-address of a hostname which you need to enter elsewhere, think of configuring a cluster or such. While I could define two variables myself, I find it better to define the hostname to use and search for the IP:
- name: get IP of testmachine shell: "dig +short testmachine.test.dom| awk '{ print ; exit }'" register: ip_test always_run: yes changed_when: False debug: msg="ip of testmachine is {{ ip_test.stdout }}"
In the above example, the hostname is testmachine.test.dom but it could be a public hostname or a variable.
Use subitems in lists
One things which I find very powerful but at the same time also complex, is the use of nested lists. Complex nested lists can be used with with_subelements but the more simple version is by using a list that defines key-value items.
Imagine the following list:
- prod_list: - prod_name: "unknown product" prod_serial: "123456" prod_location: /opt/product1" - prod_name: "popular product" prod_serial: "987654" prod_location: /opt/product2"
The values in the list can be used in a with_items loop in a very user-friendly way:
debug: msg="product name {{item.prod_name}} serial {{item.prod_serial}} location {{item.prod_location}}" with_items: prod_list
Templates
Templates are mainly used to create or modify files that have small parameters at the destination. Templates are written as Jinja2 and there is good documentation available. Nevertheless, here are some examples:
Looping a list
Using lists is also possible in templates. To do so, use the following example:
Imagine the list as I used in the example above. To use this list in a template:
{% for product in prod_list %} [Product {{loop.index}}] name={{product.prod_name}} serial={{product.prod_serial}} location={{product.prod_location}} {% endfor %}
This will generate a file containing the following:
[Product 1] name=unknown product serial=123456 location=/opt/product1 [Product 2] name=popular product serial=987654 location=/opt/product2
{{loop.index}} gives you the index of the loop
{{loop.last}} can be used to test if this is the last item in the loop (for example to close some brackets).
To combine lists in a loop as above, you can use the following syntax:
{% for product in standard_products|list + extra_products|list %}
Various
Execute an action only when another action was executed
It happens often that something only needs to be done if a previous step was executed. For example, run a first initialization of product only when it was just installed.
This can be realized by using a a register and the .changed of that register.
A small example:
- name: register with spacewalk base-channel rhn_register: server_url=http://{{ spacewalk_server }}/XMLRPC activationkey={{ spacewalk_key }} register: spacewalkregistration - name: clean up Yum command: yum clean all when: spacewalkregistration.changed
Manage known_hosts with Ansible
Doing things via Ansible often requires a different approach since you’re running everything as batch and can’t give input to questions or confirmations. To allow access to a host over SSH, it needs to be added to the known_hosts for the user accessing the system.
You can use the following example to maintain known_host-entries
- name: make sure known_hosts exists file: path=/home/testuser/known_hosts owner=testuser group=testgroup mode=600 state=touch changed_when: False - name: check if hosts are already in known_hosts shell: "ssh-keygen -f /home/testuser/known_hosts -F {{item}}" with_items: - testhost1.test.dom - testhost2.test.dom - 192.168.202.100 register: ssh_known_hosts ignore_errors: True always_run: yes changed_when: False - name: add hosts to known_hosts shell: 'ssh-keyscan -H {{item.host}}>> /home/testuser/known_hosts' with_items: - { index: 0, host: testhost1.test.dom } - { index: 1, host: testhost2.test.dom } - { index: 2, host: 192.168.202.100 } when: ssh_known_hosts.results[{{item.index}}].rc == 1
Execute a script and check the execution
A lot of functionality exists in the Ansible modules but sometimes you need to execute a small script on a target-machine in order to initiate something or just because it’s needed.
One way is to set the return code of the script (Ansible considers everything >0 as failed) but not all scripts handle return codes as they should.
With the following example, you can set the status of the task depending on the output of the script that was ran:
name: add this host to the list shell: /tmp/testscript.sh register: testscript_result changed_when: testscript_result.stdout.find("was added") == 1 failed_when: testscript_result.stdout.find("error adding") == 1
Open one or more ports with iptables
As most of you have noticed, there isn’t any Ansible module for iptables and there are no plans to make one. There are good reasons to not have such module but the work needs to be done so this is how I open up ports in the firewall with Ansible:
To open up one port:
- name: iptables - open TCP port 8080 lineinfile: dest=/etc/sysconfig/iptables line="-A INPUT -p tcp -m state --state NEW -m tcp --dport 8080 -j ACCEPT" regexp="(?=.*\btcp\b)(?=.*\bdport\b)(?=.*\b8080\b)" state=present insertbefore="(INPUT).*(REJECT)" notify: reload iptables
To open up a list of ports:
- name: iptables - open ports lineinfile: dest=/etc/sysconfig/iptables line="-A INPUT -p {{item.protocol}} -m state --state NEW -m {{item.protocol}} --dport {{item.port}} -j ACCEPT" regexp="(?=.*\b{{item.protocol}}\b)(?=.*\bdport\b)(?=.*\b{{item.port}}\b)" state=present insertbefore="(INPUT).*(REJECT)" with_items: - { protocol: "udp", port: "5404" } - { protocol: "udp", port: "5405" } - { protocol: "tcp", port: "2224" } notify: reload iptables
The handler reload iptables is just a restart of iptables:
- name: reload iptables service: name=iptables state=reloaded
Install SELinux modules idempotent
Ansible comes with a module to control SELinux booleans but in some cases, a custom SELinux is required. Since there isn’t any module to do this (I should find some time and create a pull request for this), I use the following method to idem-potently add SELinux modules:
- name: SELinux - check loaded modules shell: semodule -l register: selinuxmodules always_run: yes changed_when: False - name: copy SELinux modules for snmpd access to /tmp action: copy src=snmpd_custom.pp dest=/tmp/{{item}}.pp owner=root mode=600 when: "selinuxmodules.stdout.find(snmpd_custom') == -1" - name: install SELinux module for snmpd access command: semodule -i /tmp/snmpd_custom.pp when: "selinuxmodules.stdout.find(snmpd_custom') == -1"
Configure SELinux ports idempotent
Same thing as above is valid to allow non-standard ports in SELinux in a way that is safe to execute multiple times:
- name: check SELinux allowed ports for LDAP shell: semanage port -l|egrep "^ldap_port_t"|egrep "\b2389\b" register: selinuxldapports always_run: yes changed_when: False ignore_errors: True - name: configure SELinux to allow non-standard ldap-port command: semanage port -a -t ldap_port_t -p tcp 2389 when: selinuxldapports.stdout.find("2389") == -1
Include specific task files
Altough this is well documented, I wanted to mention it here because it can come in very handy.
Imagine you want to create a role to deploy different Oracle Java versions. The role is defined in /etc/ansible/playbooks/java and there is a variable for every version to determine what to install.
One way would be to go trough all steps for every version and add a version-condition to all of them. Another way is to use includes:
The java-role will have a tasks/main.yml which is executed by default can contains statements like this:
- include: oracle_java_17.yml when: instal_oracle_java_17 - include: oracle_java_18.yml when: instal_oracle_java_18
This allows you to keep a neat main.yml and specific task-files in /tasks/oracle_java_1*.yml where you are more specific. The condition only needs to be written on the include statement and not for every statement in the included file.
I also use this for roles which differ a lot between different distributions.
Use host-spefic files or templates
As with the above example, this is also documented but very useful.
Image a situation where you need to copy a configuration file in XML that’s completely different for every host you’re deploying to. You can’t really define 100’s of lines as a variable for every line in the file and there is no way that you find enough similarities to create a template. For such case, it can be easy to create a file or folder with the name of the host to deploy to and use the name of the host in the path:
- name: configure license.xml copy: src={{ ansible_hostname }}/license.xml dest=/opt/productX/license.xml owner=prod_X mode=644
Replace or remove a word in a line with lineinfile
This one is more of a regular expressions and syntax thing but took me some time to figure out correctly.
To replace words in an existing, unpredictable, line of a file, you can use a regex with backrefs.
Example: remove a kernel parameter
- name: GRUB (el7) - remove kernel parameter rhgb lineinfile: dest=/etc/default/grub state=present regexp='^(.*)rhgb(.*)$' line='\1\2' backrefs=yes notify: update grub2 when: ansible_distribution_major_version =='7'
Check if a file or directory exists
Checking if a file or directory exists sound a little weird but if you think a little further, it can come in very useful to check if something has been installed or if a certain script has already been executed. In other ways, to make non-repeatable scripts or commands idempotent.
Example: move a file if it exists
- name: check if testfile exists stat: path=/var/lib/testfile register: testfilestat - name: move testfile to disable it command: mv /var/lib/testfile /var/lib/testfile.disabled when: testfilestat.stat.exists
stat.isdir can also be used to check if something is a directory
Example: check if a custom program is installed to prevent multiple executions of the installer (another way to do the same as above):
- name: check if product X is installed shell: 'if [[ -f /opt/productX/VERSION ]]; then echo "ok"; else echo "empty"; fi' register: installstatus always_run: yes changed_when: False - name: unpack installation unarchive: src=productX.tar.gz dest=/opt/ when: installstatus.stdout.find("ok") == -1 - name: run installation of productX shell: /opt/productX/install.sh>>/tmp/productx-install.log when: installstatus.stdout.find("ok") == -1
More tips & tricks
Hopefully the above examples and small explanations can help you to get even more out of Ansible. Feel free to mention common issues with or without their solution because I’m sure the above list is far from complete.
Hello thanks for the write-up. As for the iptables part, I created a role to ease the iptables rules management: https://github.com/mikegleasonjr/ansible-role-firewall
Great article and one of the few for Ansible tricks and tips. I like the iptables suggestion. I use UFW which is still super easy to handle with the ‘ufw’ module.
Not bad! :) Thanks
The known hosts snipped can now be replaced with the known_hosts module. http://docs.ansible.com/ansible/known_hosts_module.html
Your idempotent approach for managing SELinux modules is very useful. Thanks.
You are looking to be an expert in ansible. Would you like to solve the following problem:
– there is a role that should replase some text in a csv file
– in defaults there are many variables(one for each column) that read a line from a csv file to replace values in particular columns, like that
myvar: “{{ lookup( ‘csvfile’, ‘{{ hier }} file={{ path }}/{{ name }} delimiter=; col=1 encoding=utf-8’) }}”
– the row where the text is to replace is erased in csv file
– use lineinfile with the variables from defaults section to write a new line
Problem:
cvs file holds [] instead of content of the read variable, it is not the scope, the variable exists and has content, one can see the content with debug message – it is the entry from the read csv file.
i have no ideas any more, that the problem could be, do you have one?
Great tips !!! Thanks a lot you saved me lot of time digging around :)
Yes, man i know the solution, dont use csv files!
Pingback: Ansible top tips – DevOps meets QA