Back in the early days of computers, they were like loners, just sitting there wanting to connect. Then, out of nowhere, a network popped up, linking these once-alone machines into what we now know as the internet.

In the time when people had to manually handle servers, brave sysadmins went on missions across the digital landscape. Armed with keyboards and a strong will, they traveled through the virtual world, logging into each server to set up apps and run updates like careful digital gardeners.

And guess what? A big change was on the way, dressed in automation's gear and going by the name Ansible. When it showed up, all the hassle of manually dealing with servers was tossed out the window, and a new era started. Ansible, the tech wizard, made everything way more efficient, turning hard work into automated magic.

So, my friend, get ready to learn some cool stuff. In the next stories, we'll dig into Ansible's wonders and find out why knowing its tricks is a big deal in today's digital world. Buckle up for a fun ride through the evolution of server magic!

Ansible vs. old school methods

Think about when you SSH'd into a host and run an uptime command:

[user@ansible ~]$ ssh myhost
[user@myhost ~]$ uptime 
 10:09:27 up 2 days, 17:51,  1 user,  load average: 0.65, 0.72, 0.67

Just, why? This is a waste of time, and we as DevOps know that time is precious? Let's rewrite this block above with something that your colleague will surely envy:

[user@ansible ~]$ ssh myhost uptime
 10:09:27 up 2 days, 17:51,  1 user,  load average: 0.65, 0.72, 0.67

Something is still missing. You want to run the uptime command on your 3 nodes. You are thinking of using a for loop. This one, maybe?

[user@ansible ~]$ for i in myhost1 myhost2 myhost3; do ssh $i uptime; done
 10:18:31 up 35 days, 18:05,  0 user,  load average: 1.14, 1.20, 1.27
 10:18:33 up 35 days, 18:00,  0 user,  load average: 1.12, 1.33, 1.40
 10:18:36 up 35 days, 18:11,  0 user,  load average: 1.05, 1.12, 1.16

This is old school. It would impress everybody, even your IT manager, but we're no longer in the fantastic 90s.
So, how do we rewrite then the SSH command above with uptime in an Ansible form?

The quick answer is:

[user@ansible ~]$ ansible myhost1 -m command -a "uptime"
localhost | CHANGED | rc=0 >>
 10:45:11 up 10:46,  1 user,  load average: 0.51, 0.75, 0.77

And the quick answer with multiple hosts is:

[user@ansible ~]$ ansible my_host_group -m command -a "uptime"

Wait! What's myhost1?" And what's my_host_group?

If you just ran this command there's a 100% chance that the command did not succeed, and printed an information like:

