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.

Pin It on Pinterest

Sharing is caring

Share this post with your friends!