Ansible 101: #003 - Getting started

Hello everyone! We're finally getting started with Ansible. Lets start with a practical setup.


My example setup

To illustrate this part with practical examples, I have set up a small test scenario. This will remain basically the same for the rest of the guide, but will be expanded to include additional hosts.

If you feel like it, feel free to create a similar scenario and just try out the examples.

Let's start with this:

My laptop running Ubuntu Desktop 20.04 LTS and Ansible version 2.9.6 as the Ansible controller.

Three VMs running Ubuntu Server 20.04 LTS as targets:
    ansible-guide-1 (192.168.0.11)
    ansible-guide-2 (192.168.0.12)
    ansible-guide-3 (192.168.0.13)

The target user for all three hosts is 'ansible'. This user has full sudo privileges, i.e. is not a root user itself, but can execute commands with root privileges using the 'sudo' command, short for 'super user do'. Ansible also uses this mechanism. But more on that in a moment.

Setting up SSH public key authentication

In theory, Ansible can do password authentication, but this is not really useful and is also very time consuming for a certain number of target hosts. The easiest and most secure option is to use SSH keys. We create a key pair consisting of a private and a public key, and store the public part (public key) on the target systems.

We store the private key securely on our client.

Assuming that SSH key authentication has not yet been set up, but is done using a password, the following steps need to be taken:

Create a key pair on the local client

ssh-keygen -t rsa -b 4096

the output will look similar to this:

Generating a public/private rsa key pair.
Specify the file to store the key in (/home/mow/.ssh/id_rsa): /home/mow/.ssh/id_rsa
Enter the passphrase (empty for no passphrase):
Enter the same passphrase again:
Your identification is stored in /home/mow/.ssh/id_rsa
Your public key is stored in /home/mow/.ssh/id_rsa.pub
The key fingerprint is
SHA256:uIRqKu6ySijEPaHeNrkNJrs/PmzTH6T+0+WB9F+GKek mow@ubuntu1
The random image of the key is
+---[RSA 4096]----+
| |
| |
| . |
|. o .. . . |
| + o. o.S o . o |
|+ ..o.o. . * o o |
|o++B..... + + o |
|=o=**. ... E . |
|X*=++ooo. |
-—[SHA256]--—+

What have we done? We have created a new SSH key pair. This is of the RSA type (-b rsa) and 4096 bytes long (-b 4096).

Important: For keys used in a production environment, I strongly recommend using a passphrase, as a potential attacker cannot do anything with the private key alone. However, this is not absolutely necessary for this guide.

Now that we are the proud owners of a freshly printed SSH key pair, we want to store the public part of it, i.e. the public key, on the target systems. A useful tool for this is 'ssh-copy-id'.

We run the following command on our Ansible controller

ssh-copy-id -i ~/.ssh/id_rsa ansible@ansible-guide-1

We will then be prompted for the password of the target user. The public key is then stored on the target system. Of course, we repeat this process for the rest of the systems.

If everything has worked, we should be able to connect to the target systems without a password:

ssh ansible@ansible-guide-1
ssh ansible@ansible-guide-2
ssh ansible@ansible-guide-3

And now we're ready to start using Ansible!

Creating a project directory and inventory

Let's start with a project directory and an inventory. Although we could start with the default inventory (/etc/ansible/hosts) and any directory, I always recommend using a separate directory for each Ansible project.

So first we create the project directory:

Create a directory in your home

mkdir ~/ansible-guide

Then create a file in ~/ansible-guide/inventory.txt with the following content

[guide]
ansible-guide-1 ansible_ssh_user=ansible ansible_host=192.168.0.11
ansible-guide-2 ansible_ssh_user=ansible ansible_host=192.168.0.12
ansible-guide-3 ansible_ssh_user=ansible ansible_host=192.168.0.13

In this case we have created an inventory with a 'guide' group. This contains our 3 test hosts. With 'ansible_ssh_user=ansible' we specify the user to use for the SSH connection.

If the hostnames cannot be resolved to IPs via DNS, we use 'ansible_host=***' to specify the target IPs of the hosts.

This can all be done a bit more easily, but should be enough for now. After all, we still want to go into detail about inventories!

AdHoc commands

A term I unfortunately forgot to mention in the first article. So I'll make up for it here:

AdHoc commands can be used to perform specific tasks with Ansible at the command line level. They are very easy to use and are especially useful when you need to do something quickly on multiple hosts at once.

