Skip to content

Ansible Automation

Ansible is an agentless IT automation tool that connects to remote hosts via SSH to perform configuration management, application deployment, and task orchestration. It uses YAML-formatted Playbooks with a gentle learning curve, making it one of the most popular automation solutions in Enterprise Linux environments.

Ansible only needs to be installed on the control node (the machine from which you run commands). Managed remote hosts do not require any additional software — just SSH and Python.

Terminal window
# Method 1: Install via DNF (recommended)
sudo dnf install -y epel-release
sudo dnf install -y ansible-core
# Method 2: Install via pip (for the latest version)
sudo dnf install -y python3 python3-pip
pip3 install --user ansible
# Verify installation
ansible --version

Ansible modules are distributed as Collections:

Terminal window
# Install commonly used collections
ansible-galaxy collection install ansible.posix
ansible-galaxy collection install community.general
ansible-galaxy collection install community.mysql
# List installed collections
ansible-galaxy collection list

The Inventory file defines the hosts and groups that Ansible will manage.

Create /etc/ansible/hosts or inventory.ini in your project directory:

# Individual hosts
10.0.0.100
# Define groups
[webservers]
web01.example.com
web02.example.com
web03.example.com
[dbservers]
db01.example.com ansible_port=2222
db02.example.com
[appservers]
app[01:05].example.com # app01 through app05
# Group variables
[webservers:vars]
ansible_user=ops
ansible_become=true
http_port=80
[dbservers:vars]
ansible_user=dba
ansible_become=true
# Parent group (containing multiple child groups)
[production:children]
webservers
dbservers
appservers
inventory.yml
all:
children:
webservers:
hosts:
web01.example.com:
web02.example.com:
vars:
ansible_user: ops
http_port: 80
dbservers:
hosts:
db01.example.com:
ansible_port: 2222
db02.example.com:
vars:
ansible_user: dba
production:
children:
webservers:
dbservers:
Terminal window
# List all hosts
ansible-inventory -i inventory.yml --list
# Display as a graph
ansible-inventory -i inventory.yml --graph
# View variables for a specific host
ansible-inventory -i inventory.yml --host web01.example.com

Ad-Hoc commands are suitable for one-off quick operations.

Terminal window
# Test connectivity to all hosts
ansible all -i inventory.yml -m ping
# View remote host information
ansible webservers -i inventory.yml -m setup
# Execute shell commands
ansible all -i inventory.yml -m shell -a "uptime"
ansible all -i inventory.yml -m shell -a "free -h"
ansible all -i inventory.yml -m shell -a "df -h /"
# Manage packages
ansible webservers -i inventory.yml -m dnf -a "name=nginx state=present" --become
ansible webservers -i inventory.yml -m dnf -a "name=* state=latest" --become
# Manage services
ansible webservers -i inventory.yml -m service -a "name=nginx state=started enabled=yes" --become
# Copy files
ansible webservers -i inventory.yml -m copy -a "src=/tmp/app.conf dest=/etc/app.conf owner=root mode=0644" --become
# Manage users
ansible all -i inventory.yml -m user -a "name=deploy state=present groups=wheel" --become
# Reboot hosts
ansible dbservers -i inventory.yml -m reboot --become
# Limit execution scope
ansible webservers -i inventory.yml -m shell -a "hostname" --limit web01.example.com
# Parallelism control (default is 5 parallel)
ansible all -i inventory.yml -m ping -f 20

Playbooks are Ansible’s core configuration files, using YAML format to define an ordered series of tasks.

