Ansible
For the examples in this guide, we’ll be using Rocky Linux 9.6 for our Ansible controller node.
Prerequisites
We’ll need the latest version of python3
and python3-pip
in order to install Ansible with pip
dnf update -y
dnf install python3 python3-pip -y
pip3 install --upgrade pip
Check pip version:
[root@mawenzi-01 ansible]# python3 -m pip -V
pip 25.2 from /usr/local/lib/python3.9/site-packages/pip (python 3.9)
Installation
Install Ansible using pip
:
python3 -m pip install --user ansible
[root@mawenzi-01 ansible]# python3 -m pip install --user ansible
Collecting ansible
Downloading ansible-8.7.0-py3-none-any.whl.metadata (7.9 kB)
Collecting ansible-core~=2.15.7 (from ansible)
Downloading ansible_core-2.15.13-py3-none-any.whl.metadata (7.0 kB)
Collecting jinja2>=3.0.0 (from ansible-core~=2.15.7->ansible)
Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
Requirement already satisfied: PyYAML>=5.1 in /usr/lib64/python3.9/site-packages (from ansible-core~=2.15.7->ansible) (5.4.1)
Collecting cryptography (from ansible-core~=2.15.7->ansible)
Downloading cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl.metadata (5.7 kB)
Collecting packaging (from ansible-core~=2.15.7->ansible)
Downloading packaging-25.0-py3-none-any.whl.metadata (3.3 kB)
Collecting resolvelib<1.1.0,>=0.5.3 (from ansible-core~=2.15.7->ansible)
Downloading resolvelib-1.0.1-py2.py3-none-any.whl.metadata (4.0 kB)
Collecting importlib-resources<5.1,>=5.0 (from ansible-core~=2.15.7->ansible)
Downloading importlib_resources-5.0.7-py3-none-any.whl.metadata (2.8 kB)
Collecting MarkupSafe>=2.0 (from jinja2>=3.0.0->ansible-core~=2.15.7->ansible)
Downloading MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.0 kB)
Collecting cffi>=1.14 (from cryptography->ansible-core~=2.15.7->ansible)
Downloading cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (1.5 kB)
Collecting pycparser (from cffi>=1.14->cryptography->ansible-core~=2.15.7->ansible)
Downloading pycparser-2.22-py3-none-any.whl.metadata (943 bytes)
Downloading ansible-8.7.0-py3-none-any.whl (48.4 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 48.4/48.4 MB 1.8 MB/s 0:00:28
Downloading ansible_core-2.15.13-py3-none-any.whl (2.3 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.3/2.3 MB 4.6 MB/s 0:00:00
Downloading importlib_resources-5.0.7-py3-none-any.whl (24 kB)
Downloading resolvelib-1.0.1-py2.py3-none-any.whl (17 kB)
Downloading jinja2-3.1.6-py3-none-any.whl (134 kB)
Downloading MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (20 kB)
Downloading cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl (4.4 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.4/4.4 MB 4.4 MB/s 0:00:00
Downloading cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (445 kB)
Downloading packaging-25.0-py3-none-any.whl (66 kB)
Downloading pycparser-2.22-py3-none-any.whl (117 kB)
Installing collected packages: resolvelib, pycparser, packaging, MarkupSafe, importlib-resources, jinja2, cffi, cryptography, ansible-core, ansible
Successfully installed MarkupSafe-3.0.2 ansible-8.7.0 ansible-core-2.15.13 cffi-1.17.1 cryptography-45.0.7 importlib-resources-5.0.7 jinja2-3.1.6 packaging-25.0 pycparser-2.22 resolvelib-1.0.1
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
Confirm installation with ansible --version
:
[root@mawenzi-01 ansible]# ansible --version
ansible [core 2.15.13]
config file = None
configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /root/.local/lib/python3.9/site-packages/ansible
ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections
executable location = /root/.local/bin/ansible
python version = 3.9.21 (main, Jun 27 2025, 00:00:00) [GCC 11.5.0 20240719 (Red Hat 11.5.0-5)] (/usr/bin/python3)
jinja version = 3.1.6
libyaml = True
Configuring Ansible
Generate a default ansible configuration file (ansible.cfg
) to the local directory:
ansible-config init --disabled > ansible.cfg
Or, you can use the following bare-bones ansible.conf
:
[defaults]
inventory=hosts
private_key_file=/root/.ssh/id_ed25519_ansible
Also, create a hosts
inventory file with the names of your servers, and whatever group you want them in.
For this example, we’ll be using mawenzi-02
through mawenzi-07
and will be putting them in the ungrouped
group:
mawenzi-02
mawenzi-03
mawenzi-04
Edit these config files to your liking, then copy them to the default location /etc/ansible
:
mkdir -p /etc/ansible && cp ./ansible.cfg ./hosts /etc/ansible/
Set up SSH access
Use the following script to set up passwordless-SSH access to your managed nodes:
#!/bin/bash
ansible_keyfile="$HOME/.ssh/id_ed25519_ansible"
if [[ ! -f "$ansible_keyfile" ]]; then
ssh-keygen -t ed25519 -C "Ansible" -f "$ansible_keyfile" -N ""
else
echo "$ansible_keyfile already exists; skipping"
fi
while IFS= read -r host ; do
if [[ -n $host ]]; then
ssh-copy-id -i $ansible_keyfile.pub $host
fi
done < hosts
Test Ansible Connection
Run the Ansible ping
module to discover facts about nodes in your inventory.
You can specify the inventory file and keyfile to use manually, like:
ansible all --key-file ~/.ssh/id_ed25519_ansible -i hosts -m ping
Or, you can set the field private_key_file=/root/.ssh/id_ed25519_ansible
in your ansible.cfg
,
and put your hosts
file at /etc/ansible/hosts
so all you have to run is ansible all -m ping
.
[root@mawenzi-01 ansible]# ansible all -m ping
mawenzi-02 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
mawenzi-04 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
mawenzi-03 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
List Ansible hosts:
[root@mawenzi-01 ansible]# ansible all --list-hosts
hosts (3):
mawenzi-02
mawenzi-03
mawenzi-04
Running Ad-hoc Ansible Commands
You can run shell commands with ansible via ansible <hosts> -m shell -a <command>
:
[root@mawenzi-01 ansible]# ansible all -m shell -a uptime
mawenzi-02 | CHANGED | rc=0 >>
15:55:42 up 8 days, 6:00, 1 user, load average: 0.04, 0.03, 0.00
mawenzi-04 | CHANGED | rc=0 >>
15:55:42 up 45 min, 1 user, load average: 0.00, 0.00, 0.00
mawenzi-03 | CHANGED | rc=0 >>
15:55:42 up 7 days, 4:03, 1 user, load average: 1.44, 1.53, 1.45
[root@mawenzi-01 ansible]# ansible mawenzi-02 -m shell -a 'dnf install vim -y'
mawenzi-02 | CHANGED | rc=0 >>
Last metadata expiration check: 2:34:52 ago on Wed 03 Sep 2025 01:32:39 PM MDT.
Package vim-enhanced-2:8.2.2637-22.el9_6.x86_64 is already installed.
Dependencies resolved.
Nothing to do.
Complete!
Playbooks
We can create playbooks, which are essentially just YAML definitions of tasks we want to perform.
We’ll start with an example playbook that copies a pre-defined, static dnf.conf
file to /etc/dnf/dnf.conf
on
all target nodes.
Create the following directory structure:
.
├── ansible.cfg
├── configfiles
│ └── dnf.conf
├── hosts
├── install_ansible.sh
├── playbooks
│ └── install_dnf_conf.yml
└── setup_ssh.sh
The dnf.conf
file will have the following contents:
[main]
gpgcheck=0
installonly_limit=3
clean_requirements_on_remove=True
best=True
skip_if_unavailable=False
proxy=http://proxy.houston.hpecorp.net:8080
…and the playbook will define a task
using a copy
action:
---
- name: Install a dnf.conf file for HPE proxy
hosts: all
become: yes
tasks:
- name: Copy static dnf.conf file from controller
copy:
src: ../configfiles/dnf.conf # Path on control node
dest: /etc/dnf/dnf.conf # Destination path on target node
owner: root # Desired owner on target node
group: root # Desired group on target node
mode: '0644' # Desired permissions on target node
We’ll run the playbook with ansible-playbook <playbook_path>
:
[root@mawenzi-01 ansible]# ansible-playbook playbooks/install_dnf_conf.yml
PLAY [Install a dnf.conf file for HPE proxy] ******************************************************************************************************************************
TASK [Gathering Facts] ****************************************************************************************************************************************************
ok: [mawenzi-02]
ok: [mawenzi-04]
ok: [mawenzi-03]
TASK [Copy static dnf.conf file from controller] **************************************************************************************************************************
changed: [mawenzi-02]
changed: [mawenzi-04]
changed: [mawenzi-03]
PLAY RECAP ****************************************************************************************************************************************************************
mawenzi-02 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
mawenzi-03 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
mawenzi-04 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Then, using SSH we can verify that /etc/dnf/dnf.conf
was successfully replaced:
[root@mawenzi-01 ansible]# ssh mawenzi-02 cat /etc/dnf/dnf.conf
[main]
gpgcheck=0
installonly_limit=3
clean_requirements_on_remove=True
best=True
skip_if_unavailable=False
proxy=http://proxy.houston.hpecorp.net:8080