Ansible Fundamentals
Agentless, push-based config management. Writes YAML, runs over SSH (or WinRM, or network device APIs). Declarative-leaning. The default tool for “I want to reliably make the same changes across 100 servers.”
Why people pick Ansible
- Agentless — no software to install on managed hosts. Just needs SSH + Python on the target. Huge operational win.
- Push-based — you run ansible-playbook from your laptop (or a CI runner) and it connects to the targets. No constantly-running server to manage.
- YAML playbooks — readable, diffable, review-friendly. No “real” programming required for most tasks.
- Massive module library — Ansible ships with modules for everything: package managers, cloud APIs, network devices, firewalls, Windows, Kubernetes, SaaS APIs.
- Idempotent by convention — well-written tasks converge to the desired state whether you run them once or a hundred times.
The mental model
Ansible connects to one or more hosts (servers, network devices) and runs tasks on each. Each task uses a module that knows how to reach a particular desired state.
Your laptop / CI
│
│ (SSH/WinRM/API)
▼
┌───────────┬───────────┬───────────┐
│ host1 │ host2 │ host3 │
└───────────┴───────────┴───────────┘
└──────── tasks run in parallel ────────┘
The core concepts, bottom up
1. Inventory — what hosts exist
A file listing the hosts you can target, organised into groups:
[webservers]
web1.example.com
web2.example.com
[databases]
db1.example.com
[all:vars]
ansible_user=deployYAML inventory is equivalent. Dynamic inventories query cloud APIs (AWS, Azure, etc.) or CMDBs to build the list at runtime.
2. Module — the actual work
A module is a small program Ansible ships to the target and runs. Examples:
apt— Debian/Ubuntu packagesyum/dnf— RHEL/Fedora packagesservice/systemd— service statecopy/template— filesuser/group— local userslineinfile/blockinfile— safe file editscommand/shell— raw commands (use only when no module fits)uri— HTTP APIscisco.ios.ios_config— Cisco IOS network config- …thousands more
3. Task — one module invocation
- name: nginx is installed
apt:
name: nginx
state: presentThe name is a human-readable description. The module (apt) and its parameters (name, state) do the work.
4. Play — a list of tasks targeted at a group
- name: Web server setup
hosts: webservers
become: yes # sudo
tasks:
- name: nginx is installed
apt:
name: nginx
state: present
- name: nginx is running
service:
name: nginx
state: started
enabled: yes5. Playbook — a file with one or more plays
A playbook is a YAML file. Running it applies every play in order:
ansible-playbook -i inventory.ini site.yml6. Role — reusable playbook chunks
As playbooks grow, you extract reusable bits into roles. A role has a standard directory layout:
roles/nginx/
├── tasks/main.yml
├── handlers/main.yml
├── templates/nginx.conf.j2
├── defaults/main.yml
├── vars/main.yml
└── meta/main.yml
Plays use roles:
- hosts: webservers
roles:
- nginx
- firewallAnsible Galaxy is the public registry of community roles.
7. Variables & templates
Variables come from inventory, group_vars, host_vars, defaults, vars files, extra-vars on the command line — there’s a strict precedence order.
Templates use Jinja2 for interpolation:
server {
listen 80;
server_name {{ ansible_facts['hostname'] }};
root /var/www/{{ site_name }};
}8. Handlers — respond to changes
A handler is a task that runs only if something “notifies” it. Classic use: restart nginx when its config changes.
- name: Deploy nginx config
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
handlers:
- name: restart nginx
service:
name: nginx
state: restartedIf the template doesn’t change anything (idempotent), the handler doesn’t fire.
A complete minimal example
inventory.ini
[web]
192.168.1.10
192.168.1.11site.yml
- name: Web servers
hosts: web
become: yes
vars:
app_version: "1.2.3"
tasks:
- name: App user exists
user:
name: app
state: present
- name: App directory exists
file:
path: /opt/app
state: directory
owner: app
group: app
- name: App package installed
apt:
deb: "https://example.com/app-{{ app_version }}.deb"
state: present
- name: App service running
service:
name: app
state: started
enabled: yesRun:
ansible-playbook -i inventory.ini site.ymlRun again: everything already in desired state, nothing happens (thanks to Idempotence).
Ansible’s superpower: ansible ad-hoc
You don’t always need a playbook. For quick one-offs:
ansible web -i inventory.ini -m service -a "name=nginx state=restarted" --become“On the web group, run the service module with these args, use sudo.”
Useful for emergency fixes, running shell commands across a fleet, or testing before committing to a playbook.
Ansible for network automation
A core use case. Network modules target devices instead of servers:
- hosts: routers
gather_facts: no
connection: network_cli
tasks:
- name: Configure loopback
cisco.ios.ios_config:
lines:
- ip address 10.1.1.1 255.255.255.255
parents: interface Loopback0Ansible logs into the device via SSH, runs the CLI changes, and idempotently converges config.
Ansible vs alternatives
| Ansible | Puppet | Chef | Salt | |
|---|---|---|---|---|
| Agent on targets? | No | Yes | Yes | Yes (can be agentless) |
| Language | YAML + Jinja | DSL | Ruby DSL | YAML |
| Style | Push, mostly declarative | Pull, declarative | Pull, imperative-ish | Both |
| Learning curve | Low | Medium | High (Ruby) | Medium |
Ansible wins on the “agentless + low barrier to entry” combo, which is why it dominates.
Ansible vs Terraform
They overlap but aren’t the same:
- Terraform: provisions infrastructure (VMs, networks, DNS zones, cloud resources). Great at creating/modifying/destroying. Keeps state.
- Ansible: configures what’s inside those systems (users, packages, services, files, device configs). Weaker at provisioning; great at configuration.
Common pattern: Terraform provisions → Ansible configures.
Gotchas to know
commandandshellmodules are not idempotent by default. Usecreates:/removes:/changed_when:to make them so.- Inventory precedence is subtle; variables from
group_vars/allcan be overridden byhost_vars/specific. become(sudo) isn’t automatic. Set it at play, task, or command level explicitly.- Long-running tasks time out; use
async+pollfor fire-and-forget. - Dry run:
ansible-playbook ... --check --diffshows what would change without changing it. Always test first.
See also
- Declarative vs Imperative Automation
- Idempotence
- Python for Operations — Ansible modules are Python
- Git Fundamentals — playbooks live in git
- Automation-IaC