# site.yml - Basic web server configuration
---
- name: Configure web servers
hosts: webservers
become: true
vars:
http_port: 80
doc_root: /var/www/myapp
tasks:
- name: Install required packages
ansible.builtin.dnf:
name:
- nginx
- firewalld
- vim-enhanced
state: present
- name: Create website directory
ansible.builtin.file:
path: "{{ doc_root }}"
state: directory
owner: nginx
group: nginx
mode: '0755'
- name: Deploy homepage
ansible.builtin.copy:
content: |
<!DOCTYPE html>
<html>
<head><title>Welcome</title></head>
<body><h1>Server: {{ ansible_hostname }}</h1></body>
</html>
dest: "{{ doc_root }}/index.html"
owner: nginx
group: nginx
mode: '0644'
- name: Start and enable Nginx
ansible.builtin.service:
name: nginx
state: started
enabled: true
- name: Start and enable Firewalld
ansible.builtin.service:
name: firewalld
state: started
enabled: true
- name: Open HTTP port
ansible.posix.firewalld:
service: http
permanent: true
state: enabled
immediate: true
Terminal window
# Execute a Playbook
ansible-playbook -i inventory.yml site.yml
# Dry run (no actual changes)
ansible-playbook -i inventory.yml site.yml --check
# Verbose output
ansible-playbook -i inventory.yml site.yml -v
ansible-playbook -i inventory.yml site.yml -vvv
# Limit to specific hosts
ansible-playbook -i inventory.yml site.yml --limit web01.example.com
# Start from a specific task
ansible-playbook -i inventory.yml site.yml --start-at-task="Start and enable Nginx"
# Step-by-step confirmation
ansible-playbook -i inventory.yml site.yml --step
# Pass extra variables
ansible-playbook -i inventory.yml site.yml -e "http_port=8080"
tasks:
# Install a single package
- name: Install Nginx
ansible.builtin.dnf:
name: nginx
state: present
# Install multiple packages
- name: Install development tools
ansible.builtin.dnf:
name:
- gcc
- make
- python3-devel
state: present
# Install a specific version
- name: Install specific version
ansible.builtin.dnf:
name: nginx-1.20.1
state: present
# Update all packages
- name: System update
ansible.builtin.dnf:
name: '*'
state: latest
# Install a package group
- name: Install Development Tools group
ansible.builtin.dnf:
name: '@Development Tools'
state: present
# Remove a package
- name: Remove old software
ansible.builtin.dnf:
name: httpd
state: absent
tasks:
- name: Start and enable service
ansible.builtin.service:
name: nginx
state: started
enabled: true
- name: Restart service
ansible.builtin.service:
name: nginx
state: restarted
- name: Reload service configuration
ansible.builtin.service:
name: nginx
state: reloaded
- name: Stop and disable service
ansible.builtin.service:
name: postfix
state: stopped
enabled: false
tasks:
# Copy a file from the control node
- name: Copy configuration file
ansible.builtin.copy:
src: files/nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: true
notify: Reload Nginx
# Write content directly
- name: Write configuration
ansible.builtin.copy:
content: |
server {
listen 80;
server_name {{ ansible_fqdn }};
root /var/www/html;
}
dest: /etc/nginx/conf.d/default.conf
owner: root
group: root
mode: '0644'

Jinja2 templates are one of Ansible’s most powerful features, enabling dynamic configuration file generation based on variables.

Create a template file templates/nginx.conf.j2:

# Ansible managed - Do not edit manually
worker_processes {{ ansible_processor_vcpus }};
events {
worker_connections {{ nginx_worker_connections | default(1024) }};
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
{% for vhost in virtual_hosts %}
server {
listen {{ vhost.port | default(80) }};
server_name {{ vhost.server_name }};
root {{ vhost.doc_root }};
{% if vhost.ssl | default(false) %}
listen 443 ssl;
ssl_certificate {{ vhost.ssl_cert }};
ssl_certificate_key {{ vhost.ssl_key }};
{% endif %}
}
{% endfor %}
}

Use the template in a Playbook:

- name: Configure web servers
hosts: webservers
become: true
vars:
nginx_worker_connections: 2048
virtual_hosts:
- server_name: app.example.com
doc_root: /var/www/app
port: 80
- server_name: api.example.com
doc_root: /var/www/api
port: 8080
tasks:
- name: Deploy Nginx configuration
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
validate: nginx -t -c %s
notify: Reload Nginx
handlers:
- name: Reload Nginx
ansible.builtin.service:
name: nginx
state: reloaded
tasks:
# File and directory management
- name: Create a directory
ansible.builtin.file:
path: /data/app
state: directory
owner: app
group: app
mode: '0755'
- name: Create a symbolic link
ansible.builtin.file:
src: /data/app/current
dest: /var/www/app
state: link
# Manage scheduled tasks
- name: Add a backup cron job
ansible.builtin.cron:
name: "Database backup"
minute: "0"
hour: "2"
job: "/usr/local/bin/backup.sh >> /var/log/backup.log 2>&1"
user: root
# Manage SELinux booleans
- name: Allow Nginx network connections
ansible.posix.seboolean:
name: httpd_can_network_connect
state: true
persistent: true
# Manage sysctl parameters
- name: Set kernel parameters
ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: '1'
sysctl_set: true
reload: true
# Download files from a URL
- name: Download application package
ansible.builtin.get_url:
url: https://example.com/releases/app-1.0.tar.gz
dest: /tmp/app-1.0.tar.gz
checksum: sha256:abc123...
# Extract archives
- name: Extract application
ansible.builtin.unarchive:
src: /tmp/app-1.0.tar.gz
dest: /opt/
remote_src: true
tasks:
- name: Run only on AlmaLinux
ansible.builtin.dnf:
name: almalinux-release
state: latest
when: ansible_distribution == "AlmaLinux"
- name: Run only on EL9
ansible.builtin.debug:
msg: "Running on EL9"
when: ansible_distribution_major_version == "9"
- name: Restart if service exists
ansible.builtin.service:
name: nginx
state: restarted
when: "'nginx' in ansible_facts.packages"
tasks:
- name: Create multiple users
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
state: present
loop:
- { name: 'alice', groups: 'wheel' }
- { name: 'bob', groups: 'developers' }
- { name: 'charlie', groups: 'developers' }
- name: Start multiple services
ansible.builtin.service:
name: "{{ item }}"
state: started
enabled: true
loop:
- nginx
- firewalld
- fail2ban

