Understanding YAML for Ansible
If you write or use Ansible playbooks, then you're used to reading YAML configuration files. YAML can be deceptively simple and yet strangely overwhelming all at once, especially when you consider the endless possible Ansible modules at your disposal. It feels like it should be easy to jot down a few options in a YAML file and then run Ansible, but what options does your favorite module require? And why are some key-value pairs while others are lists?
YAML for Ansible can get complex, so understanding how Ansible modules translate to YAML is an important part of getting better at both. Before you can understand how YAML works for Ansible modules, you must understand the basics of YAML.
If you don't know the difference between a mapping block and a sequence block in YAML, read this quick introduction to the basics of YAML article.
Command syntax
Aside from ad-hoc use, Ansible is used through playbooks. A playbook is composed of one or more plays in an ordered list (a YAML sequence). Each play can run one or more tasks, and each task invokes an Ansible module.
Ansible modules are basically front-ends for commands. If you're familiar with the Linux terminal or Microsoft's Powershell, then you know how to construct a command using options (such as --long
or -s
) along with arguments (also called parameters).
Here's a simple example:
$ mkdir foo
This command uses the mkdir
command to create a directory called foo
.
An Ansible playbook also constructs commands. They're the same commands, but they're invoked using a different syntax than what you're used to in a terminal.
[ You might also enjoy: Getting started with Ansible ]
Ansible modules and YAML
As a task in an Ansible playbook, though, the syntax is a lot different. First, the play is given a name, which is a human-readable description of what is being performed. A play accepts many keywords, including hosts
to limit what hosts it's meant to run on and remote_user
to define the username Ansible must use to access remote hosts.
Keywords for plays are defined by Ansible itself, and there's a list of keys (and the types of information each expects as its value) available in the Ansible Play Keywords documentation.
These keys are not separate list items. In YAML terminology, they're mappings embedded in the play sequence.
Here's a simple play declaration:
---
- name: “Create a directory”
hosts: localhost
The final mapping block in a play declaration is the tasks
keyword, which opens up a new sequence to define what Ansible module the play is going to run, and with what arguments. It's here that you're using familiar commands in an unfamiliar, YAML-ified way. In fact, it's so unfamiliar to you that you probably need to read up on the module in order to discover what arguments it expects from you.
In this example, I use the builtin file module. From the module's documentation, you can see that the required parameter is path
, which expects a valid POSIX file path. Armed with that information, you can generate a very simple Ansible playbook that looks like this:
---
- name: "Create a directory"
hosts: localhost
tasks:
- name: "Instantiate"
file:
path: "foo"
If you're still getting used to the significance of YAML's indentation, notice that the task's name is not indented relative to tasks
because name
is the start of a new YAML sequence block (which, as it happens, serves as the value for the tasks
key). The word file
identifies what module is being used, which is part of the task definition, and path
is a required parameter of the file
module.
In other words, a play's task is a YAML sequence block (that is, an ordered list) of definitions invoking a module and its parameters.
You can test this play to verify that it works as expected, but first, run yamllint
on it to avoid any syntactical surprises:
$ yamllint folder.yaml || echo “fail”
$ ansible-playbook folder.yaml
[…]
TASK [Instantiate] ******************
fatal: [localhost]:
FAILED! => {“changed”: false,
“msg”: “file (foo) is absent, cannot continue” …
The playbook was processed, but the task failed. Reading through the parameter list of the file
module reveals that its behavior largely depends on the value of state
. Specifically, the default action is to return the status of path
.
Modify your sample YAML file to include a state
mapping:
---
- name: "Create a directory"
hosts: localhost
tasks:
- name: "Instantiate"
file:
path: "foo"
state: directory
Run it again for success:
$ yamllint folder.yaml || echo “fail”
$ ansible-playbook folder.yaml
[…]
$ ls
foo
Control modules
Not all Ansible modules map directly to a single command. Some modules modify how Ansible processes your playbook. For instance, the with_items
module enumerates items you want another module to operate upon. You might think of it as a sort of do while
or for
loop.
Its documentation indicates it only accepts one parameter: a list of items. A "list" in YAML terminology is a sequence, so you know without even looking at the sample code in the docs that each item must start with a dash space (-
).
Here's a new iteration of folder creation, this time with multiple subfolders (using the recurse
parameter in the file
module) and an extra parameter to set file permissions. Don't let the additional lines fool you. This is essentially the same code as before, only with extra parameters as described in the file
module documentation, plus the with_items
module to enable iteration:
---
- name: "Create directory structure"
hosts: localhost
tasks:
- name: "Instantiate"
file:
path: "{{ item }}"
recurse: true
mode: "u=rwx,g=rwx,o=r"
state: directory
with_items:
- "foo/src"
- "foo/dist"
- "foo/doc"
Run the playbook to see the results:
$ yamllint folder.yaml
$ ansible-playbook folder.yaml
[…]
$ ls foo
dist doc src
[ Need more on Ansible? Take a free technical overview course from Red Hat. Ansible Essentials: Simplicity in Automation Technical Overview. ]
Ansible principles
An Ansible playbook is a YAML sequence, which itself consists of mappings and sequences.
Playbooks also contain Ansible modules, each of which accepts parameters as defined by its developer. Both required and optional parameters are listed in a module's documentation.
To construct an Ansible playbook, start a YAML sequence that names the play, and then lists (in a sequence) one or more tasks. In each task, one or more modules may be invoked.
Pay close attention to indentation by understanding the type of data you're entering into your YAML file. It might help to avoid thinking of indentation as an indication of logical inheritance and, instead, to view each line as its YAML data type (that is, a sequence or a mapping).
Use yamllint
to verify your YAML files.
Once you understand the structure of a playbook, it's just a matter of following along with module documentation to execute the tasks you want your playbook to perform. There are hundreds of modules available, so start exploring them and see what amazing things you can do with this amazing tool.
Seth Kenlon
Seth Kenlon is a UNIX geek and free software enthusiast. More about me