Preface: I have no great love for Ansible, but nor do I want to rag on it excessively; I’ve chosen to roll it out at the company I work for, and we’ll likely keep it even as we switch to the “shorter-lived, disposable VMs” model using something like Terraform + Packer.

I gave a presentation in September called “Ansible: A Puppet User’s Perspective” that elaborates on the problems I see with both Ansible and Puppet, in the context of the “long-lived system” model they were both built for.

Ansible has a hash-in-a-string format that, in the docs, may be better known as “key=value” options for module actions. They look a bit like this:

tasks:
- name: make sure apache is running
  service: name=httpd state=running

Every playbook file is a YAML file; what we have above is a list of one task hash that has two keys (name and service), with a string value for each.

Formatted properly, the above looks like:

tasks:
- name: "make sure apache is running"
  service: "name=httpd state=running"

The Problem

What’s going on is that Ansible has its own key=value parser that it runs over the strings, which then runs the Jinja2 templating engine over.

This is fine and dandy if you have plain arguments, but as is common in Ansible, you often want to interpolate in variables. Here’s a simple example:

tasks:
- name: "Install basic packages"
  apt: pkg={{ item }} state=installed
  with_items:
  - screen
  - vim
  - cowsay

# Interpreted as:
# apt: pkg=screen state=installed
# apt: pkg=vim state=installed
# etc.

Here’s a not-so-simple example:

tasks:
- copy: "dest=/tmp/1.txt content={{ contains_quote }}"
# ^-- Broken; interpreted as:
#       copy: dest=/tmp/1.txt content=Hello"World
#     (Throws "try quoting the entire line" error.)

- copy: dest=/tmp/2.txt content="{{ contains_quote }}"
# ^-- Also broken, interpreted as:
#       copy: dest=/tmp/2.txt content="Hello"World"

- copy: dest=/tmp/3.txt content='{{ contains_quote }}'
# ^-- "Fixed" by swapping " for ' quotes, but...

- copy: dest=/tmp/4.txt content='{{ contains_alt_quote }}'
# ^-- Also broken, interpreted as:
#       copy: dest=/tmp/4.txt content='Hello'World'

vars:
  contains_quote: 'Hello"World'
  contains_alt_quote: "Hello'World"

Same goes for things like campfire: room=example subscription=example token=example msg="{{ contains_a_quote }}"; it’s a fruitless game of whack-a-mole.

The Fix

Stick to using YAMLs own features for structured data; use a hash:

tasks:
- name: "Long-form Hash"
  service:
    dest: "/tmp/whatever.txt"
    content: "{{ contains_quotes_or_other_things }}"

- name: "Inline Hash"
  service: { name: "httpd", state: "running" }

And if you’re using something like shell or command that expect a string and key=value arguments (eg. shell: "something --file /home/foo/bar.txt creates=/home/foo/bar.txt"), you can instead provide an args key:

tasks:
- name: "A complicated command meant to only run once, which leaves a file around when it's finished."
  shell: "something --file /home/foo/bar.txt -o -m -g"
  args:
    creates: "/home/foo/bar.txt"

Why not use …

  • service: name=httpd state=running sometimes, and
  • service: { name: "{{ some_variable }}", state: "running" } other times when needed for variable interpolation?

Consistency, in an attempt to increase safety; sticking to an “Always use YAML hashes” rule means one less thing to forget about or train the new ops people up on.

Remembering the special rules for key=value can be done, yes, but it’s another edge case in a tool that’s arguably already full of unsafe edge cases.