Handlers are special tasks that only run when triggered by notify, and they execute after all tasks have completed:

- name: Configure servers
hosts: webservers
become: true
tasks:
- name: Update Nginx configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify:
- Validate Nginx configuration
- Reload Nginx
- name: Update sysctl configuration
ansible.builtin.copy:
src: sysctl.conf
dest: /etc/sysctl.d/99-custom.conf
notify: Apply sysctl
handlers:
- name: Validate Nginx configuration
ansible.builtin.command: nginx -t
- name: Reload Nginx
ansible.builtin.service:
name: nginx
state: reloaded
- name: Apply sysctl
ansible.builtin.command: sysctl --system

Roles are the best practice for organizing Playbooks. They categorize related tasks, variables, templates, and files into a standardized directory structure.

Terminal window
# Use ansible-galaxy to create a role skeleton
ansible-galaxy role init roles/webserver
# Generated directory structure
tree roles/webserver/
# roles/webserver/
# ├── defaults/
# │ └── main.yml # Default variables (lowest priority)
# ├── files/ # Static files
# ├── handlers/
# │ └── main.yml # Handlers
# ├── meta/
# │ └── main.yml # Role metadata and dependencies
# ├── tasks/
# │ └── main.yml # Main tasks
# ├── templates/ # Jinja2 templates
# └── vars/
# └── main.yml # Role variables (higher priority)
roles/webserver/defaults/main.yml
---
nginx_port: 80
nginx_worker_connections: 1024
doc_root: /var/www/html
roles/webserver/tasks/main.yml
---
- name: Install Nginx
ansible.builtin.dnf:
name: nginx
state: present
- name: Deploy Nginx configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Reload Nginx
- name: Create website directory
ansible.builtin.file:
path: "{{ doc_root }}"
state: directory
owner: nginx
group: nginx
mode: '0755'
- name: Start Nginx
ansible.builtin.service:
name: nginx
state: started
enabled: true
roles/webserver/handlers/main.yml
---
- name: Reload Nginx
ansible.builtin.service:
name: nginx
state: reloaded
site.yml
---
- name: Configure web servers
hosts: webservers
become: true
roles:
- webserver
- name: Configure database servers
hosts: dbservers
become: true
roles:
- role: database
vars:
db_port: 3306
Terminal window
# Search for a role
ansible-galaxy search nginx --platforms EL
# Install a role
ansible-galaxy role install geerlingguy.nginx
# Batch install from a requirements file
cat > requirements.yml << 'EOF'
---
roles:
- name: geerlingguy.nginx
- name: geerlingguy.mysql
collections:
- name: community.general
- name: ansible.posix
EOF
ansible-galaxy install -r requirements.yml
ansible-project/
├── ansible.cfg # Ansible configuration
├── inventory/
│ ├── production.yml # Production environment hosts
│ └── staging.yml # Staging environment hosts
├── group_vars/
│ ├── all.yml # Variables for all hosts
│ ├── webservers.yml # Web server group variables
│ └── dbservers.yml # Database group variables
├── host_vars/
│ └── web01.example.com.yml
├── roles/
│ ├── common/
│ ├── webserver/
│ └── database/
├── site.yml # Main Playbook
├── webservers.yml
└── dbservers.yml
# ansible.cfg
[defaults]
inventory = inventory/production.yml
remote_user = ops
roles_path = roles
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
[privilege_escalation]
become = True
become_method = sudo
become_ask_pass = False

Ansible Vault for Encrypting Sensitive Data

Section titled “Ansible Vault for Encrypting Sensitive Data”
Terminal window
# Encrypt a file
ansible-vault encrypt group_vars/dbservers.yml
# Edit an encrypted file
ansible-vault edit group_vars/dbservers.yml
# Decrypt a file
ansible-vault decrypt group_vars/dbservers.yml
# Run a Playbook with encrypted data
ansible-playbook site.yml --ask-vault-pass
# Use a password file
ansible-playbook site.yml --vault-password-file ~/.vault_pass

Ansible is a powerful tool for Enterprise Linux operations automation. From simple Ad-Hoc commands to complex multi-role Playbooks, it efficiently manages large-scale server clusters. Combining Cloud-Init for initial configuration with Ansible for subsequent deployment is a classic and proven workflow.