Logging into dozens of servers one by one to install packages, edit configuration files, and restart services is tedious and error-prone. A forgotten step on one server leads to configuration drift, and suddenly your “identical” servers behave differently. Ansible solves this by letting you define your entire server configuration in simple YAML files and apply it consistently across hundreds of machines with a single command. Because Ansible is agentless and communicates over SSH, there is nothing to install on your managed servers — if you can SSH into a host, Ansible can manage it.
What Is Ansible?
Ansible is an open-source automation tool created by Red Hat that manages server configuration, application deployment, and orchestration. Unlike tools that require a dedicated agent running on every managed server, Ansible uses SSH to connect to remote hosts and execute tasks.
Core characteristics:
- Agentless — No software to install or maintain on managed nodes. Ansible uses existing SSH connections
- Push-based — The control node pushes configurations to managed hosts on demand, rather than hosts polling a central server
- Idempotent — Running a playbook multiple times produces the same result. Ansible checks the current state before making changes
- YAML-based — Playbooks are written in human-readable YAML, making them accessible to anyone who can read a configuration file
- Extensible — Thousands of built-in modules for managing packages, files, services, cloud resources, and more
How Ansible Works
Ansible follows a straightforward execution model:
- You write a playbook (YAML file) describing the desired state of your servers
- You define an inventory listing which hosts to manage
- You run
ansible-playbook, and Ansible connects to each host over SSH - Ansible transfers small programs called modules to the remote host
- Modules execute, enforce the desired state, and report back
- Ansible removes the modules and displays a summary
No daemon runs on the managed hosts. No database tracks state. Each playbook run is self-contained.
Prerequisites
Before you begin, ensure you have:
- An Ubuntu 22.04 or 24.04 machine as the control node (where Ansible runs)
- One or more target servers accessible via SSH
- SSH key-based authentication configured between the control node and managed hosts
- Python 3 installed on all managed nodes (pre-installed on most Linux distributions)
- A user account with sudo privileges on the managed hosts
Tip: If you have not set up SSH key authentication yet, follow our SSH hardening guide to configure secure key-based access before proceeding.
Installing Ansible on Ubuntu
Using the system package manager
sudo apt update
sudo apt install ansible -y
Using pip (recommended for the latest version)
sudo apt install python3-pip python3-venv -y
python3 -m venv ~/ansible-venv
source ~/ansible-venv/bin/activate
pip install ansible
Verify the installation
ansible --version
You should see output showing the Ansible version, configuration file path, and Python version:
ansible [core 2.16.x]
config file = /etc/ansible/ansible.cfg
configured module search path = ['/home/user/.ansible/plugins/modules']
python version = 3.12.x
Inventory Files
The inventory tells Ansible which hosts to manage. You can write inventories in INI or YAML format.
INI format (simple)
# inventory.ini
[webservers]
web1.example.com
web2.example.com
192.168.1.50
[dbservers]
db1.example.com ansible_port=2222
db2.example.com
[loadbalancers]
lb1.example.com
[production:children]
webservers
dbservers
loadbalancers
[all:vars]
ansible_user=deployer
ansible_ssh_private_key_file=~/.ssh/id_ed25519
YAML format
# inventory.yml
all:
vars:
ansible_user: deployer
ansible_ssh_private_key_file: ~/.ssh/id_ed25519
children:
webservers:
hosts:
web1.example.com:
web2.example.com:
192.168.1.50:
dbservers:
hosts:
db1.example.com:
ansible_port: 2222
db2.example.com:
loadbalancers:
hosts:
lb1.example.com:
production:
children:
webservers:
dbservers:
loadbalancers:
Testing connectivity
Verify that Ansible can reach all hosts:
ansible all -m ping -i inventory.ini
Expected output for a successful connection:
web1.example.com | SUCCESS => {
"changed": false,
"ping": "pong"
}
Your First Playbook
A playbook is a YAML file containing one or more “plays.” Each play targets a group of hosts and defines tasks to execute.
Install and configure nginx
# site.yml
---
- name: Configure web servers
hosts: webservers
become: true
tasks:
- name: Update apt cache
apt:
update_cache: true
cache_valid_time: 3600
- name: Install nginx
apt:
name: nginx
state: present
- name: Copy nginx configuration
copy:
src: files/nginx.conf
dest: /etc/nginx/sites-available/default
owner: root
group: root
mode: "0644"
notify: Restart nginx
- name: Enable nginx site
file:
src: /etc/nginx/sites-available/default
dest: /etc/nginx/sites-enabled/default
state: link
notify: Restart nginx
- name: Ensure nginx is running and enabled
service:
name: nginx
state: started
enabled: true
handlers:
- name: Restart nginx
service:
name: nginx
state: restarted
Run the playbook:
ansible-playbook -i inventory.ini site.yml
Run with verbose output for debugging:
ansible-playbook -i inventory.ini site.yml -v
Perform a dry run to see what would change without applying:
ansible-playbook -i inventory.ini site.yml --check --diff
Ansible Modules
Modules are the units of work in Ansible. Each module handles a specific task. Here are the most commonly used modules:
Package management (apt)
- name: Install multiple packages
apt:
name:
- nginx
- curl
- ufw
- fail2ban
state: present
update_cache: true
- name: Remove a package
apt:
name: apache2
state: absent
purge: true
Service management (service)
- name: Start and enable a service
service:
name: nginx
state: started
enabled: true
- name: Restart a service
service:
name: mysql
state: restarted
File management (copy, template, file)
- name: Copy a file to remote host
copy:
src: files/app.conf
dest: /etc/app/app.conf
owner: www-data
group: www-data
mode: "0644"
- name: Deploy a template with variables
template:
src: templates/vhost.conf.j2
dest: /etc/nginx/sites-available/myapp.conf
owner: root
group: root
mode: "0644"
- name: Create a directory
file:
path: /var/www/myapp
state: directory
owner: www-data
group: www-data
mode: "0755"
Command execution (command, shell)
- name: Run a command
command: /usr/bin/myapp --init
args:
creates: /var/lib/myapp/initialized
- name: Run a shell command with pipes
shell: cat /var/log/syslog | grep error | wc -l
register: error_count
changed_when: false
User management (user)
- name: Create a deploy user
user:
name: deployer
groups: sudo,www-data
shell: /bin/bash
create_home: true
state: present
- name: Add SSH authorized key
authorized_key:
user: deployer
key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
state: present
Variables and Facts
Variables make your playbooks flexible and reusable. Ansible supports several variable sources.
Defining variables in playbooks
---
- name: Deploy application
hosts: webservers
become: true
vars:
app_name: myapp
app_port: 8080
app_user: www-data
packages:
- nginx
- python3
- python3-pip
tasks:
- name: Install required packages
apt:
name: "{{ packages }}"
state: present
- name: Create app directory
file:
path: "/var/www/{{ app_name }}"
state: directory
owner: "{{ app_user }}"
mode: "0755"
Group variables and host variables
Create directories for group-specific and host-specific variables:
project/
inventory.ini
group_vars/
all.yml
webservers.yml
dbservers.yml
host_vars/
web1.example.com.yml
db1.example.com.yml
playbook.yml
# group_vars/webservers.yml
http_port: 80
https_port: 443
document_root: /var/www/html
max_workers: 4
# host_vars/web1.example.com.yml
server_name: web1.example.com
ssl_certificate: /etc/ssl/certs/web1.pem
Gathering facts
Ansible automatically gathers system facts (OS, IP addresses, memory, disk) from managed hosts. Use them in templates and conditionals:
- name: Display system information
debug:
msg: >
Hostname: {{ ansible_hostname }},
OS: {{ ansible_distribution }} {{ ansible_distribution_version }},
IP: {{ ansible_default_ipv4.address }},
RAM: {{ ansible_memtotal_mb }} MB
- name: Install packages based on OS
apt:
name: nginx
state: present
when: ansible_os_family == "Debian"
- name: Install packages on Red Hat
yum:
name: nginx
state: present
when: ansible_os_family == "RedHat"
Handlers and Notifications
Handlers are special tasks that run only when notified by another task. They are triggered at the end of a play, after all regular tasks have completed. This prevents unnecessary service restarts.
---
- name: Configure web server
hosts: webservers
become: true
tasks:
- name: Install nginx
apt:
name: nginx
state: present
- name: Deploy main nginx config
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify:
- Validate nginx config
- Restart nginx
- name: Deploy virtual host
template:
src: templates/vhost.conf.j2
dest: /etc/nginx/sites-available/{{ domain }}.conf
notify:
- Validate nginx config
- Restart nginx
- name: Enable virtual host
file:
src: /etc/nginx/sites-available/{{ domain }}.conf
dest: /etc/nginx/sites-enabled/{{ domain }}.conf
state: link
notify:
- Validate nginx config
- Restart nginx
handlers:
- name: Validate nginx config
command: nginx -t
listen: "Validate nginx config"
- name: Restart nginx
service:
name: nginx
state: restarted
listen: "Restart nginx"
Even though three tasks notify the handlers, Restart nginx runs only once at the end of the play.
Roles and Directory Structure
Roles organize your playbooks into reusable, shareable components. Each role encapsulates tasks, handlers, variables, templates, and files for a specific function.
Creating a role
ansible-galaxy init roles/webserver
This generates the following structure:
roles/
webserver/
tasks/
main.yml
handlers/
main.yml
templates/
files/
vars/
main.yml
defaults/
main.yml
meta/
main.yml
Role tasks
# roles/webserver/tasks/main.yml
---
- name: Install nginx
apt:
name: nginx
state: present
update_cache: true
- name: Deploy nginx configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart nginx
- name: Deploy virtual host
template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ domain }}.conf"
notify: Restart nginx
- name: Enable virtual host
file:
src: "/etc/nginx/sites-available/{{ domain }}.conf"
dest: "/etc/nginx/sites-enabled/{{ domain }}.conf"
state: link
notify: Restart nginx
- name: Remove default virtual host
file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: Restart nginx
- name: Ensure nginx is running
service:
name: nginx
state: started
enabled: true
Role handlers
# roles/webserver/handlers/main.yml
---
- name: Restart nginx
service:
name: nginx
state: restarted
Role defaults
# roles/webserver/defaults/main.yml
---
domain: example.com
http_port: 80
https_port: 443
document_root: /var/www/html
worker_processes: auto
worker_connections: 1024
Using roles in a playbook
# site.yml
---
- name: Configure infrastructure
hosts: webservers
become: true
roles:
- role: webserver
vars:
domain: knowledgexchange.xyz
- role: security
- role: monitoring
Practical Examples
LAMP stack deployment
# lamp.yml
---
- name: Deploy LAMP stack
hosts: webservers
become: true
vars:
mysql_root_password: "{{ vault_mysql_root_password }}"
php_version: "8.3"
tasks:
- name: Install Apache and PHP
apt:
name:
- apache2
- libapache2-mod-php{{ php_version }}
- php{{ php_version }}
- php{{ php_version }}-mysql
- php{{ php_version }}-curl
- php{{ php_version }}-xml
- php{{ php_version }}-mbstring
state: present
update_cache: true
- name: Install MySQL server
apt:
name: mysql-server
state: present
- name: Start and enable MySQL
service:
name: mysql
state: started
enabled: true
- name: Start and enable Apache
service:
name: apache2
state: started
enabled: true
- name: Deploy PHP info page for testing
copy:
content: "<?php phpinfo(); ?>"
dest: /var/www/html/info.php
owner: www-data
group: www-data
mode: "0644"
User management across servers
# users.yml
---
- name: Manage user accounts
hosts: all
become: true
vars:
admin_users:
- name: jcarlos
key: "ssh-ed25519 AAAAC3Nza... jcarlos@workstation"
groups: sudo,adm
- name: deployer
key: "ssh-ed25519 AAAAC3Nza... deployer@ci"
groups: www-data
removed_users:
- oldadmin
- tempuser
tasks:
- name: Create admin users
user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
shell: /bin/bash
create_home: true
state: present
loop: "{{ admin_users }}"
- name: Add SSH keys for admin users
authorized_key:
user: "{{ item.name }}"
key: "{{ item.key }}"
exclusive: true
loop: "{{ admin_users }}"
- name: Remove old users
user:
name: "{{ item }}"
state: absent
remove: true
loop: "{{ removed_users }}"
Security hardening
# harden.yml
---
- name: Security hardening
hosts: all
become: true
tasks:
- name: Install security packages
apt:
name:
- ufw
- fail2ban
- unattended-upgrades
state: present
update_cache: true
- name: Configure UFW defaults
ufw:
direction: incoming
policy: deny
- name: Allow SSH through UFW
ufw:
rule: allow
port: "2222"
proto: tcp
- name: Allow HTTP and HTTPS
ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop:
- "80"
- "443"
- name: Enable UFW
ufw:
state: enabled
- name: Deploy fail2ban configuration
copy:
src: files/jail.local
dest: /etc/fail2ban/jail.local
notify: Restart fail2ban
- name: Enable automatic security updates
copy:
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
dest: /etc/apt/apt.conf.d/20auto-upgrades
- name: Disable root SSH login
lineinfile:
path: /etc/ssh/sshd_config
regexp: "^PermitRootLogin"
line: "PermitRootLogin no"
notify: Restart sshd
handlers:
- name: Restart fail2ban
service:
name: fail2ban
state: restarted
- name: Restart sshd
service:
name: sshd
state: restarted
Ansible Vault for Secrets
Never store passwords or API keys in plain text. Ansible Vault encrypts sensitive data:
# Create an encrypted variables file
ansible-vault create group_vars/all/vault.yml
# Edit an encrypted file
ansible-vault edit group_vars/all/vault.yml
# Encrypt an existing file
ansible-vault encrypt secrets.yml
# Run a playbook with vault
ansible-playbook site.yml --ask-vault-pass
# Use a password file instead
ansible-playbook site.yml --vault-password-file ~/.vault_pass
Inside the vault file, prefix sensitive variables with vault_:
# group_vars/all/vault.yml (encrypted)
vault_mysql_root_password: "s3cur3P@ssw0rd"
vault_api_key: "abc123def456"
vault_ssl_private_key: |
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
Reference vault variables in your regular variables file:
# group_vars/all/vars.yml
mysql_root_password: "{{ vault_mysql_root_password }}"
api_key: "{{ vault_api_key }}"
Troubleshooting
SSH connection failures
# Test SSH connectivity manually
ssh -i ~/.ssh/id_ed25519 deployer@web1.example.com
# Run with verbose mode to see SSH details
ansible all -m ping -i inventory.ini -vvvv
# Check if Python is available on the remote host
ansible all -m raw -a "python3 --version" -i inventory.ini
Permission denied errors
# Ensure become (sudo) is configured
ansible-playbook site.yml -i inventory.ini --become --ask-become-pass
# Check sudo configuration on the remote host
ssh deployer@web1.example.com "sudo -l"
Module not found errors
# List all available modules
ansible-doc -l | grep apt
# View documentation for a specific module
ansible-doc apt
# Check Ansible version for module compatibility
ansible --version
Playbook syntax errors
# Validate playbook syntax without executing
ansible-playbook site.yml --syntax-check
# Lint your playbook for best practices
pip install ansible-lint
ansible-lint site.yml
Debugging task output
- name: Run command and capture output
command: systemctl status nginx
register: nginx_status
ignore_errors: true
- name: Display the output
debug:
var: nginx_status.stdout_lines
Essential Commands Reference
| Command | Description |
|---|---|
ansible --version | Display Ansible version and configuration |
ansible all -m ping -i inventory.ini | Test connectivity to all hosts |
ansible all -m setup -i inventory.ini | Gather and display host facts |
ansible webservers -m apt -a "name=nginx state=present" -i inventory.ini --become | Install nginx on webservers via ad-hoc command |
ansible-playbook site.yml -i inventory.ini | Execute a playbook |
ansible-playbook site.yml --check --diff | Dry run with diff output |
ansible-playbook site.yml --limit web1.example.com | Run playbook on a single host |
ansible-playbook site.yml --tags "nginx,security" | Run only tasks with specific tags |
ansible-playbook site.yml --start-at-task "Install nginx" | Resume playbook from a specific task |
ansible-vault create secrets.yml | Create a new encrypted file |
ansible-vault edit secrets.yml | Edit an encrypted file |
ansible-galaxy init roles/myrole | Create a new role directory structure |
ansible-galaxy install geerlingguy.nginx | Install a role from Ansible Galaxy |
ansible-inventory -i inventory.ini --graph | Display inventory host graph |
ansible-doc apt | View module documentation |
Summary
Ansible provides a powerful yet accessible path to infrastructure automation. Its agentless architecture means you can start automating immediately — if you can SSH into your servers, you can manage them with Ansible. Begin with simple ad-hoc commands, progress to playbooks for repeatable tasks, and organize complex configurations into roles. The combination of YAML readability, idempotent execution, and thousands of built-in modules makes Ansible the go-to tool for configuration management across Linux and Windows environments.
For provisioning the infrastructure itself — creating cloud servers, networks, and DNS records before configuring them — explore our Terraform infrastructure as code guide. To ensure the servers Ansible manages are secure from the start, follow our SSH hardening guide to lock down remote access with key-based authentication, fail2ban, and firewall rules.