5.2.1. Ansible: Inventory, Playbooks, and Modules
💡 First Principle: Ansible connects to managed nodes over SSH (agentless), copies Python code to the node, executes it, captures results, and removes the code. The control node needs Ansible installed; managed nodes only need SSH access and Python. This is why Ansible is praised for low barrier to adoption — you don't need to pre-install agents on every machine.
Ansible Architecture:
Control Node (Ansible installed)
│
├── Inventory file (lists managed nodes)
│ 192.168.1.10
│ 192.168.1.11
│
├── Playbooks (.yml — what to do)
│
└── SSH connection to Managed Nodes
(no agent required — needs Python + SSH)
Inventory:
# /etc/ansible/hosts or custom inventory file
# Simple list
192.168.1.10
192.168.1.11
# Groups
[webservers]
web01.example.com
web02.example.com
[databases]
db01.example.com ansible_user=dbadmin ansible_port=2222
[production:children] # Group of groups
webservers
databases
[all:vars] # Variables for all hosts
ansible_python_interpreter=/usr/bin/python3
# Dynamic inventory — executable that outputs JSON
ansible-inventory --list -i inventory.py
Ad-hoc Commands (quick one-liners):
# Test connectivity
ansible all -m ping -i inventory.ini
# Run a shell command
ansible webservers -m shell -a "df -h" -i inventory.ini
# Install a package
ansible all -m package -a "name=nginx state=present" -b -i inventory.ini
# -b = become (sudo) -m = module -a = arguments
# Copy a file
ansible webservers -m copy -a "src=app.conf dest=/etc/app/app.conf"
Playbooks:
# site.yml
---
- name: Configure web servers
hosts: webservers
become: yes # Equivalent to sudo
vars:
nginx_port: 8080
max_connections: 1000
tasks:
- name: Install nginx
package:
name: nginx
state: present
- name: Deploy nginx config
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
notify: Restart nginx # Trigger handler if this task changes anything
- name: Ensure nginx is running and enabled
service:
name: nginx
state: started
enabled: yes
- name: Open firewall port
firewalld:
service: http
permanent: yes
state: enabled
immediate: yes
handlers:
- name: Restart nginx
service:
name: nginx
state: restarted
Key Ansible Modules:
| Module | Purpose | Example |
|---|---|---|
package | Install/remove packages (distro-agnostic) | name: nginx state: present |
apt / dnf | Distro-specific package management | name: nginx state: latest |
copy | Copy file to remote | src: file dest: /etc/ |
template | Deploy Jinja2 template | src: nginx.j2 dest: /etc/nginx/nginx.conf |
service | Manage systemd services | name: nginx state: started enabled: yes |
user | Manage user accounts | name: alice state: present groups: wheel |
file | Manage files/dirs/symlinks | path: /data state: directory mode: '0755' |
command / shell | Run commands (command = no shell features) | cmd: /usr/bin/myapp --init |
lineinfile | Ensure a line exists in a file | path: /etc/hosts line: '10.0.0.5 db01' |
cron | Manage cron entries | name: backup minute: 0 hour: 2 job: '/backup.sh' |
Ansible Variables and Conditionals:
# Variable precedence (highest wins): extra vars > task vars > host vars > group vars > role defaults
vars:
env: production
tasks:
- name: Install dev tools
package:
name: git
state: present
when: env == "development" # Conditional execution
- name: Restart service on RHEL only
service:
name: httpd
state: restarted
when: ansible_os_family == "RedHat"
# Loop
- name: Create multiple users
user:
name: "{{ item }}"
state: present
loop:
- alice
- bob
- carol
Ansible Galaxy — Roles:
ansible-galaxy install geerlingguy.nginx # Download a role
ansible-galaxy list # List installed roles
Roles are reusable collections of tasks, variables, handlers, and templates — packaged for sharing.
⚠️ Exam Trap (M9): Ansible modules are idempotent — running the same playbook twice should produce the same result with no unintended changes. The command and shell modules are NOT idempotent by default (they always run their command). Use creates or removes with command/shell to make them idempotent, or better yet, use purpose-built modules (package, service, copy) whenever possible.
Reflection Question: An Ansible task uses the shell module to run mkfs.ext4 /dev/sdb1. Why is this dangerous in a playbook that might be run more than once, and what would be a safer approach?