Using Ansible to automate local tasks and setup Jun 19 2019

We can use an automation tool like Ansible to configure servers, set up CI/CD pipelines, and many more DevOpsy tasks.

This same tool can be used to maintain the setup of our day to day environment (as suggested by my friend Gerardo Santoveña) or automate tasks. We can create playbooks that contain the instructions for completing tasks or defining the state you want your environment to have. In this post, I’ll show you Ansible’s basics. By the end of the article, you’ll have enough knowledge to start automating some of your everyday tasks or maybe even create a playbook to set up your environment.

Concepts

Let’s go through some basic concepts that will help us better understand the Ansible workflow.

Ansible uses an inventory file to keep track of the hosts that you want to manage so they can be easily referenced by name in your ansible playbooks.

Playbooks are, in simple terms, files that describe the hosts, the tasks to execute on the hosts and some other properties.

If you want to work with remote servers, you add them in your inventory file, located by default in /etc/ansible/hosts. We are not going to work with remote servers on our examples to keep things simple, but everything applies the same to remote hosts, you just need to reference the remote servers instead of using localhost. Because we won’t define any remote servers, we don’t need to add anything to our inventory. But for the sake of completeness, if you want to have a glimpse of what information is contained on the inventory file, let’s have a look.

Inventory file

The default location for our inventory file is: /etc/ansible/hosts if it’s not created you can create it. The inventory contains sever-names, group definitions, ports and variables. The inventory file can be in INI format or in YML. Let’s look at an inventory file in ini format :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#makes Ansible aware of the server mail.test.com
mail.test.com
# makes Ansible aware of db.test.com that uses the non-standard port for SSH 49001
db.test.com:49001
# we can create an alias for an IP, ansible_port is not required if we use the default ssh port 22 but just added it as an example
loadbalancer ansible_host:10.0.10.1 ansible_port:22

#make a group of all server named infrastructure
[infrastructure]
mail.test.com
db.test.com
loadbalancer

# Create a group of two servers that run the main app
[app]
10.0.10.2 PGPASSWORD=test123 # don't use a password like that
10.0.10.3 PGPASSWORD=dUV7$g9%PdTTLnkFo!I

Now you can reference those hosts on your Playbooks or via the ansible command-line tool. If you want to learn more visit the official documentation working with inventory.

Run commands on local connection

Normally, we would want to manage remote hosts, Ansible uses ssh for this. But we want to work on our local computer, to do this we can directly execute commands passing the flag -c local to the ansible command. First, let’s see the command to obtain the disk space of a remote computer called remote_server, it would look something like this:

1
$   ansible remote_server -a "df -h"

Because we will be working on our local computer, as I mentioned before, we will use a local connection, and the command will look like the following:

1
$   ansible localhost -c local -a "df -h"

The -c flag with the argument local makes it run in a local connection. There are other types of connections(for ssh: ssh, smart, paramiko. Non-ssh: local, docker) but we’ll use local for the current example. After running the command, we should see something similar to the following:

1
2
3
4
5
6
7
8
localhost | CHANGED | rc=0 >>
Filesystem      Size   Used  Avail Capacity iused               ifree %iused  Mounted on
/dev/disk1s1   932Gi  550Gi  374Gi    60% 3841305 9223372036850934502    0%   /
devfs          202Ki  202Ki    0Bi   100%     698                   0  100%   /dev
/dev/disk1s4   932Gi  7.0Gi  374Gi     2%       8 9223372036854775799    0%   /private/var/vm
map -hosts       0Bi    0Bi    0Bi   100%       0                   0  100%   /net
map auto_home    0Bi    0Bi    0Bi   100%       0                   0  100%   /home
/dev/disk1s3   932Gi  972Mi  374Gi     1%      42 9223372036854775765    0%   /Volumes/Recovery

What we’ve seen is useful to run commands manually, but we don’t want to have to run each command manually. What we want is to create a playbook with all the tasks listed so they can be run and rerun at any time. The benefit of using Ansible is that commands are idempotent, this means that we can re-run commands and if the effect we desire is already there nothing will change, this is an improvement over creating our own scripts that will probably have unforeseen consequences when we run them multiple times.

Let’s create a simple playbook. Ansible playbooks are written in YML an easy to read data serialization format. Let’s start by creating a directory to have all our files:

1
$ mkdir local_setup

Now let’s create our playbook, name it main.yml, it will contain the following lines:

1
2
3
4
5
6
7
8
9
10
---
# main.yml
- hosts: localhost
  connection: local

  tasks:
    - name: Check disk space
      command: df -h
      register: df_result
    - debug: var=df_result.stdout_lines 

In the playbook we define the task we want Ansible to run on the localhost. The only strange part is the last two lines, they are there only to capture the output of the df command so we can see it on screen. Usually, this is not necessary we just want to run a command and Ansible will notify us of the status of the command’s execution.

Let’s run the command:

1
$ ansible-playbook main.yml

You should see the output of the df -h command on your screen along with the additional information from Ansible.