[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
[WARNING]: Could not match supplied host pattern, ignoring: my_host_group

This is because localhost is implicit to Ansible, and it refers to this computer. But Ansible needs an inventory file to run. In such inventory we add all of our hosts, grouped by hostgroups. And yes, hosts can belong to multiple hostgroups.

We will provide a few examples later, but for the sake of completeness, the Ansible inventory, in INI format because we believe it's easier to read by anybody starting with Ansible, that would run the above code can be:

[my_host_group]
myhost1

Spoiler alert: Even with this inventory you will still not be able to run the Ansible ad-hoc command above. Unless myhost1 is a real host in your DNS system or in your /etc/hosts file or uses the special variable ansible_host, then you really can not connect to myhost1.

Understanding Ansible's Core Concepts

Declarative Language: Ansible understands what you want without micromanaging. You say, Ansible does. Simple as that.

Idempotence: Ansible ensures consistency. No matter how many times you run it, the outcome remains the same. This is true when the playbooks are coded correctly of course, but like a trustworthy recipe, it never fails.

Agentless Architecture: No need for installations. Ansible communicates through SSH, keeping your systems clean and hassle-free. All that the remote systems need is Python. Network equipments like switches or firewalls can be managed through their APIs and in general, everything that has an API can also be managed.

Playbooks: These are your automation guides. Written in YAML, they contain tasks and roles, helping you orchestrate your automation with ease.

Modules: Think of modules as specialized tools. Each one does a specific job on your managed nodes, making complex tasks manageable. We have written a beautiful post on how to build your own Ansible module here.

Inventory: The inventory is the answer to the the question: In which of my servers will this automation apply changes? The inventory contains all your infrastructure. It can be a single INI or YAML file or subdivided into folders. No matter the shape, the inventory contains all of your hosts and every variable you will ever use at host or at group level.

Variables: Variables store data, making your automation flexible. There are multiple "flavors" of variables. Some come from the inventory and are injected during the playbook execution for changing values where required, and these are common in every programming language; others are taken from the systems and are called facts; Facts can also be set, same as when declaring a variable while the application is running. Want to know more about facts? We wrote this article that dives deeper into the Ansible world of Facts and explores all the potentialities that Facts can unlock in playbooks!

Handlers: Handlers wait backstage until triggered, adding efficiency to your tasks for not repeating the same block of code multiple times along the execution. Handlers are called by tasks that require them, and if multiple tasks call the same handler, this will run once, as handlers do, at the end of the playbook.

And that's the essence of Ansible's core concepts. Now, let's dive into practical applications!

Setting Up Your Environment

Install Ansible

There are multiple ways for getting Ansible installed and ready to rock. All of them are covered in the Installation intro and Installation for distros.

For a simple installation the repository way is enough. On Fedora open up your terminal and run:

sudo dnf install ansible -y

Configure ansible.cfg for default settings

Starting with Ansible-core, the file in /etc/ansible/ansible.cfg does not contain anymore the default settings, instead, it tells us to create our own ansible.cfg file:

[user@ansible ~]$ cat /etc/ansible/ansible.cfg 
# Since Ansible 2.12 (core):
# To generate an example config file (a "disabled" one with all default settings, commented out):
#               $ ansible-config init --disabled > ansible.cfg
#
# Also you can now have a more complete file by including existing plugins:
# ansible-config init --disabled -t all > ansible.cfg

# For previous versions of Ansible you can check for examples in the 'stable' branches of each version
# Note that this file was always incomplete  and lagging changes to configuration settings

# for example, for 2.9: https://github.com/ansible/ansible/blob/stable-2.9/examples/ansible.cfg

Let's create it, and let's use some of the most used defaults value. In your home folder (or anywhere you like), create an ansible folder:

[user@ansible ~]$ mkdir ansible && cd ansible
[user@ansible ansible]$ ansible-config init --disabled -t all > ansible.cfg

The option --disabled will create an ansible.cfg file entirely commented. From the [defaults] section we want to enable:

[defaults]
inventory=/home/user/ansible/inventory
collections_path=/home/user/ansible/collections
roles_path=/home/user/ansible/roles

Your playbooks will take the settings from the ansible.cfg file most close to the playbooks folder. In this case, it is /home/user/ansible.

If you want to test that your playbook will use This ansible configuration, the command:

[user@ansible ansible]$ ansible --version

Will print:

ansible [core 2.16.0]
  config file = /home/user/ansible/ansible.cfg
  configured module search path = ['/home/user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.11/site-packages/ansible
  ansible collection location = /home/user/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.12.0 (main, Oct  2 2023, 00:00:00) [GCC 13.2.1 20230918 (Red Hat 13.2.1-3)] (/usr/bin/python3)
  jinja version = 3.1.2
  libyaml = True

ansible --version is useful because it also shows which Python version is in use.

If you want to use a different configuration in ansible.cfg it is enough to create it within a different directory:

[user@ansible ansible]$ mkdir /home/user/ansible2 && cd /home/user/ansible2
[user@ansible ansible2]$ ansible-config init --disabled -t all > ansible.cfg

This is because:

Changes can be made and used in a configuration file which will be searched for in the following order:

 * ANSIBLE_CONFIG (environment variable if set)
 * ansible.cfg (in the current directory)
 * ~/.ansible.cfg (in the home directory)
 * /etc/ansible/ansible.cfg

You can learn more in the Official Documentation.

Build up your inventory

Sure, you could dig into the official inventory documentation but we believe in making things as clear as a sunny day.

So, here's the lowdown on creating your inventory. It's like building your dream team, but for servers. An ultra-simple inventory file, the backbone of your Ansible operations, goes something like this. Pop it in /home/user/ansible/inventory and voila, magic happens:

[my_host_group]
myhost1 ansible_host=192.168.150.11
myhost2 ansible_host=192.168.150.12
myhost3 ansible_host=192.168.150.13

Note: If you edit your /etc/hosts file or use a DNS server, and your myhost1 points already to 192.168.150.11, you do not have to use the Special variable ansible_host.

Bonus points: If you use Vim you get awarded as an amazing DevOps/genius.

When you want to know if an host exists in the inventory:

[user@ansible ansible]$ ansible myhost1 --list-hosts
  hosts (1):
    myhost1

And for listing all hosts in the group:

[user@ansible ansible]$ ansible my_host_group --list-hosts
  hosts (3):
    myhost1
    myhost2
    myhost3

Lastly, to know which host does not belong to any group:

[user@ansible ansible]$ ansible ungrouped --list-hosts
[WARNING]: No hosts matched, nothing to do
  hosts (0):

Well, it's obviously empty in our case because the inventory above does not have any host without a group.

The inventory can also be dynamically generated through the use of scripts. We're not covering how to use them in this post but if you're interested at it make sure to check this documentation.

Your first connection to a host

In general, if your user can connect to the host:

[user@ansible ansible]$ ssh myhost1
Last login: Thu Nov  9 14:22:52 2023 from 192.168.150.2
[user@myhost1 ~]$

Then Ansible can too, because it defaults to the same user that ran the command.

There are cases, many cases actually, where you would want to use an user just for Ansible.

  • Create the ansible user (it can really be any name) on the remote host and protect it with a password. Later on you should set it to only authenticate using ssh-keys:
[user@myhost1 ~]$ sudo -i
[sudo] password for user:
[root@myhost1 ~]$ useradd ansible
[root@myhost1 ~]$ passwd ansible
  • Generate a private key on the Ansible controller (yes, your computer; And yes, using your 'user'). It is not required to set a password but needless to say, never move the private key outside the current computer.
[user@ansible ~]$ ssh-keygen -t ed25519
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/user/.ssh/id_ed25519): 
Created directory '/home/user/.ssh'.

Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/user/.ssh/id_ed25519
Your public key has been saved in /home/user/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:eTkO9FBboe8o6CIZ9MYWISwNzkvtt3BvECbhoTl36X0 user@ansible
The key's randomart image is:
+--[ED25519 256]--+
|.+ o      . o.   |
|+ O + .  . +     |
| O * *  o o      |
|. * * o. + o     |
| o = * .SE= .    |
|  . O +..+ +     |
|   = ..o. o .    |
|  o ...  .       |
|   . ..          |
+----[SHA256]-----+
$
  • Copy the public key to the user's .ssh folder in the remote host
[user@ansible ~]$ ssh-copy-id ansible@myhost1
The authenticity of host 'myhost1 (192.168.150.11)' can't be established.
ECDSA key fingerprint is 70:9c:03:cd:de:ba:2f:11:98:fa:a0:b3:7c:40:86:4b.
Are you sure you want to continue connecting (yes/no)? yes
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
ansible@myhost1's password:

Number of key(s) added: 1

Now try logging into the machine, with:    "ssh 'ansible@myhost1'"
and check to make sure that only the key(s) you wanted were added.
  • Test the connection
[user@ansible ~]$ ssh ansible@myhost1
Last login: Wed Nov  8 10:10:17 2023 from 192.168.150.2
[ansible@myhost1 ~]$

Do you know that you can create a playbook to automate this process? This is Ansible after all!

Deploy public key with Ansible

Create the file /home/user/ansible/deploy-ansible-key.yml with the following content:

- name: Deploy the public key to Ansible managed hosts
  hosts: all
  vars:
    ansible_public_key: /home/user/.ssh/id_ed25519.pub
  tasks:
  - name: Ensure public key is in ansible's ~/.ssh/authorized_hosts
    ansible.posix.authorized_key:
      user: ansible
      state: present
      key: "{{ ansible_public_key }}"

The only catch here is that the public key is not yet deployed into the hosts. We must insert the Ansible's password to connect.

This is easily done when launching the playbook:

[user@ansible ansible]$ ansible-playbook deploy-ansible-key.yml --ask-pass

In case the playbooks fails, one reason is you might miss the ansible.posix collection. You can install it running the following command:

[user@ansible ansible]$ ansible-galaxy collection install ansible.posix

Remember when we wrote collections_path=/home/user/ansible/collections in the ansible.cfg file?
With Ansible Content Collections, it's like giving your Ansible code a wardrobe upgrade while keeping modules and plug-ins in a separate VIP section. Imagine it as a curated collection of modules, roles, and plug-ins ready to rock your playbooks. Vendors and developers can drop their updates whenever, totally independent of Ansible releases.

Welcome to Ansible and to your first playbook!

Add connection settings to the configuration

Now that we have the ansible user we can update our defaults configuration in ansible.cfg and use this user for connections to the hosts:

[defaults]
remote_user = ansible

Wait, how exactly does this work? Check out this following diagram:

    Ansible Controller                      myhost1
   +-------------------+                   +---------------------+
   |                   |    -- SSH --      |                     |
   |  user             |-------------------|   ansible           |
   |  (Private Key)    |                   |   (Authorized Key)  |
   |                   |                   |                     |
   +-------------------+                   +---------------------+

By creating an SSH key pair on the Ansible Controller under the user 'user', and sending the public-key file to the remote host in the ansible's user home folder (in the .ssh/authorized_key file to be precise), we established a channel where the user 'user' can access through SSH into the host myhost1 as the user 'ansible', without inserting the password of the ansible user.

Privileges escalation

Many times we require sudo rights to perform certain tasks. We must make sure that the user that connects to the hosts is able to run those tasks with elevated permissions.

One simple way is to add the user ansible to the sudoers file.

  • Add user to sudoers

Login as root (or any other privileged user) on the remote host:

[user@myhost1 ~]$ sudo -i
[sudo] password for user:
[root@myhost1 ~]#
  • Create /etc/sudoers.d/ansible with this content:
## password-less sudo for Ansible user
ansible ALL=(ALL) NOPASSWD:ALL

And think it through all the possible implication that a user with root rights without password can have on a system.

  • Test if ansible user can now sudo
[ansible@myhost1 ~]$ sudo uptime
 10:35:27 up 35 days, 15:29,  1 user,  load average: 1.14, 1.20, 1.27

Yes, I find the command uptime perfect.

  • Update ansible.cfg
[defaults]
inventory=/home/user/ansible/inventory
collections_path=/home/user/ansible/collections
roles_path=/home/user/ansible/roles

[privilege_escalation]
#become = true
become_method = sudo
become_user = root
become_ask_pass = false

Notice that we commented the become=true option. When this is enabled it tells Ansible to run every task with elevated privileges (sudo), and this is not what we want. Always remember to have full control on your playbooks and to only execute elevated tasks when needed.

Not only root!

The command sudo grants SuperUser privileges to the user that calls it. Ansible can become any user that you specify, and it doesn't have to be root. Webpages for example are generally managed (owned) by apache or nginx.

We can use root to edit webservers files using playbooks and we can choose to keep the original user permissions:

[user@myhost1 ~]$ ll /var/www/html/
total 4
-rw-r-----. 1 apache apache 7 Nov 10 15:46 file.html

If we had to create new files and tell Ansible task to use root, the new files will be created with root permissions, and since the apache user is not able to read them because the read permission for others is missing, the webserver won't be able to serve the new file.

[user@myhost1 ~]$ ll /var/www/html/
total 4
-rw-r-----. 1 apache apache 7 Nov 10 15:46 file.html
-rw-r-----. 1 root root 7 Nov 10 15:46 file2.html

Here we have two choices:

  1. Edit the task to use a different become user
- name: Create a file for Apache
  hosts: myhost1
  tasks:
  - name: Create file2.html
    copy:
      content: '<!-- This file was created by Ansible -->'
      dest: /var/www/html/file2.html
    become_user: apache

But this approach is hardly the most fitting one.

  1. Read the documentation of the Ansible module that creates files which can be copy, template or others, and you will see that options like owner, group and mode are always present when a module handles files.

The playbook for creating a file and apply properly elevated permission is:

- name: Create a file for Apache
  hosts: myhost1
  tasks:
  - name: Create file2.html
    copy:
      content: '<!-- This file was created by Ansible -->'
      dest: /var/www/html/file2.html
      owner: apache
      group: apache
      mode: '0640'
    become: true

Let's have a look at the permissions of file2.html

[user@myhost1 ~]$ ll /var/www/html/
total 4
-rw-r-----. 1 apache apache 7 Nov 10 15:46 file.html
-rw-r-----. 1 apache apache 7 Nov 10 15:58 file2.html

No, it's not Magic. It's Ansible!

Conclusions

Alright, let's break it down. The ansible.cfg is like your go-to manual for Ansible. It's got comments that explain what you can do with Ansible. And if you want more info, check out the Red Hat documentation.

Now, what's cool about Ansible? Picture this: it can handle not just a few but a ton of hosts—like, thousands of them. It's like a super-smart computer friend that can organize stuff way better than any human could.

In simple terms, Ansible is a champ in the world of servers. It takes tricky tasks and makes them easy. Whether you're a pro at this or just getting started, Ansible is your ticket to making things happen without pulling your hair out. Get ready for some automation fun!