Automating cron jobs with the ansible cron module

Over the past month I have been rewriting some cron scripts to enhance monitoring and observability. I’ve also been refactoring my ansible
playbooks to handle deploying these scripts in a consistent fashion. Ansible ships with the cron module which makes this process a breeze.
The cron module has all of the familiar cron attributes (hour, minute, second, program to run, etc.) and takes the following form:

- name: Cron job to prune old elasticsearch indexes
  cron:
    name: cleanup-elasticsearch-indexes
    minute: 0
    hour: 0
    job: /scripts/curator/curator_clean_logs.sh
    state: present
    user: curator

When I first played around with this module I noticed that each playbook run would result in a cron entry being added. So instead of getting one curator log cleanup job when the play is executed I would get a one entry per run. This is obviously very bad. When I read back through the cron module documentation I came across this little nugget for the “name” parameter:

Note that if name is not set and state=present, then a new crontab entry will always be created, regardless of existing ones.

Ansible uses the name to tag the entry and if the tag already exists a new cron job won’t be added to the system (in case your interested this is implemented by the find_job() method in cron.py). Small subtleties like this really bring to light the importance of a robust test environment. I am currently using vagrant to solve this problem but there are also a number of solutions documented in the Ansible testing strategies guide.

Debugging ansible playbooks, plays and tasks

I am a long time ansible user and have wrangled it into automating just about everything I do. As my roles and playbooks have increased in quantity and size I’ve found it’s essential to have a good grasp of the debugging capabilities built into ansible. These are useful for detecting syntax errors, finding ordering issues and most importantly for learning how ansible works under the covers. In this post I’m going to cover a number of methods to test playbooks and troubleshoot issues when they pop up. In a future post I’ll go into some tips and tricks I’ve learned while developing new ansible modules. Here are some of my favorites:

1. Checking the syntax of your playbooks to find YAML problems

One of the first things I encountered while developing roles were YAML formatting subtleties. A space missing here, a hyphen missing there or a filter not returning the correct results. They all add up to a pissed off ansible-playbook run! I use atom for editing my playbooks though sometimes I’ve been known to fire up vim when I need to test something quickly from an SSH session. Ansible has a “–syntax-check” option which can be used to make sure your YAML is properly structured:

$ ansible-playbook --syntax-check sshd-disable-root-login.yml
ERROR! 'tsks' is not a valid attribute for a Play

The error appears to have been in
'/ansible/playbooks/sshd-disable-root-login.yml': line 2, column 3,
but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:

---
- hosts: localhost
  ^ here

The ansible check error output is extremely verbose and 99% of the time you will read the error and say doh!

2. Testing playbooks before they are committed to a git repository

Like most admins I use git repos for everything I touch. This allows me to see what changed, when it changed, and provides an easy way to share changes with my colleagues. Git provides a set of hooks which you can use to run commands at various stages of the commit process. You can read about hooks here. To ensure that my playbooks are syntactically correct prior to check-in I like to use a pre-commit hook to check syntax:

$ git commit -a -m "Added debug statement to playbook disable root login"
ERROR: Found a syntax error with ./playbooks/sshd-disable-root-login.yml
ERROR: Please run ansible-playbook --syntax-check
./playbooks/sshd-disable-root-login.yml to view the error

If I snarbled something git won’t let me commit the change and I get a simple error message that points me to the problematic file. This goes hand in-hand with a CI system to test your playbooks when they are committed.

3. Getting verbose output during playbook runs

Periodically issues arise and you want to see what ansible is doing when a playbook is applied. Running ansible with multiple verbose flags provides a significant amount of detail:

$ ansible-playbook -vvv playbooks/sshd-disable-root-login.yml
Using /ansible/ansible.cfg as config file

PLAYBOOK: sshd-disable-root-login.yml ******************************************
1 plays in playbooks/sshd-disable-root-login.yml

PLAY [localhost] ***************************************************************

