Automated administration with Ansible
Ansible automates the management of remote systems and controls their desired state.
In this post we will install and configure Ansible for managing remote hosts. Finally we will do the first steps in using it for automated administration.
Wikipedia:
“Ansible is a suite of software tools that enables infrastructure as code. Infrastructure as code (IaC) is the process of managing and provisioning computer data center resources through machine-readable definition files, rather than physical hardware configuration or interactive configuration tools.
Ansible is agentless, relying on temporary remote connections via SSH. Ansible does not deploy agents to nodes. Only OpenSSH and Python are required on the managed nodes.
The Ansible control node runs on most Unix-like systems that are able to run Python. The control node (master host) is intended to manage (orchestrate) target machines (nodes termed as “inventory”).”
Installation
Let’s install Ansible according to Getting started with Ansible and Installing Ansible.
Install Python
If Python is not installed by default, install it:
$ sudo apt install python3
python3 ist schon die neueste Version (3.12.6-0ubuntu1).
Summary:
Upgrading: 0, Installing: 0, Removing: 0, Not Upgrading: 0
Install pipx
First we install Ansible on our control node machine. To install Python apps we use pipx. We install pipx as basis:
$ sudo apt update
$ sudo apt install pipx
Installing:
pipx
Installing dependencies:
python3-argcomplete python3-platformdirs python3-userpath python3.12-venv
python3-pip-whl python3-setuptools-whl python3-venv
...
$ pipx ensurepath
Success! Added ~/.local/bin to the PATH environment variable.
Consider adding shell completions for pipx. Run 'pipx completions' for instructions.
You will need to open a new terminal or re-login for the PATH changes to take effect. Alternatively, you can source your
shell's config file with e.g. 'source ~/.bashrc'.
Otherwise pipx is ready to go! ✨ 🌟 ✨
$ source ~/.bashrc
Install Ansible
$ pipx install --include-deps ansible
installed package ansible 11.3.0, installed using Python 3.12.7
These apps are now globally available
- ansible
- ansible-community
- ansible-config
- ansible-console
- ansible-doc
- ansible-galaxy
- ansible-inventory
- ansible-playbook
- ansible-pull
- ansible-test
- ansible-vault
done! ✨ 🌟 ✨
Install argcomplete
Reference: https://kislyuk.github.io/argcomplete/:
“Argcomplete provides easy, extensible command line tab completion of arguments for your Python application.”
$ pipx inject --include-apps ansible argcomplete
⚠️ Note: activate-global-python-argcomplete was already on your PATH at /usr/bin/activate-global-python-argcomplete
⚠️ Note: python-argcomplete-check-easy-install-script was already on your PATH at
/usr/bin/python-argcomplete-check-easy-install-script
⚠️ Note: register-python-argcomplete was already on your PATH at /usr/bin/register-python-argcomplete
installed package argcomplete 3.5.3, installed using Python 3.12.7
These apps are now globally available
- activate-global-python-argcomplete
- ansible
- ansible-community
- ansible-config
- ansible-console
- ansible-doc
- ansible-galaxy
- ansible-inventory
- ansible-playbook
- ansible-pull
- ansible-test
- ansible-vault
- python-argcomplete-check-easy-install-script
- register-python-argcomplete
done! ✨ 🌟 ✨
injected package argcomplete into venv ansible
done! ✨ 🌟 ✨
Configure argcomplete to allow shell completion of the Ansible command line utilities (globally):
$ activate-global-python-argcomplete --user
Adding shellcode to ~/.zshenv...
Added.
Adding shellcode to ~/.bash_completion...
Added.
Please restart your shell or source the installed file to activate it.
$ source ~/.bash_completion
Test environment
$ python3 --version
Python 3.12.7
$ pipx --version
1.6.0
$ ansible --version
ansible [core 2.18.3]
config file = None
configured module search path = ['~/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = ~/.local/share/pipx/venvs/ansible/lib/python3.12/site-packages/ansible
ansible collection location = ~/.ansible/collections:/usr/share/ansible/collections
executable location = ~/.local/bin/ansible
python version = 3.12.7 (main, Feb 4 2025, 14:46:03) [GCC 14.2.0] (~/.local/share/pipx/venvs/ansible/bin/python)
jinja version = 3.1.5
libyaml = True
$ ansible-community --version
Ansible community version 11.3.0
Upgrading Ansible
To upgrade an existing Ansible installation to the latest released version:
$ pipx upgrade --include-injected ansible
ansible is already at latest version 11.3.0 (location: ~/.local/share/pipx/venvs/ansible)
Using Ansible
Creating a home for our Ansible DevOps stuff
Reference: https://docs.ansible.com/ansible/latest/getting_started/get_started_ansible.html#get-started-ansible
For all our ansible administration files we create an own project directory and change into it:
$ mkdir -p mycompany-devops/ansible
$ cd mycompany-devops/ansible/
Building an inventory
References:
- https://docs.ansible.com/ansible/latest/getting_started/get_started_inventory.html
- https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html
“Inventories organize managed nodes in centralized files that provide Ansible with system information and network locations. Using an inventory file, Ansible can manage a large number of hosts with a single command.”
Add hosts
Let’s create an inventory file for all hosts we want to manage using ansible. We prefer yml format over ini format:
$ nano inventory.yml
all:
hosts:
old_server:
ansible_host: 192.0.2.100
new_server:
ansible_host: 192.0.2.110
- First level: “all” is the group name (as we do not specify special groups, yet)
- Third level: “old_server” is an unique name describing the host
- Fourth level: “ansible_host”: IP-address or fully qualified domain name (FQDN) of managed host
“Even if you do not define any groups in your inventory file, Ansible creates two default groups: all and ungrouped. The all group contains every host. The ungrouped group contains all hosts that don’t have another group aside from all. Every host will always belong to at least 2 groups (all and ungrouped or all and some other group).”
Create ansible.cfg
As we changed default inventory file from inventory.ini to inventory.yml we have to pass over the inventory filename with option -i every time we use ansible, e.g. ansible -i inventory.yml -m ping all.
To change the default filename we create a local ansible.cfg file in our project directory (other locations possible).
To get an example with defaults set you can do:
$ ansible-config init > ansible.cfg
This creates a rather long default configuration. In fact we just want adding ./inventory.yml file path to inventory in ansible.cfg:
[defaults]
# (pathlist) Comma-separated list of Ansible inventory sources
inventory=/etc/ansible/hosts,./inventory.yml
With this configuration, we can skip -i option. (We still use it below to make things obvious)
Add username to use
If ansible should use specific username, we can configure this using ansible_user:
all:
vars:
ansible_user: remote_username
If you do not want to configure it, you can pass the username with option -u on ansible execution.
Verify inventory
$ ansible-inventory -i inventory.yml --list
{
"_meta": {
"hostvars": {
"new_server": {
"ansible_host": "192.0.2.110",
"ansible_user": "remote_username"
},
"old_server": {
"ansible_host": "192.0.2.100",
"ansible_user": "remote_username"
}
}
},
"all": {
"children": [
"ungrouped"
]
},
"ungrouped": {
"hosts": [
"old_server",
"new_server"
]
}
}
First contact: Ping your hosts
Make sure you are able to ssh into the servers:
$ ssh -l remote_username 192.0.2.110
If authentication is configured with public-private-key everything is setup and no SSH password has to be configured.
If you need to enter a password (e.g. at old_server), you additionally have to configure the password for the remote_user in the inventory.xml file:
all:
hosts:
old_server:
ansible_host: 192.0.2.100
ansible_password: 'your_password'
Storing passwords in clear text is not recommended. Ansible provides “Ansible Vault” for securely storing and managing secrets like passwords. See https://docs.ansible.com/ansible/latest/vault_guide/vault.html#ansible-vault
For now we use plaintext for testing purpose.
Ping the all group in your inventory if remote user is not configured in inventory using -u option:
$ ansible all -u remote_username -m ping -i inventory.yml
If it is configured in inventory:
$ ansible all -m ping -i inventory.yml
old_server | FAILED! => {
"msg": "to use the 'ssh' connection type with passwords or pkcs11_provider, you must install the sshpass program"
}
[WARNING]: Platform linux on host new_server is using the discovered Python interpreter at /usr/bin/python3.11, but future
installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-
core/2.18/reference_appendices/interpreter_discovery.html for more information.
new_server | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.11"
},
"changed": false,
"ping": "pong"
}
So we’ve got two problems to solve:
- login on
old_serverfailed, we “must install the sshpass program” - login on
new_serverwith public-private-key succeeded, but we get warning about “host new_server is using the discovered Python interpreter at /usr/bin/python3.11 - See https://docs.ansible.com/ansible-core/2.18/reference_appendices/interpreter_discovery.html”
Install sshpass
Reference: https://manpages.org/sshpass
$ sudo apt install sshpass
$ sshpass
Usage: sshpass [-f|-d|-p|-e[env_var]] [-hV] command parameters
-f filename Take password to use from file.
-d number Use number as file descriptor for getting password.
-p password Provide password as argument (security unwise).
-e[env_var] Password is passed as env-var "env_var" if given, "SSHPASS" otherwise.
With no parameters - password will be taken from stdin.
-P prompt Which string should sshpass search for to detect a password prompt.
-v Be verbose about what you're doing.
-h Show help (this screen).
-V Print version information.
At most one of -f, -d, -p or -e should be used.
After this we got the same warning about Python interpreter location and another problem, targeting only old_server:
$ ansible -m ping -i inventory.yml old_server
[WARNING]: Platform linux on host old_server is using the discovered Python interpreter at /usr/bin/python3, but future
installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-
core/2.18/reference_appendices/interpreter_discovery.html for more information.
old_server | FAILED! => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"msg": "ansible-core requires a minimum of Python version 3.8. Current version: 3.7.3 (default, Mar 23 2024, 16:12:05) [GCC 8.3.0]"
}
To solve “ansible-core requires a minimum of Python version 3.8” we upgrade python on managed host.
Upgrade Python on managed host
Reference: https://docs.python.org/3.13/using/unix.html#getting-and-installing-the-latest-version-of-python and https://www.python.org/downloads/source/
Remove previous python installation:
$ sudo apt-get remove python3
Install idle, zlib and libssl:
$ sudo apt install -y idle
$ sudo apt install -y zlib1g zlib1g-dev zlibc
$ sudo apt install -y libssl-dev
$ sudo apt install -y libssl1.1 || sudo apt install -y libssl1.0
Build and install Python on server from source:
$ wget https://www.python.org/ftp/python/3.13.2/Python-3.13.2.tgz
$ tar xvfz Python-3.13.2.tgz
$ cd Python-3.13.2/
$ ./configure
$ make
$ sudo make install
$ python3 --version
Python 3.13.2
“Ping” old_server again:
$ ansible -m ping -i inventory.yml old_server
[WARNING]: Platform linux on host old_server is using the discovered Python
interpreter at /usr/local/bin/python3.13, but future installation of another
Python interpreter could change the meaning of that path. See
https://docs.ansible.com/ansible-
core/2.18/reference_appendices/interpreter_discovery.html for more information.
old_server | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/local/bin/python3.13"
},
"changed": false,
"ping": "pong"
}
Ok, just same warning as on new_server, let’s fix it.
Fix Python interpreter discovery warning
Reference: https://docs.ansible.com/ansible-core/2.18/reference_appendices/interpreter_discovery.html
First option would be to specify location of python interpreter with ansible_python_interpreter for each host. Second option would be to set auto_silent to suppress warning.
We choose second option and set it in inventory.yml:
all:
vars:
ansible_python_interpreter: auto_silent
Final ping
Now all problems are solved, let’s ping all hosts:
$ ansible all -m ping -i inventory.yml
old_server | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/local/bin/python3.13"
},
"changed": false,
"ping": "pong"
}
new_server | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.11"
},
"changed": false,
"ping": "pong"
}
Creating first ansible playbook
References:
- https://docs.ansible.com/ansible/latest/getting_started/get_started_playbook.html
- https://docs.ansible.com/ansible/latest/playbook_guide/index.html
“Playbooks are automation blueprints, in YAML format, that Ansible uses to deploy and configure managed nodes.”
- Playbook: A list of plays that define the order in which Ansible performs operations, from top to bottom, to achieve an overall goal.
- Play: An ordered list of tasks that maps to managed nodes in an inventory.
- Task: A reference to a single module that defines the operations that Ansible performs.
- Module: A unit of code or binary that Ansible runs on managed nodes. Ansible modules are grouped in collections with a Fully Qualified Collection Name (FQCN) for each module.
Create a directory for your playbooks:
$ mkdir - p mycompany-devops/ansible/playbooks
$ cd mycompany-devops/ansible
Let’s create a playbook that pings your hosts and prints a “Hello world” message.
$ nano playbooks/helloworld-playbook.yaml
- name: My first play ("Hello world")
hosts: all
tasks:
- name: Ping my hosts
ansible.builtin.ping:
- name: Print message
ansible.builtin.debug:
msg: Hello world
Run your playbook:
$ ansible-playbook -i inventory.yml playbooks/helloworld-playbook.yaml
PLAY [My first play ("Hello world")] ********************************************************************************************************
TASK [Gathering Facts] **********************************************************************************************************************
ok: [new_server]
ok: [old_server]
TASK [Ping my hosts] ************************************************************************************************************************
ok: [new_server]
ok: [old_server]
TASK [Print message] ************************************************************************************************************************
ok: [old_server] => {
"msg": "Hello world"
}
ok: [new_server] => {
"msg": "Hello world"
}
PLAY RECAP **********************************************************************************************************************************
new_server : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
old_server : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
In this output you can see:
- The names that you give the play and each task. You should always use descriptive names that make it easy to verify and troubleshoot playbooks.
- The “Gathering Facts” task runs implicitly. By default, Ansible gathers information about your inventory that it can use in the playbook.
- The status of each task. Each task has a status of ok which means it ran successfully.
- The play recap that summarizes results of all tasks in the playbook per host. In this example, there are three tasks so ok=3 indicates that each task ran successfully.
Congratulations, you have started using Ansible!
Continue reading https://docs.ansible.com/ansible/latest/getting_started/basic_concepts.html.