Ansible: The Complete Guide for 2026
Ansible automates the tedious parts of managing servers. Instead of SSH-ing into machines one at a time, running commands, and hoping you remembered every step, you write a YAML playbook that describes what each server should look like. Ansible connects over SSH, makes the changes, and reports back. No agents to install, no master server to maintain, no new language to learn beyond YAML.
This guide covers Ansible from installation through production patterns. Every section includes real playbook examples you can adapt immediately. Pair this with our Terraform Complete Guide for infrastructure provisioning and Docker Compose Guide for container orchestration.
Table of Contents
- What Is Ansible and Why It Matters
- Installing Ansible
- Inventory Files
- Ad-Hoc Commands
- Playbook Basics
- Modules
- Variables, Facts, and Jinja2 Templates
- Roles and Directory Structure
- Ansible Galaxy
- Ansible Vault for Secrets
- Error Handling
- Real-World Playbook Examples
- Ansible vs Terraform vs Chef vs Puppet
- Best Practices
- FAQ
What Is Ansible and Why It Matters
Ansible is an open-source automation engine maintained by Red Hat. It handles configuration management, application deployment, and orchestration through declarative YAML files called playbooks. Three properties set it apart from older tools like Chef and Puppet:
Agentless. Ansible connects to managed nodes over SSH. No daemon runs on target machines. Nothing to install, update, or secure on the hosts you manage. If you can SSH to a server, Ansible can manage it.
Idempotent. Running the same playbook twice produces the same result. If a package is already installed, Ansible skips it. If a config file already has the correct content, Ansible leaves it alone. This makes playbooks safe to run repeatedly.
YAML-based. Playbooks are YAML files, not Ruby (Chef) or a custom DSL (Puppet). Any developer who can read a config file can read an Ansible playbook. The learning curve is measured in hours, not weeks.
Ansible runs on a control node (your laptop, a CI server, or a bastion host) and manages remote nodes. The control node needs Python 3.9+ and the ansible package. Managed nodes need only Python 3 and an SSH server — both present by default on every modern Linux distribution.
Installing Ansible
Install Ansible on your control node (not on managed servers):
# Ubuntu/Debian
sudo apt update
sudo apt install -y pipx
pipx install --include-deps ansible
# macOS
brew install ansible
# pip (any platform)
pip install ansible
# Verify installation
ansible --version
Ansible 9+ (ansible-core 2.16+) requires Python 3.10+ on the control node. Managed nodes need Python 3.6+ minimum. The pipx method keeps Ansible in an isolated environment and is the recommended approach for 2026.
Inventory Files
The inventory tells Ansible which hosts to manage. You define groups of servers with connection details. Ansible supports INI and YAML formats.
INI format (simple and common):
# inventory.ini
[webservers]
web1.example.com
web2.example.com ansible_port=2222
[dbservers]
db1.example.com ansible_user=dbadmin
db2.example.com
[loadbalancers]
lb1.example.com
[production:children]
webservers
dbservers
loadbalancers
[all:vars]
ansible_python_interpreter=/usr/bin/python3
YAML format (better for complex inventories):
# inventory.yml
all:
children:
webservers:
hosts:
web1.example.com:
web2.example.com:
ansible_port: 2222
dbservers:
hosts:
db1.example.com:
ansible_user: dbadmin
db2.example.com:
production:
children:
webservers:
dbservers:
The :children keyword creates groups of groups. all is a built-in group containing every host. Use ansible-inventory --list -i inventory.yml to verify your inventory structure. For cloud environments, use dynamic inventory plugins that query AWS EC2, Azure, or GCP for current instances automatically.
Ad-Hoc Commands
Ad-hoc commands run a single module against hosts without writing a playbook. They are useful for quick checks and one-off tasks:
# Ping all hosts (tests connectivity)
ansible all -i inventory.ini -m ping
# Check disk space on web servers
ansible webservers -m command -a "df -h"
# Install a package on db servers
ansible dbservers -m apt -a "name=htop state=present" --become
# Restart nginx on web servers
ansible webservers -m service -a "name=nginx state=restarted" --become
# Copy a file to all hosts
ansible all -m copy -a "src=./motd.txt dest=/etc/motd" --become
# Run with 10 parallel forks (default is 5)
ansible all -m ping -f 10
The --become flag escalates to root via sudo. The -m flag selects the module. The -a flag passes module arguments. Ad-hoc commands are for quick operations; use playbooks for anything you need to repeat.
Playbook Basics
A playbook is a YAML file containing one or more plays. Each play targets a group of hosts and defines an ordered list of tasks:
# site.yml
---
- name: Configure web servers
hosts: webservers
become: true
vars:
http_port: 80
doc_root: /var/www/html
tasks:
- name: Install nginx
apt:
name: nginx
state: present
update_cache: true
- name: Copy site configuration
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/default
notify: Restart nginx
- name: Create document root
file:
path: "{{ doc_root }}"
state: directory
owner: www-data
mode: "0755"
- name: Deploy index page
copy:
content: "<h1>Hello from {{ inventory_hostname }}</h1>"
dest: "{{ doc_root }}/index.html"
handlers:
- name: Restart nginx
service:
name: nginx
state: restarted
Run it with ansible-playbook -i inventory.ini site.yml.
Handlers are tasks that run only when notified by another task. If the nginx config file does not change, the handler never fires. This prevents unnecessary service restarts. Handlers run once at the end of all tasks, even if notified multiple times.
Key playbook elements:
hosts— which inventory group to targetbecome— escalate privileges (sudo)vars— variables available to all tasks in the playtasks— ordered list of actions to performhandlers— tasks triggered bynotifypre_tasks/post_tasks— run before/after roles
Modules
Modules are the units of work in Ansible. Each module handles a specific task. Ansible ships with thousands of modules. Here are the ones you will use daily:
Package management:
# Debian/Ubuntu
- apt:
name: [nginx, curl, htop]
state: present
update_cache: true
# RHEL/CentOS/Fedora
- yum:
name: httpd
state: latest
# Generic (auto-detects package manager)
- package:
name: git
state: present
File operations:
# Copy a file
- copy:
src: files/app.conf
dest: /etc/app/app.conf
owner: root
mode: "0644"
# Render a Jinja2 template
- template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: nginx -t -c %s
notify: Reload nginx
# Create a directory
- file:
path: /opt/myapp/logs
state: directory
owner: appuser
mode: "0755"
# Download a file
- get_url:
url: https://example.com/app-v2.tar.gz
dest: /tmp/app-v2.tar.gz
checksum: sha256:abc123...
Services and containers:
# Manage systemd services
- service:
name: nginx
state: started
enabled: true
# Run a Docker container
- docker_container:
name: redis
image: redis:7-alpine
state: started
restart_policy: always
ports:
- "6379:6379"
volumes:
- redis_data:/data
# Execute a shell command
- shell: |
cd /opt/myapp && ./deploy.sh
args:
creates: /opt/myapp/.deployed
Users and permissions:
- user:
name: deploy
groups: [sudo, docker]
shell: /bin/bash
- authorized_key:
user: deploy
key: "{{ lookup('file', 'keys/deploy.pub') }}"
Every module is idempotent. state: present ensures something exists. state: absent removes it. state: latest ensures the newest version.
Variables, Facts, and Jinja2 Templates
Variables make playbooks reusable. Ansible has a well-defined variable precedence order with over 20 levels. The most common sources, from lowest to highest priority:
- Role defaults (
roles/x/defaults/main.yml) - Inventory group_vars (
group_vars/webservers.yml) - Inventory host_vars (
host_vars/web1.yml) - Play vars (in the playbook)
- Task vars (in a task)
- Extra vars (
-eon the command line) — highest priority
Facts are variables automatically gathered from managed nodes:
# Access system facts
{{ ansible_hostname }}
{{ ansible_os_family }} # "Debian", "RedHat"
{{ ansible_default_ipv4.address }}
{{ ansible_memtotal_mb }}
# Disable fact gathering for speed: gather_facts: false
Jinja2 templates render dynamic config files. Create a template at templates/nginx.conf.j2:
server {
listen {{ http_port }};
server_name {{ server_name }};
root {{ doc_root }};
{% for location in proxy_locations %}
location {{ location.path }} {
proxy_pass {{ location.backend }};
}
{% endfor %}
{% if enable_ssl %}
listen 443 ssl;
ssl_certificate {{ ssl_cert_path }};
ssl_certificate_key {{ ssl_key_path }};
{% endif %}
}
Conditionals and loops in tasks:
# Conditional execution
- name: Install Apache on RedHat
yum:
name: httpd
state: present
when: ansible_os_family == "RedHat"
# Loop over a list
- name: Create app users
user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
loop:
- { name: alice, groups: "sudo,docker" }
- { name: bob, groups: "docker" }
# Register output and use it
- name: Check if app is running
command: pgrep myapp
register: app_status
ignore_errors: true
- name: Start app if not running
command: /opt/myapp/start.sh
when: app_status.rc != 0
Roles and Directory Structure
Roles organize playbooks into reusable components. A role has a fixed directory structure:
roles/
webserver/
tasks/
main.yml # Task list
handlers/
main.yml # Handlers
templates/
nginx.conf.j2 # Jinja2 templates
files/
index.html # Static files
vars/
main.yml # Role variables (high priority)
defaults/
main.yml # Default variables (low priority)
meta/
main.yml # Role metadata and dependencies
Create a role scaffold with ansible-galaxy init roles/webserver.
Role tasks (roles/webserver/tasks/main.yml):
---
- name: Install nginx
apt:
name: nginx
state: present
update_cache: true
- name: Deploy nginx config
template:
src: nginx.conf.j2
dest: /etc/nginx/sites-available/default
notify: Reload nginx
- name: Ensure nginx is running
service:
name: nginx
state: started
enabled: true
Use roles in a playbook:
---
- name: Configure infrastructure
hosts: all
become: true
roles:
- common
- { role: webserver, http_port: 8080 }
- { role: monitoring, when: enable_monitoring }
Roles are the correct abstraction level for sharing automation. A common role handles base packages, SSH keys, and NTP. A webserver role handles nginx. A database role handles PostgreSQL. Each role is testable, reusable, and self-contained.
Ansible Galaxy
Ansible Galaxy is the community hub for sharing roles and collections. Instead of writing a PostgreSQL role from scratch, install a battle-tested one:
# Install a single role
ansible-galaxy install geerlingguy.docker
# Install a collection
ansible-galaxy collection install community.docker
# Install from a requirements file
ansible-galaxy install -r requirements.yml
requirements.yml (pin versions for reproducible builds):
---
roles:
- name: geerlingguy.docker
version: "7.1.0"
- name: geerlingguy.nginx
- name: geerlingguy.postgresql
collections:
- name: community.docker
version: ">=3.0.0"
- name: amazon.aws
Collections are the modern packaging format, bundling roles, modules, and plugins into a single installable unit. The community.docker collection provides docker_container and docker_compose_v2 modules. The amazon.aws collection provides EC2, S3, and RDS modules. Always pin versions and run ansible-galaxy install -r requirements.yml in CI.
Ansible Vault for Secrets
Ansible Vault encrypts sensitive data — passwords, API keys, TLS certificates — so you can safely commit them to version control.
# Create an encrypted file
ansible-vault create secrets.yml
# Encrypt an existing file
ansible-vault encrypt group_vars/production/secrets.yml
# Edit an encrypted file
ansible-vault edit secrets.yml
# Decrypt (view) an encrypted file
ansible-vault view secrets.yml
# Run a playbook with vault
ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file ~/.vault_pass
Encrypt individual variables instead of entire files:
# Encrypt a single value
ansible-vault encrypt_string 'SuperSecret123' --name 'db_password'
# Output (paste this into your vars file):
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
6531326164...
Best practice: keep encrypted vars in separate files (group_vars/production/vault.yml) alongside unencrypted vars (group_vars/production/vars.yml). This way you can see variable names without decrypting, and diffs show which files changed.
Error Handling
Ansible stops execution when a task fails. You can customize this behavior with several mechanisms:
block/rescue/always (try/catch/finally for Ansible):
- name: Deploy with rollback
block:
- name: Deploy new version
copy:
src: app-v2.tar.gz
dest: /opt/app/app.tar.gz
- name: Extract and restart
shell: |
cd /opt/app && tar xzf app.tar.gz
systemctl restart myapp
rescue:
- name: Rollback to previous version
copy:
src: /opt/app/backup/app.tar.gz
dest: /opt/app/app.tar.gz
- name: Restart with old version
service:
name: myapp
state: restarted
always:
- name: Clean up temp files
file:
path: /tmp/deploy-staging
state: absent
Other error handling patterns:
# Ignore errors on a task
- name: Check optional service
command: systemctl status optional-service
ignore_errors: true
# Custom failure conditions
- name: Check app health
uri:
url: http://localhost:8080/health
register: health
failed_when: health.json.status != "UP"
# Custom change detection
- name: Run database migration
command: python manage.py migrate
register: migrate_result
changed_when: "'No migrations to apply' not in migrate_result.stdout"
# Retry a task
- name: Wait for service to start
uri:
url: http://localhost:8080/health
register: result
until: result.status == 200
retries: 10
delay: 5
Real-World Playbook Examples
LAMP stack deployment:
---
- name: Deploy LAMP stack
hosts: webservers
become: true
vars:
app_db_name: myapp
app_db_user: myapp_user
php_version: "8.3"
tasks:
- name: Install Apache, PHP, and MariaDB
apt:
name: [apache2, "php{{ php_version }}", "php{{ php_version }}-mysql",
mariadb-server, python3-pymysql]
state: present
update_cache: true
- name: Start and enable services
service:
name: "{{ item }}"
state: started
enabled: true
loop: [apache2, mariadb]
- name: Create application database
mysql_db:
name: "{{ app_db_name }}"
state: present
login_unix_socket: /var/run/mysqld/mysqld.sock
- name: Create database user
mysql_user:
name: "{{ app_db_user }}"
password: "{{ vault_db_user_pass }}"
priv: "{{ app_db_name }}.*:ALL"
state: present
login_unix_socket: /var/run/mysqld/mysqld.sock
- name: Deploy application code
synchronize:
src: app/
dest: /var/www/html/
notify: Restart Apache
handlers:
- name: Restart Apache
service:
name: apache2
state: restarted
Docker deployment:
---
- name: Deploy Docker application stack
hosts: appservers
become: true
tasks:
- name: Install Docker
apt:
name: [docker-ce, docker-ce-cli, containerd.io]
state: present
update_cache: true
- name: Deploy application container
docker_container:
name: webapp
image: "myregistry/webapp:{{ app_version | default('latest') }}"
state: started
restart_policy: unless-stopped
ports: ["8080:8080"]
env:
DATABASE_URL: "{{ db_connection_string }}"
networks:
- name: app_network
- name: Deploy Redis
docker_container:
name: redis
image: redis:7-alpine
state: started
restart_policy: unless-stopped
networks:
- name: app_network
User management:
---
- name: Manage system users
hosts: all
become: true
vars:
admin_users:
- { name: alice, key: "ssh-ed25519 AAAA...", groups: "sudo,docker" }
- { name: bob, key: "ssh-ed25519 AAAA...", groups: "docker" }
removed_users: [charlie, dave]
tasks:
- name: Create admin users
user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
shell: /bin/bash
create_home: true
loop: "{{ admin_users }}"
- name: Set authorized keys
authorized_key:
user: "{{ item.name }}"
key: "{{ item.key }}"
exclusive: true
loop: "{{ admin_users }}"
- name: Remove former employees
user:
name: "{{ item }}"
state: absent
remove: true
loop: "{{ removed_users }}"
- name: Harden SSH config
lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
loop:
- { regexp: "^PermitRootLogin", line: "PermitRootLogin no" }
- { regexp: "^PasswordAuthentication", line: "PasswordAuthentication no" }
notify: Restart sshd
handlers:
- name: Restart sshd
service:
name: sshd
state: restarted
Ansible vs Terraform vs Chef vs Puppet
| Feature | Ansible | Terraform | Chef | Puppet |
|---|---|---|---|---|
| Primary Use | Config mgmt | Infra provisioning | Config mgmt | Config mgmt |
| Language | YAML | HCL | Ruby DSL | Puppet DSL |
| Architecture | Agentless (SSH) | Agentless (API) | Agent + Server | Agent + Server |
| State | Stateless | State file | Server-side | Server-side |
| Learning Curve | Low | Medium | High | High |
| Best For | Server config, deploy | Cloud infra | Complex policies | Enterprise compliance |
The practical answer: Use Terraform to create infrastructure (VMs, networks, databases). Use Ansible to configure what runs on that infrastructure (packages, services, deployments). They complement each other. Chef and Puppet are mature tools but require dedicated infrastructure (Chef Server, PuppetDB) and custom DSLs that increase operational overhead. For teams starting fresh in 2026, Ansible + Terraform covers most automation needs.
Best Practices
- Use roles for everything. Even small projects benefit from role structure. It forces you to organize variables, templates, and handlers logically.
- Pin versions. Pin role versions in
requirements.yml, Ansible version in CI, and collection versions. Unversioned dependencies break builds. - Use
ansible-lint. Catches common mistakes, enforces style, and prevents deprecated module usage. Add it to CI alongside your playbooks. - Test with Molecule. Molecule creates ephemeral Docker or Vagrant instances, runs your role, and verifies results. Test roles before deploying to production.
- Keep playbooks small. A playbook should orchestrate roles, not contain hundreds of tasks. If a playbook exceeds 100 lines, refactor into roles.
- Name every task.
- name: Install nginxis mandatory, not optional. Unnamed tasks produce unreadable output and are impossible to debug. - Use
group_varsandhost_vars. Never hardcode server-specific values in playbooks. Environment differences belong in inventory variables. - Vault for all secrets. Never commit plaintext passwords, API keys, or certificates. Use Vault with a password file in CI.
- Use
--checkand--diff. Dry-run playbooks before applying.--diffshows exact file changes. This is your safety net. - Tag tasks for partial runs. Add
tags: [deploy, nginx]to tasks. Run subsets with--tags deployinstead of the full playbook.
Frequently Asked Questions
What is Ansible and what is it used for?
Ansible is an open-source automation tool for configuration management, application deployment, and infrastructure orchestration. It uses YAML playbooks to describe desired system state and executes tasks over SSH without requiring agents on managed nodes. It is used to provision servers, deploy applications, manage configurations, and automate repetitive IT tasks across hundreds or thousands of machines simultaneously.
What is the difference between Ansible and Terraform?
Terraform excels at provisioning infrastructure: creating cloud resources like VMs, networks, and databases using declarative HCL with state tracking. Ansible excels at configuring those resources after they exist: installing packages, deploying code, managing services. Terraform tracks resource state in a state file; Ansible is stateless and checks current state on each run. They are complementary — use Terraform to create servers, then Ansible to configure them.
How does Ansible's agentless architecture work?
Ansible connects to managed nodes over SSH (Linux) or WinRM (Windows). It copies small Python modules to the target, executes them, captures the output, and removes the temporary files. No daemon runs on managed nodes between runs. This means zero software to install on targets, no listening ports to secure, and no resource overhead. You only need Python installed on managed nodes, which is present by default on most Linux distributions.
What is the difference between ansible-playbook and the ansible command?
The ansible command runs single ad-hoc tasks, useful for one-off operations like restarting a service or checking disk space. The ansible-playbook command executes YAML playbooks containing ordered lists of tasks, handlers, variables, and roles. Use ansible for quick checks; use ansible-playbook for repeatable, version-controlled automation.
What is Ansible Galaxy and how do I use it?
Ansible Galaxy is a public repository of community-contributed roles and collections. Install roles with ansible-galaxy install geerlingguy.docker or define dependencies in a requirements.yml file. Collections bundle roles, modules, and plugins into distributable packages. Galaxy saves you from writing common automation from scratch, with popular roles for Nginx, Docker, PostgreSQL, and hundreds of other tools.
Conclusion
Ansible remains the most accessible automation tool in 2026. Its agentless architecture, YAML playbooks, and massive module library mean you can go from zero to automating your entire infrastructure in a day. The patterns in this guide — roles for organization, Vault for secrets, Galaxy for reuse, block/rescue for error handling — scale from a handful of servers to thousands.
Start with three things: write an inventory file for your servers, create a playbook that installs one package, and run it with --check first. Once you see the power of declarative configuration, you will never SSH into servers to make manual changes again.
Learn More
- Terraform: The Complete Guide — provision the infrastructure Ansible configures
- Kubernetes: The Complete Guide — orchestrate containers at scale
- Docker Compose: The Complete Guide — multi-container applications on a single host
- GitHub Actions CI/CD Guide — automate Ansible playbook runs in pipelines
- Linux Commands Cheat Sheet — manage the servers Ansible configures