Ansible
Overview
If we have one or two systems we need to administer, using shell scripts is probably sufficient. However as we have more systems we need to take care of, or need to swap out systems quickly, this approach leaves something to be desired. Copying scripts to multiple machines and running them, and making sure all machines are the same becomes more difficult.
An alternative is to use a configuration management system. The idea is that these systems take a file which dictate the state of a system, such as what packages should be installed, what files there should be, what users should exist, and so on. Then the configuration management tool will apply the configuration to a machine or set of machines. Benefits of configuration management include:
- Can quickly setup multiple machines
- Can be re-run to make sure the machines are still in the desired state
- Makes configuration drift less likely
- The configuration files can be tracked with git, so changes can be tracked
- The configuration files act as documentation
Configuration Management
There are multiple configuration management systems available, with Ansible, Pupper, Chef, and Salt being the most popular. The important distinctions between them are:
- Agent-based vs. agentless. An "agent" is a program which runs on the machines being managed and applies the configuration changes. Ansible has no agent, with changes being applied over SSH. The other three systems all have agents that must be installed on the hosts.
- Whether changes are pushed or pulled. Ansible pushes changes from the machine doing the management to the one(s) being managed. Puppet and chef pull changes instead, and Salt supports both models.
- Configuration language. Ansible and Salt both use YAML. Chef uses Ruby and Puppet uses its own language.
Ansible
Ansible is the easiest to get started with, and is widely used. It is developed by Red Hat and is free software. It does not require an agent to run on the managed nodes, that is, the machines we are configuring. Instead it is installed on the control node, the machine doing the management, and pushes changes over regular SSH connections.
For Ansible to be able to manage a machine, therefore, we must be able to SSH into that machine. In practice this needs to be as a user with sudo access. Otherwise we could not install packages or make other changes needing root permissions. We also ideally would use SSH keys so Ansible does not need to ask our password for every connection.
On Debian, Ansible can be installed on the machine doing the changes
with the ansible package.
Inventories
With Ansible an inventory is the set of host machines which can be configured. These can be specified in the INI format or in the YAML format. For consistency, the examples here will use YAML. Hosts can be grouped together to allow for different sets of configurations. For example, you could make one group for web servers, and another for database servers. Or one set for test machines and another for production machines.
Below is an example inventory file:
webservers:
hosts:
company.net
mirror1.company.net
dbservers:
hosts:
10.1.1.23
10.1.1.24
10.1.1.25
This inventory file creates two groups called webservers and dbservers. The hosts can be specified with either IP addresses (internal or external ones), or host names. The host is used as the address to SSH in, so it must be reachable from the machine doing the configuring.
We can manage the machine we are currently on with a localhost inventory:
all:
hosts:
127.0.0.1:
ansible_connection: local
The use of a local connection makes it so we don't need to SSH into the machine we are currently on, but rather run commands directly.
We can test the inventory file by pinging the hosts in it with ansible:
$ ansible all -m ping -i inventory.yaml
127.0.0.1 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.13"
},
"changed": false,
"ping": "pong"
}
cpsc.umw.edu | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.10"
},
"changed": false,
"ping": "pong"
}
I used two machines for this example, my localhost and the CPSC server.
Playbooks
Ansible uses the following terminology to refer to configurations it can perform:
Module
A function that Ansible provides to do something. There are built-in modules to install packages, create users, start programs, write files and so on. There are also third-party modules available.
Task
A call to some module to do some specific thing. For instance, one task might be to call upon the
ansible.builtin.packageto install the Apache web server. Another task might be to call theansible.builtin.filemodule to set the permissions on a system file.Play
An list of tasks to be carried out, in order, upon a host. A play will accomplish some goal, such as setting up a service or making sure a system is up to date, which might each involve multiple tasks.
Playbook
A playbook is a collection of plays, which might consist of multiple plays. For example, we might install write a playbook to install a LAMP stack, which will contain a play to install and configure Apache, another one to install and configure MySQL, and another to install PHP.
Here is an example playbook file consisting of one play with two tasks:
---
- name: Ensure Apache is running
hosts: all
tasks:
- name: Ensure apache is up to date
ansible.builtin.package:
name: apache2
state: latest
- name: Ensure apache is up and running
ansible.builtin.service:
name: apache2
enabled: yes
state: started
We can then run this playbook on an inventory of machines with the following:
$ ansible-playbook test1.yaml -i inventory.yaml PLAY [Ensure Apache is running] ************************************************************************************************************************** TASK [Gathering Facts] *********************************************************************************************************************************** ok: [127.0.0.1] ok: [cpsc.umw.edu] TASK [Ensure apache is up to date] *********************************************************************************************************************** changed: [127.0.0.1] ok: [cpsc.umw.edu] TASK [Ensure apache is up and running] ******************************************************************************************************************* changed: [cpsc.umw.edu] ok: [127.0.0.1] PLAY RECAP *********************************************************************************************************************************************** 127.0.0.1 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 cpsc.umw.edu : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
One important aspect of Ansible is idempotence which means that an operation can be applied multiple times without negative effect. This is different from a script which may not be re-runnable without changing anything. Note that the playbook is written in a declarative way. We don't say "install apache", we say "apache should be the latest version". If it already is, no change will be made.
We can see from the output here that no change was made to to cpsc.umw.edu
because Apache was already updated and running on that machine.
We can also run a playbook in "check mode" which means that it doesn't actually perform any changes, but tells you if changes would be made using the command:
$ ansible-playbook --check test1.yaml -i inventory.yaml
Modules
Some of the most useful Ansible modules are:
- ansible.builtin.package: This module can be used to make sure that packages are installed, updated, or not installed. There are also modules for distribution-specific package managers like apt, dnf, etc. The general package module is usually better.
- ansible.builtin.service:
Can be used to manage services, as with
systemctl. It can be used to make sure services are enabled, started, stopped or restarted. - ansible.builtin.copy: Copies files from the control node to the managed node(s), while allowing permissions and owners to be set for the copied files.
- ansible.builtin.file: Allows setting owner, permissions, and other file metadata for specific files.
- ansible.builtin.lineinfile: This module allows for adding or replacing lines in text files. Useful for making changes to configuration files.
- ansible.builtin.user: Creates users and allows setting user account details such as shell, groups, uid, etc.
- ansible.builtin.command: Allows for running general commands on the managed node(s). Can be used to do pretty much anything, but one should prefer more specific modules where possible. For example, if we need to install a package, prefer using the package module to using the command module to run apt manually. This makes idempotence easier: ansible is better able to reason about whether the task is needed or not.