Ansible Modules

Modules are packages that allow Ansible to perform tasks on the hosts. We could have a module that interacts with a specific service and abstracts all the commands and more important most modules strive to make the tasks idempotent. This means that no matter how many times a task is executed it will always produce the same effect, that is, the module has the logic to avoid making changes if the module verifies that the current state is the same as the desired final state.

Modules can be developed on any programming language it doesn’t have to be in python, the only expectation is that they return JSON objects. To see a list of available Ansible modules click here to visit the official list, and to learn more about modules see the official documentation.

I’ll show an example of how to use the module command, you can check all the parameter it accepts in the command module documentation

1
$ ansible localhost -c local -m command -a "chdir=/Users ls -l"

That command will change directories to /Users and list all the content of that directory. Let’s see the same example using a playbook:

1
2
3
4
5
6
7
8
9
10
11
---
- hosts: localhost
  connection: local

  tasks:
  - name: List users on the system.
    command: ls -l
    register: user_list
    args:
      chdir: /Users
  - debug: var=user_list.stdout_lines

Package manager modules

Depending on your Operating System, you’ll have access to a package manager that you can use to install libraries, binaries, and in some cases applications. I’ll give a brief example of how to use Homebrew a package manager for macOS that has an Ansible module that we can use to install packages on our system.

Let’s assume you have a simple setup, you want to make sure you have htop and tmux on your computer. The following playbook that uses the Homebrew module will help you accomplish this:

1
2
3
4
5
6
7
8
9
10
#my dev setup
---
- hosts: localhost
  connection: local

  tasks:
  - name: Make sure my dev tools are installed
    homebrew:
      name: htop,tmux
      state: present

That is a trivial example, but it opens new possibilities. Now you can create a playbook that contains all the tools you need to start working on a machine, and every time you change computers and want to make sure you have all the tools, you only need to run your playbook. You will probably find a package manager module for your specific Operating System, read the documentation it will be full of examples that can help you automate your setup.

If we are only dealing with trivial configuration or automation, a single playbook is enough, but when you want to combine tasks in playbooks and take advantage of playbooks other people have created, you’ll end up using roles. Let’s look at Ansible Roles.

Ansible Roles

Roles, in simple terms, are packaged playbooks that can be included on other playbooks, this allows you to avoid repeating code. You can reuse your roles and roles from other people using Ansible Galaxy. Ansible Galaxy is an online repository where people can publish their roles, it comes with a command line tool to interact with the online repository.

The filesystem structure of a role looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
├── rderik.my_role
│   ├── README.md
│   ├── defaults
│   │   └── main.yml
│   ├── files
│   ├── handlers
│   │   └── main.yml
│   ├── meta
│   │   └── main.yml
│   ├── tasks
│   │   └── main.yml
│   ├── templates
│   ├── tests
│   │   ├── inventory
│   │   └── test.yml
│   └── vars
│       └── main.yml

Not all of the files and directories are needed, if you are going to deploy your roles to Ansible-Galaxy, you’ll need tasks and meta as the minimum structure. But because we’re working only on local, we can begin with just the essential role structure, like the following:

1
2
3
rderik.my_role
        └── tasks
            └── main.yml

Let’s see an example. First, create a roles directory that will contain all our roles. When naming a role we generally follow the following format: galaxy-username.role-name. Following that convention, our role will be named: rderik.uptime. Let’s create the directory:

1
$ mkdir -p roles/rderik.uptime/tasks`

The flag -p for mkdir creates the directory structure if it doesn’t exist. In the same command, we are telling it to create tasks inside rderik.uptime inside roles, because none of those exists mkdir will create them all.

Now let’s edit our rderik.uptime main task:

1
2
3
4
5
# rderik.uptime/tasks/main.yml
- name: Check system uptime
  command: uptime
  register: uptime_output
- debug: var=uptime_output.stdout_lines

Now to include that role in our ansible playbook, we just add it to the main.yml file that we were working on at the beginning. This is how the content of main.yml should look:

1
2
3
4
5
6
7
8
9
10
11
12
---
# main.yml
- hosts: localhost
  connection: local
  roles:
    - rderik.uptime

  tasks:
    - name: Check disk space
      command: df -h
      register: df_results
    - debug: var=df_results.stdout_lines 

You can see, we added a new section roles that contains the role we wish to include. Now we can run the command:

1
$ ansible-playbook main.yml

You should see the output of both tasks.

With these examples, we know enough of Ansible to use it to automate our local setup or any server you manage.

Final thoughts

This was a 10,000 feet view of Ansible so you can get started. With this information and examples, you can now continue exploring in more depth any area of your interest. I would recommend setting up some VMs using Vagrant or maybe spawn some containers with Docker and play with them, first gathering facts then changing configurations.

Hope this is useful, and if you have any questions, send them my way.

Related topics/notes of interest


** There is no comment system yet, but you can send me a message on twitter @rderik or send me an email: derik[at]rderik[dot]com.