Ansible: Checking and Controlling Task Execution, Orchestration and Scope
Tags: ansible, devops
Published: 2020-10-05 05:30:40

Ansible is a powerful tool that gives you great control over your infrastructure. When I developed the Ansible code to install and maintain my infrastructure at home, I wrote most scripts first as single files. This was good for some time - until you realized that the structure is getting complex and you find yourself looking for the script you needed for a considerable amount of time. Then I refactored my code base to a proper layout: A central playbook `site.yml` that contains all plays and roles. But now I was struggling how to run only specific tasks on specific nodes.

In this article, I will show you how to use the Ansible command line to control which tasks to execute, which hosts to target, and even custom orchestration of tasks. If you need a refresher on Ansible concepts, read the footnote[^1].

_This article originally appeared at [my blog](https://admantium.com/blog/a04_controlling_task_execution)_.

## Directory Layout of my Infrastructure at Home Project

As the running example, let me show you the directory layout and files from my infrastructure at home project.

At the top level, I have the following directories and files:

```bash
├── group_vars
├── host_vars
├── roles
├── scripts
├── site.yml
```

The `roles` define the basic software that gets installed on each node.

```bash
├── roles
│   ├── consul
│   └── nomad
│   └── dns
│   ├── docker-arch
│   ├── docker-arm
│   ├── nfs-client
│   ├── nfs-server
├── site.yml
```

And `scripts` include smaller tasks that I execute on a selected number of hosts, for example deploying nomad jobs or system maintenance tasks on the nodes:

```bash
├── scripts
│   ├── consul
│   ├── nomad
│   ├── tutorial
│   └── uninstall
│   ├── system
│   │   ├── clean_docker.yml
│   │   ├── connection_test.yml
│   │   ├── install_package_on_arch.yml
│   │   ├── install_package_on_debian.yml
│   │   ├── reboot.yml
│   │   ├── rotate_ssh_key.yaml
│   │   ├── shutdown.yml
│   │   └── update_packages.yml
```

All roles, and some scripts, are defined `site.yml`. This is the idempotent playbook that sets up all nodes with Docker, Nomad, Consul, DNS and NFS services.

So, how can I execute just a small set of tasks from the `site.yml` playbook, without running it completely? Read on...

## Check Playbooks before Execution

Before running a long list of tasks, it’s a good idea to check if your playbook works and that will happen. There are four different types of checks that I strongly recommend to do before you execute a script.

### Syntax Check

First, you should check the syntax with the flag `--check-syntax` if all scripts that will be used when executing a playbook.

```bash
>> ansible-playbook site.yml --syntax-check

OK playbook: site.yml
```

If you would have any error, for example in a role, you will get helpful information.

```bash
>> ansible-playbook site.yml --syntax-check
ERROR! Syntax Error while loading YAML.
  mapping values are not allowed in this context

The error appears to be in '[...]/roles/dns/tasks/main.yml': line 7, column 15, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:

  - name: Configure dnsmasq
      template:
              ^ here
```

### List Affected Hosts

Second, take a look at the hosts that will be affected.

```bash
>> ansible-playbook site.yml --list-hosts

playbook: site.yml

  play #1 (all): Install consul TAGS: [consul]
    pattern: ['all']
    hosts (6):
      raspi-3-1
      raspi-0
      raspi-4-1
      minas
      raspi-4-2
      raspi-3-2
```

### List all Tasks

Third, take a detailed look into the tasks that will be executed with the flag `--list-tasks`.

```bash
>> ansible-playbook site.yml --list-tasks

playbook: site.yml

  play #1 (all): Install consul TAGS: [consul]
    tasks:
      command   TAGS: [consul]
      consul : Check if current consul version is installed     TAGS: [consul]
      consul : Set vars when architecture is armv7l     TAGS: [consul]
      consul : Set vars when architecture is armv6l     TAGS: [consul]
      consul : Set vars when architecture is x86_64     TAGS: [consul]
      consul : Create consul group      TAGS: [consul]
      consul : Create consul user       TAGS: [consul]
      consul : Create consul dir        TAGS: [consul]
      consul : Get consul binary        TAGS: [consul]
      consul : Unzip consul binary      TAGS: [consul]
      consul : Create symlink           TAGS: [consul]
```

### Dry Run to Detail all Changes

Finally, you can make a dry run and see each the result of each step applied to your nodes with `--check --diff`. This check is especially important! Run it whenever you are modifying important system files, for example as shown here to add a new line to the `/etc/fstab` file.

```bash
>> ansible-playbook site.yml --check --diff


TASK [nfs-server : Install software] ****************************************
The following NEW packages will be installed:
  nfs-kernel-server
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
changed: [minas]

TASK [nfs-server : Create mount dir] ****************************************
--- before
+++ after
@@ -1,4 +1,4 @@
 {
     "path": "/mnt/nfs",
-    "state": "absent"
+    "state": "directory"
 }

changed: [minas]

TASK [nfs-server : Create fstab entry] ****************************************
--- before: /etc/fstab (content)
+++ after: /etc/fstab (content)
@@ -8,3 +8,4 @@
 UUID=BB43-B9B6                            /boot/efi      vfat    defaults,noatime 0 2
 UUID=1ece07a6-5072-4f25-aed0-27e01b0ded17 /              ext4    defaults,noatime 0 1
 192.168.2.203:/mnt/nfs  /mnt/nfs nfs defaults,soft,bg,noauto,rsize=32768,wsize=32768,noatime  0 0
+/dev/sda1 /mnt/nfs ext3 defaults,user 0 1

changed: [minas]

TASK [nfs-server : Configure nfs access] ****************************************
--- before: /etc/exports (content)
+++ after: /etc/exports (content)
@@ -0,0 +1 @@
+/mnt/nfs 192.168.2.0/24(rw,sync,no_subtree_check,no_root_squash)

changed: [minas]
```

Ok! At this point, you have a good understanding of what your playbook will change. Lets continue.

## Controlling Task Execution

Once you are sure that the tasks do exactly what they should do, and they target the correct hosts, lets execute them. For example, inside my `site.yml`, I have a play to configure the DNS resolution on all nodes.

```yaml
- name: Configure DNS
  hosts:
    - all
  tags:
    - dns
  become: true
  roles:
    - dns
```

However, this play is placed in the middle of the file. How can I just execute this specific play?

### Executing Specific Plays

The best option is to apply and use tags. Tags are any, well tags, that you apply to roles or tasks inside the plays. As shown above, I use a plain "tag to role" declaration. To only execute plays with a specific tag, use the `--tags` flag. You can pass a single or a list of tags to this flag.

Now, if I only want to run the DNS play, I execute `ansible-playbook site.yml --tags consul`. Another option is to use its name, like `ansible-playbook site.yml --start-at-task="Install consul"`, but tags are preferred.

### Executing Specific Tasks inside a Play

What if the play you want to start has several subtasks, and you don’t want to execute them all? Just add the flag `--step`. Before any task is performed, a prompt is opened and will ask you if you want to continue. Answer the question, or stop the playbook execution anytime by entering `ctrl-c`.

```bash
ansible-playbook -i hosts site.yml --tags "dns" --step

PLAY [Install consul]
****************************************


PLAY [Install nomad]
****************************************


PLAY [Configure DNS]
****************************************

Perform task: TASK: dns : Install dnsmasq on raspi-3-1 (N)o/(y)es/(c)ontinue:
```

### Executing Tasks only on specific Hosts

When you want to live test a play, it is useful to just modify one host before making a change to your complete inventory. The helpful `--limit` is what you need to use.

```bash
>> ansible-playbook -i hosts site.yml --tags "dns" --limit minas

TASK [dns : Install dnsmasq]
****************************************

ok: [minas]

TASK [dns : Configure dnsmasq]
ok: [minas]
```

Now you have learned all about picking a set of tasks from a central playbook. There is only one thing remaining.

### Orchestrating Tasks

When you execute a playbook, in each play, all roles will be executed first, followed by the tasks, top down in the order that they are written. If you want to orchestrate them in a different way, tags can help you as well. You can apply multiple tags to any task and any role. For example, all tasks that will install software could be tagged as `install`, and all tasks that will copy config files could be tagged `config`. Then, just execute the playbook with those tags: `ansible-playbook site.yml --tags "install, config"`.

## Conclusion

This article showed you how to work with complex Ansible playbooks more effectively. First, how to check a playbook before execution. You learned how to perform a syntax check, how to see the list of tasks and affected hosts, and how to make a dry run. The dry run is especially important when your tasks affect important system configuration files. Develop a healthy habit to apply the `--check` flag often. Second, you learned how the execute only specific plays and tasks inside plays. Tags give you great flexibility. Apply them, similarly named as the tasks and roles, inside your playbook. And then think one level above, identify the tasks that modify config files, tasks that install software, and give them grouped tags. Once you have tags in place, you can execute them specifically. Also, combine this with the `--step` flag to be prompted about the tasks that are executed. And when you define tags consistently, you can orchestrate the playbook execution like you want.

## Footnotes

[^1]: Here are the details:
  - Playbook: A collection of plays
  - Plays: A set of tasks that run on a set of hosts
  - Tasks: Concrete actions that are applied to a host
  - Role: A set of tasks that provision a host to perform a self-defined responsibility