The goal of a "play" is, like a sports team (let's say American football) to map players to their role on the field, and to an activity. In the context of IT, routers, servers, clouds,... are players. Roles are routing, firewall, blog, CMS, .... And activities could be: Update, migrate, re-route, copy file, ....
The IT professional writes playbooks to handle activities they care about. Playbooks are checked into source control and become the system of record.
Although one could get by using ansible to "execute" scripts to multiple hosts via Ansible modules, Playbooks are a structured way to organize interaction between equipment, their roles, and activities. Playbooks are defined in a .yml file and start with "---". For each play in a playbook, you get to choose what to target in your infrastructure and what remote user executes the tasks.
Where a playbook may have zero, one, or many plays. And a play may have zero, one, or many tasks. A task requires a call to a module in order to affect some change.
The above examples are just one way to declare remote_user after hosts. The
. Here is a short document that covers
.
Modules are a way of re-using code in Ansible. They can also directly invoked from the ansible command line via "-m":
The modules used above are: service, ping, and command. Here is a reboot activity done in a playbook:
---
- hosts: webservers
Playbooks are executed by the
ansible-playbook command.
Write a playbook
Let's build a "compliance" playbook that establishes the state "no errors should be in system logs." The biggest complication I had in learning Ansible was writing YAML and understanding its implicit syntax between Lists and Dictionary. These problems go away with experience and since writing YAML is foundational, it's important to get this working "for you" rather than "against you." So to get some experience, I found slowly "growing" (iterating on) the simplest playbook into something useful, helped me learn how to work with YAML. Jumping straight to the example playbooks listed above didn't give me the experience with YAML that I needed.
Here is the simplest possible playbook:
---
...
Run it:
Let's connect to a host by adding it as a list item.
---
- hosts: blogs
...
From a YAML syntax perspective, "- " means list. "hosts: blog" is a key (hosts) and value (blog).
Run it:
Since I didn't add the inventories, it couldn't resolve what "blogs" meant. So I added the inventory file (-i
) just like I did in the Enterprise Infrastructure as Code with Ansible article.
Now it is resolving the definition of "blogs" to a hostname. But it says the host is unreachable because I need to pass along the username via (-u
). (You can also embed that in the inventory or even playback. I chose to use the command line to pass the username so I can put my files into public source control without exposing my username.)
Success! Our first end-to-end playbook execution. You can get the code on
GitHub at this branch.
In YAML, "-hosts ..." is how a list is declared (dash with a space). Then "hosts : blogs" is a key and value pair. So Ansible loaded the YAML and accesses the list and looks for the key named "hosts." It then tried to resolve the value "blogs." Since there isn't a hostname literally named blog, it couldn't ignored that host until an inventory was referenced which defined "blogs."
Let's start checking logs
As you develop a playbook, keep the
Ansible Module documentation handy. Since we'll need to execute a shell script on our remote servers, let's do something easy to "kick the tires" of the shell module. (For tips for deciding between command, shell, and script module see the module notes
here and
here. In this case, we could start with the Command module but since we'll eventually want to use pipes we need to use the Shell module. ) Make the changes needed to have simplest_playbook.yml as the following:
---
- hosts: blogs
tasks:
- shell: echo "hello" > hello.txt
- shell: grep "hello" hello.txt
...
And test the file for errors:
It's happy with the YAML.
Syntax Sidebar:
"- " declares a sequence. "hosts: blogs" is a key and value pair.
"tasks: " maps the tasks to whatever follows, a sequence of more key values: "- " shell: ...:
Ansible requires the key following "tasks: " to be either "name: " or a module.
Note about YAML style:
In YAML the following two playbooks are equivalent. Normally, I advise people to use the most succinct style but I had a lot of confusion separating the sequence marks "- " from the mappings. Otherwise, at least to me, "- hosts: blogs" and "tasks:" don't seem to both be mappings. My brain keeps seeing the "- " and that keeps interfering with my understanding. If you agree with me, great. If you don't then reformat it to how you like. This article about YAML describes how to work with sequences and mappings in a general sense. It also was less confusing than the others.
This:
---
- hosts: blogs
tasks:
- shell: echo "hello" > hello.txt
- shell: grep "hello" hello.txt
...
Verses this:
---
-
hosts: blogs
tasks:
-
shell: echo "hello" > hello.txt
-
shell: grep "hello" hello.txt
...
Are equivalent.
Let's execute this against the remote host by telling ansible-playbook about our inventory and user:
Notice that it reports "OK." Notice the TASK [shell] which echoes back what module executed. More information can be sent to the console by describing what is happing with the call to the modules using the "name :" attribute before the call to the module:
---
-
hosts: blogs
tasks:
-
name: creating file
shell: echo "hello" > hello.txt
-
name: confirming it worked
shell: grep "hello" hello.txt
...
Notice the output about TASK contains what was mentioned by the "name: "mapping.
The play can be more DRY (Do not Repeat Yourself) by declaring a variable.
---
-
hosts: blogs
vars:
message: hello
tasks:
-
name: creating file
shell: echo {{message}} > hello.txt
-
name: confirming it worked
shell: grep {{message}} hello.txt
...
Check server logs
Now we can write a new playbook that checks that our system logs are error free. First, I developed the following by working and testing in a terminal window:
find /var/log -name "*.log" -type f -exec grep -i "error" {} + | grep -v "error_log" | wc -l | grep "0"
If Shell detects a command that returns a none zero code, it will signal there was an error.
(See
Ansible and error codes if you want the details.) The above is designed to return a 0 if there aren't any errors, or a non-zero if there are errors found in the log files.
---
-
hosts: blogs
tasks:
-
shell: find /var/log -name "*.log" -type f -exec grep -i "error" {} + | grep -v "error_log" | wc -l | grep "0"
And run it:
If your logs aren't clean, then the PLAY RECAP will tell you the results, which in this case is "failed." Ansible dumps a json file containing what was sent to Shell, what was sent to stderror, and so on. After cleaning the logs up (or you can cheat and chang the script to look for something more "unique" than error), run the command again.
When running the playbook, which runs top to bottom, hosts with failed tasks are taken out of the rotation for the entire playbook. If things fail, simply correct the playbook file and rerun.
Adding a "name: " mapping would make TASK output more sensible. "TASK [shell] ******" isn't a very useful message:
---
-
hosts: blogs
tasks:
-
name: Scanning Logs for error.
shell: find /var/log -name "*.log" -type f -exec grep -i "error" {} + | grep -v "error_log" | wc -l | grep "0"
...
Notice the yellow box below contains more meaningful output.
File Organization
A very basic organization is a directory that's checked into source control and containing:
- directory of staging inventory
- directory of production inventory
- playbooks
This is the organization used in the example code used in this tutorial. Building on that idea, most IT departments will need to add:
- directory of roles
- directory of group_vars
- directory of host_vars
And eventually, an organization with a mature use of Ansible will be creating their own modules for code reuse and will need directories for these files:
- library <- code="" li="" lives="" module="" the="" where="">
- module_utils <- common="" for="" li="" libraries="" live="" modules="" those="" where="">
- filter_plugins
Modules "should be idempotent" meaning running the module many times should have the same affect as running it once. They should be designed to check that the final state has already been achieved and if so, exit without performing any actions.
If you're curious to dig deeper into learning more about the more advanced levels of organization,
more details here.
Next Level Ansible
Once you've got some experience making a few playbooks, these topics that will take your Ansible work to the next level. Each other the below are an article by themselves.
Resources:
YAML syntax. The syntax doc is a short and sweet read.