TASK [Disable SSH root logins] *************************************************
task path: /ansible/playbooks/sshd-disable-root-login.yml:6
Using module file
/usr/lib/python2.7/site-packages/ansible/modules/core/files/lineinfile.py
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: ansible
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo
~/.ansible/tmp/ansible-tmp-1485817022.67-136944116724824 `" && echo
ansible-tmp-1485817022.67-136944116724824="` echo
~/.ansible/tmp/ansible-tmp-1485817022.67-136944116724824 `" ) && sleep
0'
<127.0.0.1> PUT /tmp/tmp2jJjuc TO
/home/ansible/.ansible/tmp/ansible-tmp-1485817022.67-136944116724824/lineinfile.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x
/home/ansible/.ansible/tmp/ansible-tmp-1485817022.67-136944116724824/
/home/ansible/.ansible/tmp/ansible-tmp-1485817022.67-136944116724824/lineinfile.py
&& sleep 0'
<127.0.0.1> EXEC /bin/sh -c 'sudo -H -S -n -u root /bin/sh -c
'"'"'echo BECOME-SUCCESS-ukbdiwfpcoxjqveqmvkrnhhjcorflacb;
/usr/bin/python2
/home/ansible/.ansible/tmp/ansible-tmp-1485817022.67-136944116724824/lineinfile.py;
rm -rf "/home/ansible/.ansible/tmp/ansible-tmp-1485817022.67-136944116724824/"
> /dev/null 2>&1'"'"' && sleep 0'
changed: [localhost] => {
    "backup": "",
    "changed": true,
    "diff": [
        {
            "after": "",
            "after_header": "/etc/ssh/sshd_config (content)",
            "before": "",
            "before_header": "/etc/ssh/sshd_config (content)"
        },
        {
            "after_header": "/etc/ssh/sshd_config (file attributes)",
            "before_header": "/etc/ssh/sshd_config (file attributes)"
        }
        ......
}

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0

In the output above you can see ansible perform a ESTABLISH, EXEC and PUT to get the ansiballz module deployed to a system. It also prints the playbook execution variables associated with the task. Super handy!

4. Using dry run mode to see what will change

In addition to syntax checking ansible has a “–check” option to perform a dry run. Modules that support the check feature will print the items they are going to change but won’t actually make any changes to your systems. Here is a sample run:

$ ansible-playbook --check playbooks/disable-root-sshd-login.yml

PLAY [control] *****************************************************************

TASK [setup] *******************************************************************
ok: [ansible]

TASK [Disable remote SSH logins as root] ***************************************
changed: [ansible]

PLAY RECAP *********************************************************************
ansible      : ok=2    changed=1    unreachable=0    failed=0   

In the output above we can see that the the second task will change the system configuration. If you have a complex ansible layout this option can be invaluable for helping you understand what is getting updated prior to anything changing.

5. Stepping through playbook execution

Another super useful ansible debugging option is the ability to step through your playbooks one task at a time. If your playbook contains a series of tasks that build off of each other this option can be used to stop execution so you can review what a task did to a system. To step through execution you can use the “–step” option:

$ ansible-playbook --step playbooks/disable-root-sshd-login.yml

PLAY [control] *****************************************************************
Perform task: TASK: setup (N)o/(y)es/(c)ontinue: y

Perform task: TASK: setup (N)o/(y)es/(c)ontinue: *******************************

TASK [setup] *******************************************************************
ok: [ansible.homefetch.net]
Perform task: TASK: Disable remote SSH logins as root (N)o/(y)es/(c)ontinue: y

Perform task: TASK: Disable remote SSH logins as root (N)o/(y)es/(c)ontinue: ***

TASK [Disable remote SSH logins as root] ***************************************
changed: [ansible.homefetch.net]

PLAY RECAP *********************************************************************
ansible.homefetch.net      : ok=2    changed=1    unreachable=0    failed=0   

Prior to each task running you will be prompted with an option to run the task, to ignore a task and one to run the rest of the playbook in its entirety. This is one of the best tools in my ansible bat belt.

6. Print variables and debugging information inside your playbooks

When you are testing new playbooks its super useful to be able to print debugging information during playbook runs. Ansible has a debug module which you can use to print text strings, variables and fact values. Variables are enclosed in {{ }} pairs and the list of system facts can be viewed with `ansible-playbook -m setup`. The following shows how debug can be used to print the network interfaces and the default IPv4 address for my control group:

---
- hosts: control
  gather_facts: yes
  tasks:
     - debug: 
         msg: "Network interfaces assigned to {{ inventory_hostname }}: {{ ansible_interfaces }}"
     - debug: 
         msg: "Default IPv4 address assign to {{ inventory_hostname }}: {{ ansible_default_ipv4.address }}"

If this playbook is run the values will be printed to stdout:

$ ansible-playbook playbooks/vars.yml 

PLAY [control] *****************************************************************

TASK [setup] *******************************************************************
ok: [ansible]

TASK [debug] *******************************************************************
ok: [ansible] => {
    "msg": "Network interfaces assigned to ansible: [u'lo', u'eno16780032']"
}

TASK [debug] *******************************************************************
ok: [ansible] => {
    "msg": "Default IPv4 address assign to ansible: 192.168.1.7"
}

PLAY RECAP *********************************************************************
ansible      : ok=3    changed=0    unreachable=0    failed=0   

If you are using the command and shell modules you can register the output from the command and print it using debug:

---
- hosts: control
  gather_facts: yes
  tasks:
    - command: ls -1 /tmp
      register: tmp_files

    - debug: 
        msg: "Files in /tmp: {{ tmp_files.stdout_lines }}"

$ ansible-playbook playbooks/vars.yml 

PLAY [control] *****************************************************************

TASK [setup] *******************************************************************
ok: [ansible]

TASK [command] *****************************************************************
changed: [ansible]

TASK [debug] *******************************************************************
ok: [ansible] => {
    "msg": "Files in /tmp: [u'ansible_dlMYA3', u'systemd-private-0778c17a65c04f01a4ba8765903a26fc-gorp.service-hc9Yf6', u'systemd-private-53253bf62ed94cfb9b4c5b14c5755193-gorp.service-5opN22']"
}

PLAY RECAP *********************************************************************
ansible      : ok=3    changed=1    unreachable=0    failed=0   

In the output above you can see a list that contains the files that currently reside in /tmp.

The debugging tips listed above are super useful but the best debugging tool is the ansible documentation. Reading every page and knowing exactly how modules are supposed to work will save you a lot of angst and pain. If you have any additional tips to share please leave me a comment. I would like to thank Jesse Keating for writing the AMAZING Mastering Ansible book. This well written book is chock full of great information!