You have 15 servers. They all need the same Nginx config, the same firewall rules, the same monitoring agent. You could SSH into each one and run commands manually — or you could write one YAML file and let Ansible handle it in under a minute. That's the promise of Ansible: describe what you want, not how to do it.
Ansible is a configuration management and automation tool that uses SSH to connect to remote machines. No agents to install, no daemons to manage, no complex architecture. You write a playbook, point it at your servers, and it executes tasks in order. If something fails, it tells you exactly what went wrong.
1. Installation
Ansible runs on your control node (your laptop, a CI runner, a bastion host). It connects to managed nodes over SSH — those nodes need nothing installed.
# macOS
pip3 install ansible
# Ubuntu/Debian
sudo apt update && sudo apt install -y ansible
# Verify
ansible --version
Ansible requires Python 3 on the control node. Managed nodes also need Python (2.7+ or 3.5+), which is pre-installed on virtually every Linux distribution. For nodes without Python, you can use raw module tasks that execute shell commands directly.
2. Inventory: Telling Ansible Where to Connect
The inventory file lists your hosts. The simplest format is INI:
# inventory.ini
[webservers]
web1 ansible_host=192.168.1.10
web2 ansible_host=192.168.1.11
[dbservers]
db1 ansible_host=192.168.1.20
[all:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/id_ed25519
You can also use YAML format or dynamic inventory scripts that pull hosts from AWS, GCP, or any cloud provider's API. Dynamic inventory is essential when you auto-scale — static files don't work when instances come and go.
Test connectivity with:
ansible all -i inventory.ini -m ping
This isn't an ICMP ping — it's Ansible connecting over SSH, transferring a small Python script, and running it. If it returns "pong", you're good.
3. Your First Playbook
A playbook is a YAML file that defines a set of tasks to run on specific hosts:
# site.yml
---
- name: Configure web servers
hosts: webservers
become: yes
tasks:
- name: Install Nginx
apt:
name: nginx
state: present
update_cache: yes
- name: Copy Nginx config
copy:
src: files/nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
notify: Restart Nginx
- name: Ensure Nginx is running
service:
name: nginx
state: started
enabled: yes
handlers:
- name: Restart Nginx
service:
name: nginx
state: restarted
Run it:
ansible-playbook -i inventory.ini site.yml
Key things happening here:
become: yes— run tasks with sudo. Without this, you can't install packages or modify system files.notify— triggers a handler only when the task actually changes something. If the Nginx config is already identical, no restart happens.- Idempotency — run the same playbook 10 times. The first time installs Nginx and copies the config. The next 9 times, Ansible checks the current state and does nothing because everything already matches. This is the core principle of configuration management.
4. Variables and Templates
Hardcoding values in playbooks gets messy fast. Ansible lets you define variables at multiple levels:
# group_vars/webservers.yml
---
nginx_worker_processes: 4
nginx_worker_connections: 1024
server_name: example.com
Use Jinja2 templates to generate config files dynamically:
# templates/nginx.conf.j2
worker_processes {{ nginx_worker_connections }};
events {
worker_connections {{ nginx_worker_connections }};
}
http {
server {
listen 80;
server_name {{ server_name }};
root /var/www/html;
}
}
Reference the template in your playbook with the template module:
- name: Deploy Nginx config from template
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart Nginx
Variable precedence matters. Ansible has 22 levels of precedence — from role defaults (lowest) to extra vars passed with -e (highest). In practice, keep it simple: define defaults in group_vars/, override with -e for one-off runs.
5. Roles: Organizing Complexity
When your playbook grows past 50 lines, split it into roles. A role is a directory structure that encapsulates tasks, templates, variables, and handlers:
roles/
└── nginx/
├── tasks/main.yml
├── templates/nginx.conf.j2
├── handlers/main.yml
├── defaults/main.yml
└── files/static-site.conf
Use the role in your playbook:
- name: Configure web servers
hosts: webservers
become: yes
roles:
- nginx
- monitoring
- firewall
Roles are reusable. The same nginx role can be used across dev, staging, and production. Share roles across projects via Ansible Galaxy — think of it as npm or pip for Ansible.
6. Real-World Patterns
Conditional execution
Run tasks only when a condition is met:
- name: Install security updates (Debian/Ubuntu only)
apt:
upgrade: safe
update_cache: yes
when: ansible_os_family == "Debian"
- name: Install security updates (RHEL/CentOS only)
yum:
name: "*"
state: latest
security: yes
when: ansible_os_family == "RedHat"
Loops
Iterate over lists to create multiple resources:
- name: Create app users
user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
shell: /bin/bash
loop:
- { name: "deploy", groups: "sudo" }
- { name: "monitoring", groups: "adm" }
- { name: "backup", groups: "disk" }
Vault for secrets
Never store passwords in plain text. Ansible Vault encrypts sensitive data:
# Encrypt a file
ansible-vault encrypt group_vars/webservers/vault.yml
# Run playbook with vault password
ansible-playbook site.yml --ask-vault-pass
# Or use a password file (CI-friendly)
ansible-playbook site.yml --vault-password-file ~/.vault_pass
7. Ad-Hoc Commands
Not everything needs a playbook. For quick one-off tasks, use ad-hoc commands:
# Check disk space on all web servers
ansible webservers -i inventory.ini -a "df -h"
# Restart a service across all servers
ansible webservers -i inventory.ini -m service -a "name=nginx state=restarted" --become
# Copy a file to all servers
ansible all -i inventory.ini -m copy -a "src=motd.txt dest=/etc/motd" --become
Ad-hoc commands are great for troubleshooting and quick checks. For anything you'll run more than once, write a playbook.
8. Tips for Production Use
- Use
--checkmode first. Runansible-playbook site.yml --check --diffto preview changes without applying them. This is your dry run. - Limit to specific hosts. Use
--limit web1to run only on a single host. Combine with--checkfor safe rollouts. - Tag your tasks. Add
tags: [config]to tasks, then runansible-playbook site.yml --tags configto execute only tagged tasks. - Use
ansible-lint. It catches common mistakes — missingbecome, bad YAML formatting, deprecated modules — before you run anything. - Version control everything. Your playbooks, inventory, roles, templates — all in Git. Treat infrastructure as code.
- Keep playbooks idempotent. Test by running your playbook twice in a row. The second run should produce zero changes.
Summary
Ansible's power comes from its simplicity: no agents, YAML-based playbooks, SSH transport, and idempotent execution. Start with a single playbook that configures one server. Add inventory for multiple hosts. Extract common logic into roles. Encrypt secrets with Vault. Preview with --check.
The jump from "manually SSH-ing into servers" to "one command configures everything" is the biggest productivity multiplier in infrastructure management. Ansible makes that jump accessible — install it, write your first playbook, and you'll never go back.