Although I always recommend using reusable playbooks for smaller tasks, the AdHoc commands are very useful for initial Ansible testing.

So let's see if we have our SSH connection configured correctly. Ansible has a ping module for this:

# Syntax
# ansible <hosts> -i <inventory> -m <modul>
cd ~/ansible-guide

ansible guide -i inventory.txt -m ping

If all went well, the output should look something like this

ansible-guide-1 | SUCCESS => {
ansible_facts": {
'discovered_interpreter_python": '/usr/bin/python'
},
'changed': false,
'ping': 'pong'
}
ansible-guide-2 | SUCCESS => {
'ansible_facts': {
'discovered_interpreter_python': '/usr/bin/python'
},
'changed': false,
'ping': 'pong'
}
ansible-guide-3 | SUCCESS => {
'ansible_facts': {
'discovered_interpreter_python': '/usr/bin/python'
},
'changed': false,
'ping': 'pong'
}

Perfect, we can now manage our three systems with Ansible!

With another ad hoc command, we could now display information about the operating system, for example:

# Syntax ansible <hosts> -i <inventory> -m <modul> -a <parameter>

ansible guide -i inventory.txt -m command -a "cat /etc/os-release"

So we run Ansible again with our 'guide' group, this time using the 'command' module and giving it the parameter 'cat /etc/os-release'.

The output should look something like this:

ansible-guide-1 | CHANGED | rc=0 >>
NAME="Ubuntu"
VERSION="20.04.1 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.1 LTS"
VERSION_ID="20.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=focal
UBUNTU_CODENAME=focal

ansible-guide-2 | CHANGED | rc=0 >>
NAME="Ubuntu"
VERSION="20.04.1 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.1 LTS"
VERSION_ID="20.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=focal
UBUNTU_CODENAME=focal

ansible-guide-3 | CHANGED | rc=0 >>
NAME="Ubuntu"
VERSION="20.04.1 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.1 LTS"
VERSION_ID="20.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=focal
UBUNTU_CODENAME=focal

Our first playbook!

Now it's time to create our first playbook. Let's say we want to install the 'htop' package on all three systems. I will only say a few words about the contents here, as we will go into more detail about playbooks in the next articles.

Our simple playbook looks like this

# /home/mow/ansible-guide/playbook-htop.yml
- hosts: guide
  tasks:                     
    - name: install htop 
      become: true 
      ansible.builtin.apt: 
        name: htop 
        state: present

To run the whole thing, we need another command:

ansible-playbook -i inventory.txt playbook-htop.yml

And this is what the output looks like:

PLAY [guide] **************************************************************************************************************************************************************************************************

TASK [Gathering Facts] ********************************************************************************************************************************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]
ok: [ansible-guide-3]

TASK [install htop] ***********************************************************************************************************************************************************************************************
changed: [ansible-guide-1]
changed: [ansible-guide-2]
changed: [ansible-guide-3]
]
PLAY RECAP ********************************************************************************************************************************************************************************************************
ansible-guide-1                  : ok=2    changed=1    unreachable=0 failed=0    skipped=0    rescued=0    ignored=0
ansible-guide-2                  : ok=2    changed=1    unreachable=0 failed=0    skipped=0    rescued=0    ignored=0 
ansible-guide-3                  : ok=2    changed=1    unreachable=0  failed=0    skipped=0    rescued=0    ignored=0

Task states

To conclude this section, I would like to briefly discuss some of the states a task can take. You may have noticed that some of our tasks return 'ok' and others return 'changed'.

A task will always return 'ok' if Ansible did not need to make any changes to create the target state. Of course, the opposite is true for the 'changed' state.

But wait a minute! What bullshit is this guy talking? In the example, we did a 'cat /etc/os-relase', so we are just printing the contents of a file. So why does the task say 'modified'?

This is because the 'command' module does nothing more than blatantly execute a shell command. Since Ansible has no way of 'knowing' what this does in turn (it could be a script or something else), it automatically assumes a change.

Hence, an important tip that you will hear from me many times:

Use the command (or shell) module ONLY IF YOU REALLY HAVE TO! Whenever possible, we should try to use modules where Ansible can track changes. This way we can always track the state of our environment, even with more complex playbooks.

There are other states besides ok and modified, but we will look at these in more detail in due course.

Thats it for now!

Mow