Automation
Difficulty: Intermediate
12 min read

Ansible: Automation and Configuration Management

Learn how to use Ansible, the open source automation tool, to manage your servers and deploy applications simply and efficiently.

Back to tutorials
What is Ansible?
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 dependencies rather than step 1
  • Variables in snake_case: app_port, db_host
  • Roles named by function: nginx, postgresql, monitoring
  • Use FQCNs (Fully Qualified Collection Names): ansible.builtin.apt instead of apt

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: yes is 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 with gather_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.

Key takeaways
- 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

Written by

Morgann Riu

Cybersecurity and Linux administration expert. I share my knowledge through free tutorials and training to help system administrators and developers secure their infrastructures.

Share this tutorial

Did you enjoy this article?

Comments

Checklist Sécurité Linux

30 points essentiels pour sécuriser un serveur Linux. Recevez aussi les nouveaux tutoriels par email.

Pas de spam. Désabonnement en 1 clic.