Ansible is an open source automation tool maintained by Red Hat. It lets you configure systems, deploy software and orchestrate complex workflows. Its agentless architecture sets it apart from Puppet or Chef: Ansible connects to target machines over SSH and runs the tasks described in YAML. No installation is required on the managed nodes, which considerably simplifies setup and maintenance.
Prerequisites
Before getting started with Ansible, make sure you have the following:
- Control machine: A Linux or macOS workstation with Python 3.8+ installed. Windows is not supported as a controller but works as a target node via WinRM.
- SSH access: SSH connectivity configured to the target machines, ideally with SSH keys (no password).
- Privileges: A user with sudo access on the target machines for administrative tasks.
- Python on the targets: Python 3 installed on the managed machines (present by default on most Linux distributions).
# Check Python on the control machine
python3 --version
# Generate an SSH key if needed
ssh-keygen -t ed25519 -C "ansible-controller"
# Copy the key to the target machines
ssh-copy-id user@target-server
Multi-OS installation
Ansible is installed only on the control machine. Here are the installation methods depending on your system.
Ubuntu / Debian
# Add the official Ansible PPA
sudo apt update
sudo apt install -y software-properties-common
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install -y ansible
# Verify the installation
ansible --version
CentOS / RHEL
# Enable the EPEL repository
sudo dnf install -y epel-release
sudo dnf install -y ansible
# On RHEL 8+ with a subscription
sudo subscription-manager repos --enable ansible-automation-platform
sudo dnf install -y ansible-core
Installation via pip (all platforms)
# Installation in a virtualenv (recommended)
python3 -m venv ~/ansible-venv
source ~/ansible-venv/bin/activate
pip install ansible
# Or global installation
pip3 install ansible
# Install a specific version
pip3 install ansible-core==2.16.0
Basic configuration
Ansible configuration relies on two main files: the configuration file ansible.cfg and the host inventory.
ansible.cfg
Ansible looks for its configuration in this order: the ANSIBLE_CONFIG environment variable, ./ansible.cfg (current directory), ~/.ansible.cfg, then /etc/ansible/ansible.cfg. Place your file at the root of your project.
# ansible.cfg
[defaults]
inventory = ./inventory/hosts.ini
remote_user = deploy
host_key_checking = False
retry_files_enabled = False
forks = 10
timeout = 30
log_path = ./ansible.log
[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
Static inventory
The inventory defines the machines managed by Ansible. The INI format is the most common:
# inventory/hosts.ini
[webservers]
web1.example.com ansible_host=192.168.1.10
web2.example.com ansible_host=192.168.1.11
[databases]
db1.example.com ansible_host=192.168.1.20
db2.example.com ansible_host=192.168.1.21
[monitoring]
grafana.example.com ansible_host=192.168.1.30
# Group of groups
[production:children]
webservers
databases
# Variables for the whole group
[webservers:vars]
http_port=80
ansible_python_interpreter=/usr/bin/python3
YAML inventory
# inventory/hosts.yml
all:
children:
webservers:
hosts:
web1.example.com:
ansible_host: 192.168.1.10
web2.example.com:
ansible_host: 192.168.1.11
vars:
http_port: 80
databases:
hosts:
db1.example.com:
ansible_host: 192.168.1.20
Advanced inventory
For complex environments, Ansible offers advanced inventory features.
Group and host variables
Organise variables in separate files following this convention:
# Inventory variable structure
inventory/
hosts.ini
group_vars/
all.yml # Global variables
webservers.yml # webservers group variables
databases.yml # databases group variables
host_vars/
web1.example.com.yml # Variables specific to web1
# inventory/group_vars/webservers.yml
---
nginx_worker_processes: auto
nginx_worker_connections: 1024
ssl_certificate_path: /etc/ssl/certs
deploy_user: www-data
Dynamic inventory
For cloud environments, use a dynamic inventory that queries the provider API:
# inventory/aws_ec2.yml
---
plugin: amazon.aws.aws_ec2
regions:
- eu-west-1
- eu-west-3
filters:
tag:Environment: production
keyed_groups:
- key: tags.Role
prefix: role
- key: placement.availability_zone
prefix: az
compose:
ansible_host: private_ip_address
# Test a dynamic inventory
ansible-inventory -i inventory/aws_ec2.yml --list
ansible-inventory -i inventory/aws_ec2.yml --graph
Essential modules
Ansible provides thousands of modules. Here are the most commonly used on a daily basis.
Package management: apt and yum
- name: Install packages (Debian/Ubuntu)
ansible.builtin.apt:
name:
- nginx
- curl
- htop
state: present
update_cache: yes
cache_valid_time: 3600
- name: Install packages (CentOS/RHEL)
ansible.builtin.yum:
name:
- httpd
- vim
state: latest
Service management: service
- name: Start and enable nginx
ansible.builtin.service:
name: nginx
state: started
enabled: yes
- name: Restart a service
ansible.builtin.service:
name: nginx
state: restarted
File management: copy, template, file
- name: Copy a configuration file
ansible.builtin.copy:
src: files/nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
backup: yes
- name: Deploy a Jinja2 template
ansible.builtin.template:
src: templates/vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ domain }}.conf"
owner: root
mode: "0644"
- name: Create a directory
ansible.builtin.file:
path: /var/www/app
state: directory
owner: www-data
group: www-data
mode: "0755"
recurse: yes
Running commands: shell and command
- name: Run a simple command
ansible.builtin.command:
cmd: whoami
register: current_user
- name: Shell command with a pipe
ansible.builtin.shell:
cmd: "df -h | grep /dev/sda1"
register: disk_usage
changed_when: false
- name: Display the result
ansible.builtin.debug:
msg: "Disk space: {{ disk_usage.stdout }}"
User management: user
- name: Create an application user
ansible.builtin.user:
name: deploy
shell: /bin/bash
groups: sudo,docker
append: yes
create_home: yes
generate_ssh_key: yes
ssh_key_bits: 4096
Playbooks
Playbooks are the heart of Ansible. They describe the desired state of your systems in YAML files.
Basic YAML syntax
---
# playbook-webserver.yml
- name: Configure the web servers
hosts: webservers
become: yes
vars:
app_name: myapp
app_port: 8080
tasks:
- name: Update the apt cache
ansible.builtin.apt:
update_cache: yes
cache_valid_time: 3600
- name: Install the dependencies
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop:
- nginx
- python3-pip
- supervisor
- name: Deploy the nginx configuration
ansible.builtin.template:
src: templates/nginx-app.conf.j2
dest: "/etc/nginx/sites-available/{{ app_name }}.conf"
notify: Reload nginx
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
Handlers
Handlers are tasks triggered only when another task notifies them via notify. They run only once at the end of the play, even if they are notified several times:
handlers:
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: yes
Conditions
- name: Install on Debian only
ansible.builtin.apt:
name: nginx
state: present
when: ansible_os_family == "Debian"
- name: Configure the firewall if active
ansible.builtin.ufw:
rule: allow
port: "{{ http_port }}"
when:
- firewall_enabled | default(false)
- http_port is defined
Loops
- name: Create several users
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
shell: /bin/bash
loop:
- { name: "alice", groups: "sudo" }
- { name: "bob", groups: "docker" }
- { name: "charlie", groups: "sudo,docker" }
- name: Copy several files
ansible.builtin.copy:
src: "files/{{ item }}"
dest: "/etc/app/{{ item }}"
loop:
- app.conf
- db.conf
- logging.conf
Tags
Tags let you selectively run certain tasks of a playbook:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
tags:
- install
- nginx
- name: Configure nginx
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
tags:
- configure
- nginx
# Run only the tasks tagged "configure"
ansible-playbook site.yml --tags configure
# Exclude the tasks tagged "install"
ansible-playbook site.yml --skip-tags install
# List the available tags
ansible-playbook site.yml --list-tags
Variables and templates
Variable types
Ansible offers several levels of variables with a precise order of precedence. The most common ones:
# Variables in the playbook
vars:
app_name: myapp
app_port: 8080
db_config:
host: localhost
port: 5432
name: mydb
# Variables from an external file
vars_files:
- vars/common.yml
- "vars/{{ env }}.yml"
Ansible facts
Ansible automatically collects information (facts) about the target machines. Use them in your playbooks:
- name: Display the system facts
ansible.builtin.debug:
msg: |
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
IP: {{ ansible_default_ipv4.address }}
RAM: {{ ansible_memtotal_mb }} MB
CPU: {{ ansible_processor_cores }} cores
# View all the facts of a host
ansible web1.example.com -m setup
# Filter the facts
ansible web1.example.com -m setup -a "filter=ansible_distribution*"
Jinja2 templates
Jinja2 templates let you generate dynamic configuration files:
# templates/nginx-vhost.conf.j2
server {
listen {{ http_port | default(80) }};
server_name {{ domain }};
root /var/www/{{ app_name }};
{% if ssl_enabled | default(false) %}
listen 443 ssl;
ssl_certificate {{ ssl_cert_path }};
ssl_certificate_key {{ ssl_key_path }};
{% endif %}
{% for location in app_locations | default([]) %}
location {{ location.path }} {
proxy_pass http://127.0.0.1:{{ location.port }};
proxy_set_header Host $host;
}
{% endfor %}
access_log /var/log/nginx/{{ domain }}_access.log;
error_log /var/log/nginx/{{ domain }}_error.log;
}
Roles
Roles structure your Ansible code into reusable, modular components.
Structure of a role
# Standard structure of a role
roles/
nginx/
tasks/
main.yml # Main tasks
handlers/
main.yml # Role handlers
templates/
nginx.conf.j2 # Jinja2 templates
files/
index.html # Static files
vars/
main.yml # Role variables (high priority)
defaults/
main.yml # Default values (low priority)
meta/
main.yml # Metadata and dependencies
Create a role
# Generate the structure with ansible-galaxy
ansible-galaxy init roles/nginx
# Or create it manually
mkdir -p roles/nginx/{tasks,handlers,templates,files,vars,defaults,meta}
# roles/nginx/tasks/main.yml
---
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Deploy the configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart nginx
- name: Enable the service
ansible.builtin.service:
name: nginx
state: started
enabled: yes
# roles/nginx/defaults/main.yml
---
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
# Use a role in a playbook
---
- hosts: webservers
become: yes
roles:
- role: nginx
vars:
nginx_worker_connections: 2048
- role: app
- role: monitoring
Ansible Galaxy
Galaxy is the community repository of Ansible roles. Use it to find and share roles:
# Search for a role
ansible-galaxy search nginx --platforms Ubuntu
# Install a role from Galaxy
ansible-galaxy install geerlingguy.nginx
# Install roles from a requirements file
ansible-galaxy install -r requirements.yml
# requirements.yml
---
roles:
- name: geerlingguy.nginx
version: "3.1.0"
- name: geerlingguy.postgresql
version: "3.4.0"
collections:
- name: community.general
version: ">=5.0.0"
Ansible Vault
Vault lets you encrypt sensitive data (passwords, API keys, certificates) directly in your Ansible files.
# Create an encrypted file
ansible-vault create vars/secrets.yml
# Encrypt an existing file
ansible-vault encrypt vars/production.yml
# Edit an encrypted file
ansible-vault edit vars/secrets.yml
# Decrypt a file
ansible-vault decrypt vars/secrets.yml
# Encrypt an individual variable
ansible-vault encrypt_string "MyPassword123" --name "db_password"
# vars/secrets.yml (contents before encryption)
---
db_password: "SuperSecret123"
api_key: "ak_live_xxxxxxxxxxxx"
ssl_private_key: |
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBg...
-----END PRIVATE KEY-----
# Run a playbook with Vault
ansible-playbook site.yml --ask-vault-pass
# Use a password file (recommended in CI/CD)
ansible-playbook site.yml --vault-password-file ~/.vault_pass
# Several Vault passwords with identifiers
ansible-vault encrypt --vault-id prod@prompt vars/prod-secrets.yml
ansible-playbook site.yml --vault-id prod@~/.vault_pass_prod
Best practices
Project structure
Adopt a standardised structure for your Ansible projects:
# Recommended structure
ansible-project/
ansible.cfg
site.yml # Main playbook
webservers.yml # Playbook per group
databases.yml
inventory/
production/
hosts.ini
group_vars/
all.yml
webservers.yml
host_vars/
web1.yml
staging/
hosts.ini
group_vars/
roles/
common/
nginx/
postgresql/
playbooks/ # Operational playbooks
deploy.yml
backup.yml
rollback.yml
Idempotence
Each task must be idempotent: running it several times produces the same result as a single execution. Avoid the shell and command modules when a dedicated module exists.
# Bad: not idempotent
- name: Create the directory
ansible.builtin.shell: mkdir -p /var/www/app
# Good: idempotent
- name: Create the directory
ansible.builtin.file:
path: /var/www/app
state: directory
mode: "0755"
Naming conventions
- Descriptive task names:
Install the Python dependenciesrather thanstep 1 - Variables in snake_case:
app_port,db_host - Roles named by function:
nginx,postgresql,monitoring - Use FQCNs (Fully Qualified Collection Names):
ansible.builtin.aptinstead ofapt
Troubleshooting
The debug module
- name: Display a variable
ansible.builtin.debug:
var: my_variable
- name: Display a formatted message
ansible.builtin.debug:
msg: "The user {{ user }} was created on {{ inventory_hostname }}"
- name: Capture and display the output of a command
ansible.builtin.command: systemctl status nginx
register: nginx_status
changed_when: false
- name: Display the result
ansible.builtin.debug:
var: nginx_status.stdout_lines
Verbose mode
# Verbosity levels
ansible-playbook site.yml -v # Detailed output
ansible-playbook site.yml -vv # More details
ansible-playbook site.yml -vvv # SSH connection debug
ansible-playbook site.yml -vvvv # Full debug with scripts
# Check mode (dry-run) with diff
ansible-playbook site.yml --check --diff
# Limit execution to one host
ansible-playbook site.yml --limit web1.example.com
# Start at a specific task
ansible-playbook site.yml --start-at-task "Deploy the configuration"
# Run step by step
ansible-playbook site.yml --step
Common issues
- Permission denied: Check that
become: yesis set and that the user has passwordless sudo rights, or use--ask-become-pass. - Host unreachable: Test connectivity with
ansible all -m ping. Check the SSH configuration and the keys. - Module not found: Install the required collection with
ansible-galaxy collection install community.general. - Variable undefined: Use the
default()filter to set default values:{{ var | default("fallback") }}. - Slow execution: Enable pipelining in
ansible.cfg, increase the forks, and disable fact gathering when not needed withgather_facts: no.
Conclusion
Ansible is a powerful tool that simplifies infrastructure automation thanks to its agentless approach and its accessible YAML syntax. By mastering the concepts presented in this tutorial (inventories, modules, playbooks, roles and Vault), you have a solid foundation to automate the management of your servers, standardise your deployments and guarantee the consistency of your infrastructure. Start with simple playbooks, then gradually structure your code into reusable roles. The initial investment quickly pays off through the time saved and the reduction in human error.
- Prefer dedicated modules over shell/command to guarantee idempotence
- Structure your code into roles as soon as you have more than 2-3 playbooks
- Always encrypt secrets with Ansible Vault
- Always test in --check --diff mode before applying in production
- Version your entire Ansible code with Git
Comments