Ansible Playbook Strategies

August 2017 · 4 minute read

Below I’ve outline four useful but less common Ansible strategies. My primary use cases for Ansible are application configuration, application deployment, and local development environments. The patterns aren’t overly technical, nor do they rely on hacks. But they do represent a few unusual problems Ansible can help solve.

Skip Module Parameters With Omit Filter

Say you have a playbook task that needs to accept multiple module parameters which are themselves incompatible. The omit filter allows you to control which parameters are passed to the module. The task below has one responsibility: write out an ssh deploy key to a known location. This task accepts input either as a string (the content param) or as a file (the src param). The Ansible copy module can operate with either the src or the content param, but not both at the same time. Instead of duplicating this task and adding a when clause to handle different conditions, use a dictionary and the omit filter.  The omit filter allows you to configure conditional module parameters.

- name: Deliver SSH deployment key
  copy:
    content: "{{ (item.key == 'content')|ternary(item.value, omit) }}"
    src: "{{ (item.key == 'src')|ternary(item.value, omit) }}"
    dest: "{{ deploy_base_dir }}/deploy_key"
    mode: 0400
  with_dict:
    src: "{{ deploy_key_path }}"
    content: "{{ deploy_key_content_as_string }}"
  when: item.value|trim != ""

(Beware of subtle behaviors: the last dictionary item prevails if all facts are set.) This strategy lets you process multiple input values with one task. It keeps playbook sprawl under control and avoids bugs arising from duplication of tasks to handle conditional inputs.

Run a Set of Tasks One Time Only

Certain system commands should typically be run only one time. A common example of a run-once situation is the  mysql_secure_installation command following an installation of MySQL. Specifically, resetting a root MySQL password if one exists may cause issues. This strategy utilizes the /root/ home folder to track state with a sentinel file. For example, if you store the root MySQL password at /root/.my.cnf Ansible can detect the presence of that file and only run a  mysql_secure_installation  routine if needed (i.e. the file doesn’t exist).

- name: Check for root MariaDB password existence
  stat: path=/root/.my.cnf
  register: mysql_root_password_file

- include: "mysql_secure_installation.yml"
  when: mysql_root_password_file.stat.exists != True

and the include:

---
# mysql_secure_installation.yml
#
# Adapted from
# https://github.com/PCextreme/ansible-role-mariadb/blob/master/tasks/mysql_secure_installation.yml
#  
# Note: This task generates a random root password if you don't provide a preferred password

- name: Generate a new mysql root password
  set_fact: vault_mysql_root_password="{{ lookup('pipe', 'openssl rand -base64 12') }}"
  when: vault_mysql_root_password is undefined

- name: Set root Password
  mysql_user: 
    name:     "root"
    host:     "{{ item }}"
    password: "{{ vault_mysql_root_password }}"
    state:    "present"
  with_items:
    - 127.0.0.1
    - ::1
    - localhost 

- name: Add root .my.cnf
  template: src=root.my.cnf.j2 dest=/root/.my.cnf mode=0600

- name: Reload privilege tables
  command: 'mysql -ne "{{ item }}"'
  with_items:
    - FLUSH PRIVILEGES
  changed_when: False

- name: Remove anonymous users
  command: 'mysql -ne "{{ item }}"'
  with_items:
    - DELETE FROM mysql.user WHERE User=''
  changed_when: False

- name: Disallow root login remotely
  command: 'mysql -ne "{{ item }}"'
  with_items:
    - DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1')
  changed_when: False

- name: Remove test database and access to it
  command: 'mysql -ne "{{ item }}"'
  with_items:
    - DROP DATABASE test
    - DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%'
  changed_when: False
  ignore_errors: True

- name: Reload privilege tables
  command: 'mysql -ne "{{ item }}"'
  with_items:
    - FLUSH PRIVILEGES
  changed_when: False

Run Command Only When Files Change

Sometimes you need to run a command only after new content arrives. A common Ansible strategy is to copy the files to the remote host and register a fact when the files changed.

- name: Install InCommon CA certs
  copy: content="{{ incommon_ca_certs }}" dest=/etc/pki/ca-trust/source/anchors/incommon-sha2.pem
  register: installed_new_ca_cert

- name: Update CA trusted cert bundle
  command: update-ca-trust
  when: installed_new_ca_cert.changed

The example above references the CentOS7 command update-ca-trust used to rebuild a cache of trusted CA certificates. It only needs to run when new CA certificate files arrive, not on every Ansible run.

It is possible to use Ansible to manipulate filesystems in this manner, but you might want to question why you need to change folders into symlinks in the first place. For me, it was a requirement of a web application which shipped with a folder where developers were to place custom code. We opted to place a symlink to a git repository located elsewhere in another code repo.

- name: Find folders to replace with symlinks
  stat: path={{ item }}
  register: folder_state
  with_items:
    - "{{ project_repo_dir }}/lib/local"

- name: Remove folders to replace with symlinks
  file: path={{ item.stat.path }} state=absent
  with_items: "{{ folder_state.results }}"
  when: item.stat.isdir is defined and item.stat.isdir

- name: Create new symlinks
  file: src={{ item.src }} dest={{ item.dest }} state="link"
  with_items:
    - { src: "/path/to/my_local_code", dest: "{{ project_repo_dir }}/lib/local" }

The tasks above apply if any folders need to be converted to symlinks. Otherwise, they’re skipped in subsequent playbook runs.