While Ansible doesn’t guarantee idempotency, it is something you should strive for in your playbooks. It will not only allow you to smoothly re-run a playbook that failed half way through, but will also allow you to ensure at a glimpse that your playbook ran as you expected through accurate task change status. Luckily for us, Ansible provides many tools to help us reach this goal.
Implicit module parameters
A lot of the modules will actually handle it on their own.
For example, the “file” module does not have an “action” parameter listing options like “create” or “delete”
It instead has a “state” with values like “file”, “directory”, or “absent”.
This way, running the same action that ensures the /tmp/ziaconsulting folder exists twice will create it on the first pass and just state that it already exists on the second pass:
- hosts: myserver tasks: - name: "Ensure /tmp/ziaconsulting exists" file: path: /tmp/ziaconsulting state: directory - name: "Ensure /tmp/ziaconsulting exists a second time" file: path: /tmp/ziaconsulting state: directory
Mandatory module parameters
Some other modules will force you to enter a parameter describing the expected side effect.
It’s the case for the module win_package. When used to install a .exe file in a windows environment, it will force you to specify the product_id which can then be used to check that this is already installed or even uninstall it at a later date:
- hosts: mywindowsserver
tasks:
- name: Install Microsoft URL Rewrite Module 2.0 for IIS x64
win_package:
product_id: '{08F0318A-D113-4CF0-993E-50F191D397AD}'
path: https://download.microsoft.com/download/C/9/E/C9E8180D-4E51-40A6-A9BF-776990D8BCA9/rewrite_amd64.msi
arguments: /q /norestart
Optional module parameters
Some modules gives you a lot of freedom as to what you use them for. As such it’s hard for the module itself to guess whether they actually caused a change. A good example is the “shell” module which offers the “creates” parameter:
- hosts: myserver tasks: - name: "Leave your mark" shell: cmd: echo "`whoami` was here" > /tmp/`whoami` creates: /tmp/{{ ansible_user_id }} - name: "Leave your mark a second time" shell: cmd: echo "`whoami` was here" > /tmp/`whoami` creates: /tmp/{{ ansible_user_id }}
Changed_when
Similar to the “creates” parameter in the previous paragraph, all modules have the ability to override the task status using “changed_when”.
While often used as just “changed_when: False” for actions that are just intended to retrieve information and will never change anything, it is also possible to make use of variables in it. The following task runs a shell command to delete files older than a week in out temp folder and returns a proper change status based on whether any file was deleted at all:
- hosts: myserver tasks: - name: "Delete files that are more than a week old" shell: cmd: find /tmp/ziaconsulting -mtime +7 -type f -delete -print register: deleted_files changed_when: deleted_files.stdout != "" - name: "Delete files that are more than a week old a second time" shell: cmd: find /tmp/ziaconsulting -mtime +7 -type f -delete -print register: deleted_files changed_when: deleted_files.stdout != ""
Using templates
It is common to see playbooks that will copy a base file and then run replace regex over that file to customize it with things like the name of the server. The issue is that this will often mark both actions (the copy and the replace action) as changed, when the file itself will end up being identical.
The following example demonstrating this assumes that you have:
- “files/machine.txt” with the content “The current host name is:”
- “templates/machine.txt.j2” with the content “The current host name is: {{ ansible_hostname }}”
- hosts: myserver tasks: # Avoid using this approach - name: "Copy the base file across" copy: src: machine.txt dest: /tmp/ziaconsulting/machine.txt - name: "Update the mahine name" lineinfile: path: /tmp/ziaconsulting/machine.txt regexp: "^The current host name is:" line: "The current host name is: {{ ansible_hostname }}" - debug: msg="the file content is {{lookup('file', '/tmp/ziaconsulting/machine.txt') }}" # Avoid using this approach - name: "Copy the base file across a second time" copy: src: machine.txt dest: /tmp/ziaconsulting/machine.txt - name: "Update the mahine name a second time" lineinfile: path: /tmp/ziaconsulting/machine.txt regexp: "^The current host name is:" line: "The current host name is: {{ ansible_hostname }}" - debug: msg="the file content is {{lookup('file', '/tmp/ziaconsulting/machine.txt') }}" # Use this approach - name: "Create the whole file at once using templates" template: src: machine.txt.j2 dest: /tmp/ziaconsulting/machine.txt - debug: msg="the file content is {{lookup('file', '/tmp/ziaconsulting/machine.txt') }}"
Run conditionally
It is possible to decide that a specific task should only be run when specific conditions are met. For example, you might want to install several packages and then reboot once all of those are installed. But assuming that those modules were already installed, you would not want to reboot.
Ansible offers the possibility to specify “when”, and skip running this task if the conditions are not met:
- hosts: mywindowsserver
tasks:
- name: Install IIS
win_feature:
name: Web-Server
register: iisInstalled
- name: Install Microsoft URL Rewrite Module 2.0 for IIS x64
win_package:
product_id: '{08F0318A-D113-4CF0-993E-50F191D397AD}'
path: https://download.microsoft.com/download/C/9/E/C9E8180D-4E51-40A6-A9BF-776990D8BCA9/rewrite_amd64.msi
arguments: /q /norestart
register: iisRewriteInstalled
- name: Reboot server to complete installation
win_reboot:
when: iisInstalled.changed or iisRewriteInstalled.changed
Conclusion
While it can be tempting to stop your playbook as soon as you manage to have it install what you need, putting the extra effort will greatly increase performance, reliability, and will allow you to easily review what your job actually impacted, rather than just monitoring for failure.
View additional posts on Ansible here.