Skip to main content

Understanding YAML for Ansible

Ansible playbooks are written in YAML, YAML Ain't Markup Language. Understanding YAML syntax is a key to success with Ansible.
Image
YAML for Ansible
Image by Martinelle from Pixabay

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.

Topics:   Linux   Ansible   Automation   YAML  
Author’s photo

Seth Kenlon

Seth Kenlon is a UNIX geek and free software enthusiast. More about me

Try Red Hat Enterprise Linux

Download it at no charge from the Red Hat Developer program.