Date: March 3, 2025 /  Author: Ralf Eichinger

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:

“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:

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:

“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.

 Tags:  topics devops

Previous
⏪ nginx Webserver - Installation, Configuration, Operations

Next
Automated installation of Java with Ansible ⏩