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 r
ead permission for o
thers 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:
- 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.
- Read the documentation of the Ansible module that creates files which can be
copy
,template
or others, and you will see that options likeowner
,group
andmode
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!