Ansible: Server Automation and Configuration Management (2026)
Ansible is the most widely used configuration management and automation tool in the industry. Unlike Puppet or Chef, it is agentless — it connects to remote hosts over SSH and executes tasks, requiring nothing installed on the managed nodes. This guide covers everything from basic inventory to production-grade playbooks with roles, Vault secrets, and Jinja2 templating.
Ansible Architecture
Ansible's architecture is deliberately simple:
- Control node: your machine or a CI server where Ansible is installed and playbooks are run.
- Managed nodes: the remote servers. No agent required — only SSH access and Python 3 (pre-installed on virtually all Linux systems).
- Inventory: the list of managed nodes and how they are grouped.
- Modules: the units of work (install a package, copy a file, restart a service). Ansible ships 3,000+ modules.
- Playbooks: YAML files that declare which tasks to run on which hosts.
- Plugins: extend Ansible with custom connection types, callbacks, lookups, and filters.
# Install Ansible on Ubuntu/Debian control node
sudo apt update && sudo apt install -y ansible
# Verify
ansible --version
# ansible [core 2.17.x]
# Test connectivity to all inventory hosts
ansible all -m ping -i inventory/hosts.yml
Inventory Formats
Ansible supports INI, YAML, and dynamic inventory scripts. YAML is recommended for readability:
# inventory/hosts.yml
all:
children:
webservers:
hosts:
web01:
ansible_host: 10.0.1.10
ansible_user: ubuntu
web02:
ansible_host: 10.0.1.11
ansible_user: ubuntu
vars:
http_port: 80
nginx_version: "1.26"
dbservers:
hosts:
db01:
ansible_host: 10.0.2.10
ansible_user: ubuntu
ansible_ssh_private_key_file: ~/.ssh/db_key
monitoring:
hosts:
mon01:
ansible_host: 10.0.3.10
# INI format (older, still common)
# inventory/hosts.ini
[webservers]
web01 ansible_host=10.0.1.10 ansible_user=ubuntu
web02 ansible_host=10.0.1.11 ansible_user=ubuntu
[webservers:vars]
http_port=80
[dbservers]
db01 ansible_host=10.0.2.10
[production:children]
webservers
dbservers
amazon.aws.aws_ec2) queries AWS and returns hosts by tag, region, or instance type. Configure with aws_ec2.yml in your inventory directory.
Playbook Anatomy
A playbook is a YAML file containing one or more plays. Each play targets a group of hosts and runs a list of tasks:
# playbooks/webserver.yml
---
- name: Configure and deploy web servers
hosts: webservers
become: true # sudo
gather_facts: true # collect host info (OS, IP, etc.)
vars:
app_user: deploy
app_dir: /opt/myapp
nginx_config_path: /etc/nginx/sites-available/myapp
pre_tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
tasks:
- name: Install Nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Create application directory
ansible.builtin.file:
path: "{{ app_dir }}"
state: directory
owner: "{{ app_user }}"
mode: "0755"
- name: Deploy Nginx configuration
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: "{{ nginx_config_path }}"
owner: root
group: root
mode: "0644"
notify: Reload Nginx
- name: Enable site
ansible.builtin.file:
src: "{{ nginx_config_path }}"
dest: /etc/nginx/sites-enabled/myapp
state: link
notify: Reload Nginx
handlers:
- name: Reload Nginx
ansible.builtin.service:
name: nginx
state: reloaded
post_tasks:
- name: Verify Nginx is running
ansible.builtin.uri:
url: "http://{{ ansible_host }}/health"
status_code: 200
retries: 3
delay: 5
Roles Structure
Roles are the Ansible way to organize reusable automation. A role is a directory with a conventional structure:
roles/
└── nginx/
├── tasks/
│ └── main.yml # task list
├── handlers/
│ └── main.yml # handlers (notified by tasks)
├── templates/
│ └── nginx.conf.j2 # Jinja2 templates
├── files/
│ └── index.html # static files to copy
├── vars/
│ └── main.yml # role-internal variables (high precedence)
├── defaults/
│ └── main.yml # default values (lowest precedence — override freely)
├── meta/
│ └── main.yml # role metadata, dependencies
└── README.md
# roles/nginx/defaults/main.yml
nginx_user: www-data
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
http_port: 80
https_port: 443
# roles/nginx/tasks/main.yml
---
- name: Install Nginx
ansible.builtin.apt:
name: "nginx={{ nginx_version | default('*') }}"
state: present
update_cache: true
- name: Configure Nginx
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: nginx -t -c %s
notify: Restart Nginx
- name: Ensure Nginx is enabled and started
ansible.builtin.service:
name: nginx
state: started
enabled: true
# Use the role in a playbook
- name: Set up web servers
hosts: webservers
become: true
roles:
- role: nginx
vars:
http_port: 8080
- role: app_deploy
- role: monitoring_agent
Jinja2 Templates
Ansible uses Jinja2 for templating configuration files and constructing dynamic values. Templates live in templates/*.j2 and are rendered with the template module:
# templates/nginx.conf.j2
user {{ nginx_user }};
worker_processes {{ nginx_worker_processes }};
pid /run/nginx.pid;
events {
worker_connections {{ nginx_worker_connections }};
}
http {
sendfile on;
keepalive_timeout {{ nginx_keepalive_timeout }};
{% for vhost in virtual_hosts %}
server {
listen {{ http_port }};
server_name {{ vhost.name }};
location / {
proxy_pass http://{{ vhost.backend_host }}:{{ vhost.backend_port }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
{% if vhost.enable_gzip | default(false) %}
gzip on;
gzip_types text/plain application/json;
{% endif %}
}
{% endfor %}
}
Variable Precedence
Ansible has 22 levels of variable precedence. Simplified from lowest to highest priority:
- Role defaults (
defaults/main.yml) — lowest, designed to be overridden - Inventory group vars (
group_vars/all.yml,group_vars/webservers.yml) - Inventory host vars (
host_vars/web01.yml) - Playbook
vars:block - Role vars (
vars/main.yml) include_varsandset_fact- Extra vars (
-e "key=value"on command line) — highest, overrides everything
defaults/main.yml. Put internal constants the role depends on in vars/main.yml. Use group_vars/ and host_vars/ for environment-specific overrides.
Ansible Vault
Ansible Vault encrypts sensitive data (passwords, API keys, certificates) so you can safely commit them to source control:
# Encrypt a file
ansible-vault encrypt group_vars/production/secrets.yml
# prompts for vault password
# Edit an encrypted file
ansible-vault edit group_vars/production/secrets.yml
# Encrypt a single value (inline vault)
ansible-vault encrypt_string 'supersecret123' --name 'db_password'
# outputs:
# db_password: !vault |
# $ANSIBLE_VAULT;1.1;AES256
# 61383937...
# Run a playbook with vault
ansible-playbook site.yml --ask-vault-pass
# or with a password file (for CI/CD):
ansible-playbook site.yml --vault-password-file ~/.vault_pass
# group_vars/production/secrets.yml (encrypted at rest)
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
61383937303632613031623936303530
...
aws_secret_key: !vault |
$ANSIBLE_VAULT;1.1;AES256
34623035613130623033376163376161
...
Essential Modules
| Module | Purpose | Example |
|---|---|---|
| ansible.builtin.apt / yum / dnf | Package management | name: nginx state: present |
| ansible.builtin.copy | Copy local file to remote | src: files/app.conf dest: /etc/app.conf |
| ansible.builtin.template | Render Jinja2 template to remote | src: nginx.j2 dest: /etc/nginx/nginx.conf |
| ansible.builtin.service | Start/stop/enable services | name: nginx state: started enabled: true |
| ansible.builtin.user | Manage OS users | name: deploy shell: /bin/bash groups: sudo |
| ansible.builtin.file | Manage files, dirs, symlinks | path: /opt/app state: directory mode: "0755" |
| ansible.builtin.shell / command | Run shell commands | Use command for simple; shell for pipes/redirects |
| ansible.builtin.lineinfile | Ensure a line exists in a file | path: /etc/hosts line: "10.0.1.1 db01" |
| ansible.builtin.git | Clone/checkout a repo | repo: https://github.com/... dest: /opt/app |
| ansible.builtin.uri | HTTP requests (health checks) | url: http://localhost/health status_code: 200 |
Idempotency
Idempotency means running the same playbook multiple times produces the same result without unintended side effects. Ansible modules are idempotent by design — apt: name=nginx state=present installs Nginx if absent, does nothing if already installed.
When writing custom tasks, ensure idempotency with conditions:
# Non-idempotent (runs every time, potentially harmful):
- name: Add line to config
ansible.builtin.shell: echo "max_connections=200" >> /etc/postgresql/pg.conf
# Idempotent (only adds line if absent):
- name: Set max_connections in postgres config
ansible.builtin.lineinfile:
path: /etc/postgresql/14/main/postgresql.conf
regexp: '^max_connections'
line: 'max_connections = 200'
backup: true
# Idempotent command with creates: guard
- name: Initialize application database
ansible.builtin.command:
cmd: /opt/app/bin/init-db.sh
creates: /opt/app/.db_initialized # skip if this file exists
Ansible vs Terraform
| Aspect | Ansible | Terraform |
|---|---|---|
| Primary purpose | Configuration management, app deployment | Infrastructure provisioning |
| State management | Stateless (checks live state each run) | State file (tracks what was created) |
| Language | YAML + Jinja2 | HCL (HashiCorp Config Language) |
| Agentless | Yes (SSH) | Yes (cloud APIs) |
| Idempotency | Module-level (ensure modules handle it) | Built-in (plan/apply model) |
| Best for | OS config, software install, deployments | Creating VMs, VPCs, RDS, EKS clusters |
| Use together? | Yes — Terraform provisions infrastructure, Ansible configures it | |
FAQ
- Do managed nodes need Python installed?
- Most Ansible modules require Python 3 on the managed node (this is pre-installed on all modern Linux distributions). For minimal containers or network devices, use
ansible_connection=network_clior therawmodule, which uses only SSH without Python. - What is the difference between shell and command modules?
- The
commandmodule runs a command directly without a shell — no pipes (|), redirects (>), or environment variable expansion. Theshellmodule invokes/bin/sh, supporting all shell features. Usecommandby default for security and predictability; useshellonly when you need shell features. - How do I run a playbook against only one host from a group?
- Use the
--limitflag:ansible-playbook site.yml --limit web01. You can also limit to a pattern:--limit "web*"or to a subset:--limit "webservers[0]"(first host in the group). - What is AWX and when should I use it?
- AWX is the open-source upstream of Red Hat Ansible Automation Platform (formerly Ansible Tower). It provides a web UI, role-based access control, job scheduling, and audit logs for Ansible. Use it when you need multiple team members running playbooks with controlled permissions, or when you want scheduled automation runs without managing cron jobs.
- How do I handle different Linux distributions in one playbook?
- Use
ansible_os_familyandansible_distributionfacts withwhen:conditions:when: ansible_os_family == "Debian"for apt-based tasks andwhen: ansible_os_family == "RedHat"for yum/dnf tasks. Or use thepackagemodule (distribution-agnostic) for simple installs.