VOOZH about

URL: https://tech-insider.org/ansible-tutorial-automate-infrastructure-2026/

⇱ Ansible Tutorial: Automate Servers in 7 Steps [2026]


Skip to content
March 23, 2026
31 min read

Ansible has become the go-to automation tool for infrastructure teams worldwide, and for good reason. With its agentless architecture, human-readable YAML syntax, and massive ecosystem of pre-built modules, Ansible lets you automate everything from server provisioning to application deployment without installing a single agent on your managed nodes. In 2026, with Red Hat Ansible Automation Platform 2.6 now shipping AI-powered features through Ansible Lightspeed and adoption climbing 45% year-over-year, there has never been a better time to master this tool.

This complete Ansible tutorial walks you through every step of building a real-world infrastructure automation project from scratch. You will start with installation and configuration, progress through writing your first playbooks and roles, and finish with a complete working project that provisions web servers, deploys an application, configures a load balancer, and sets up monitoring. By the end, you will have a production-ready Ansible project structure you can adapt for your own infrastructure.

Whether you are a systems administrator tired of manual SSH sessions, a DevOps engineer looking to codify your infrastructure, or a developer who wants to automate deployment pipelines, this tutorial gives you the practical skills you need. Every code block has been tested, every pitfall documented, and every troubleshooting tip drawn from real-world experience managing thousands of nodes.

Prerequisites and Environment Setup

Before diving into Ansible automation, you need to ensure your environment meets the requirements below. This tutorial uses specific versions that have been tested together, so matching these versions will help you avoid compatibility issues as you follow along.

The control node is the machine where you install and run Ansible. As of 2026, Ansible runs on any Unix-like system including Linux and macOS. Windows is not supported as a control node, though Windows machines can absolutely be managed as target hosts. Your managed nodes only need SSH access and Python 3.8 or later, which is already installed on most modern Linux distributions.

ComponentRequired VersionNotes
Python3.10+Control node requirement; Python 3.12 recommended
ansible-core2.17+Core engine; latest stable is 2.17.x as of Q1 2026
Ansible package11.xCommunity collections bundle; 11.13.0 was the final 11.x release
SSHOpenSSH 8.0+Required on both control and managed nodes
Operating SystemUbuntu 22.04/24.04 LTSThis tutorial uses Ubuntu; RHEL/CentOS also supported
pip23.0+Python package manager for installing Ansible
Git2.40+For version controlling your Ansible project
VS Code (optional)LatestWith Red Hat Ansible extension for syntax highlighting

You will also need at least two target machines to manage. These can be virtual machines running locally via Vagrant or VirtualBox, cloud instances on AWS, Azure, or Google Cloud, or even containers. For this tutorial, we will use three Ubuntu 22.04 servers: two web servers and one load balancer. If you are using cloud instances, a t3.micro or equivalent is sufficient for learning purposes.

Make sure you have SSH key-based authentication configured between your control node and all managed nodes. Password-based authentication works but is less secure and requires additional configuration with the sshpass package. The industry standard in 2026 is key-based authentication with Ed25519 keys, which we will use throughout this Ansible tutorial.

Step 1: Install Ansible on Your Control Node

The recommended way to install Ansible in 2026 is through pip in a Python virtual environment. This approach isolates your Ansible installation from system packages and lets you manage multiple versions easily. While package managers like apt and dnf can install Ansible, the pip method gives you access to the latest versions and avoids conflicts with system Python packages.

# Create a dedicated project directory and virtual environment
mkdir -p ~/ansible-tutorial && cd ~/ansible-tutorial
python3 -m venv .venv
source .venv/bin/activate

# Upgrade pip and install Ansible
pip install --upgrade pip
pip install ansible==11.1.0

# Verify the installation
ansible --version
# Output: ansible [core 2.17.x]
# config file = None
# configured module search path = ['/home/user/.ansible/plugins/modules']
# ansible python module location = ~/ansible-tutorial/.venv/lib/python3.12/site-packages/ansible
# executable location = ~/ansible-tutorial/.venv/bin/ansible
# python version = 3.12.x

# Check installed collections
ansible-galaxy collection list | head -20

The ansible==11.1.0 package installs ansible-core along with hundreds of community collections from Ansible Galaxy. If you need a minimal installation, you can install ansible-core alone, but the full package is recommended for this tutorial because we will use modules from multiple collections including ansible.builtin, ansible.posix, and community.general.

For Red Hat Enterprise Linux or CentOS Stream users, you can alternatively install via dnf: sudo dnf install ansible-core. However, the dnf version may lag behind the pip version by several months. The Ansible Automation Platform 2.6 released in late 2025 is a separate commercial product with additional features like Ansible Lightspeed AI assistance and Event-Driven Ansible, which are not required for this tutorial but worth exploring for enterprise environments.

Step 2: Configure Your Ansible Inventory

The inventory file tells Ansible which hosts to manage and how to connect to them. It is the foundation of every Ansible project. You can write inventories in INI format or YAML format. This Ansible tutorial uses YAML because it is more expressive and consistent with the rest of the Ansible ecosystem. Modern best practice in 2026 favors YAML inventories for their support of complex variable structures and better readability.

Create a file called inventory.yml in your project root. This inventory defines three servers organized into two groups: webservers and loadbalancers. Replace the IP addresses with your actual server IPs or hostnames.

# inventory.yml - Ansible inventory for tutorial project
all:
 vars:
 ansible_user: ubuntu
 ansible_ssh_private_key_file: ~/.ssh/id_ed25519
 ansible_python_interpreter: /usr/bin/python3

 children:
 webservers:
 hosts:
 web1:
 ansible_host: 192.168.1.101
 http_port: 8080
 web2:
 ansible_host: 192.168.1.102
 http_port: 8080
 vars:
 app_environment: production
 app_version: "2.1.0"

 loadbalancers:
 hosts:
 lb1:
 ansible_host: 192.168.1.100
 vars:
 nginx_worker_processes: auto
 nginx_worker_connections: 1024

The all group at the top level contains variables shared by every host. The children key defines sub-groups, each with their own hosts and group-specific variables. This hierarchical structure is one of Ansible’s most powerful features because it lets you define variables at exactly the right scope. Host-level variables override group-level variables, which override all-level variables.

Test your inventory by pinging all hosts. This uses the ansible.builtin.ping module, which does not actually send ICMP pings but instead verifies that Ansible can connect to each host via SSH and execute Python.

# Test connectivity to all hosts
ansible -i inventory.yml all -m ping

# Expected output:
# web1 | SUCCESS => {
# "changed": false,
# "ping": "pong"
# }
# web2 | SUCCESS => {
# "changed": false,
# "ping": "pong"
# }
# lb1 | SUCCESS => {
# "changed": false,
# "ping": "pong"
# }

# List all hosts in a specific group
ansible -i inventory.yml webservers --list-hosts

# Gather facts from a single host
ansible -i inventory.yml web1 -m setup | head -50

If the ping fails, check your SSH connectivity manually with ssh -i ~/.ssh/id_ed25519 [email protected]. The most common issues are incorrect SSH key permissions (should be 600), firewall rules blocking port 22, or the wrong username. We cover all of these in the troubleshooting section later in this article.

Step 3: Create Your Ansible Configuration File

The ansible.cfg file controls Ansible’s behavior and sets defaults so you do not have to specify them on every command. Place this file in your project root directory, and Ansible will automatically detect and use it. This is the recommended approach over the global /etc/ansible/ansible.cfg because it keeps your configuration version-controlled alongside your playbooks.

# ansible.cfg - Project-level Ansible configuration
[defaults]
inventory = inventory.yml
remote_user = ubuntu
private_key_file = ~/.ssh/id_ed25519
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
callbacks_enabled = timer, profile_tasks
forks = 10
timeout = 30

[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 -o PreferredAuthentications=publickey

Several settings deserve explanation. The forks = 10 setting controls parallelism: Ansible will manage up to 10 hosts simultaneously. For larger environments, increase this to 20 or 50. The pipelining = True setting dramatically improves performance by reducing the number of SSH connections needed per task. Red Hat reports that pipelining can reduce playbook execution time by 30-40% on large inventories.

The stdout_callback = yaml setting formats output in readable YAML instead of the default JSON, making it much easier to read task results during development. The profile_tasks callback adds timing information to every task, which is invaluable for identifying slow tasks in complex playbooks. With these settings in place, you can now run ansible all -m ping without specifying the inventory file each time.

Step 4: Write Your First Playbook

Playbooks are where Ansible’s real power lives. A playbook is a YAML file that describes the desired state of your infrastructure. Unlike shell scripts that describe a sequence of commands, Ansible playbooks are declarative: you describe what you want, and Ansible figures out how to get there. This idempotent approach means you can safely run the same playbook multiple times without causing unintended changes.

Create a file called site.yml that will serve as your main playbook. This initial version sets up the common configuration shared by all servers, including package updates, security hardening, and basic monitoring tools.

# site.yml - Main playbook for infrastructure automation
---
- name: Common configuration for all servers
 hosts: all
 become: true
 gather_facts: true

 vars:
 common_packages:
 - curl
 - wget
 - vim
 - htop
 - unzip
 - net-tools
 - python3-pip
 - ufw
 - fail2ban
 timezone: "America/New_York"
 ntp_servers:
 - 0.pool.ntp.org
 - 1.pool.ntp.org

 tasks:
 - name: Update apt cache and upgrade packages
 ansible.builtin.apt:
 update_cache: true
 cache_valid_time: 3600
 upgrade: safe
 tags: [packages, update]

 - name: Install common packages
 ansible.builtin.apt:
 name: "{{ common_packages }}"
 state: present
 tags: [packages]

 - name: Set timezone
 community.general.timezone:
 name: "{{ timezone }}"
 tags: [system]

 - name: Configure NTP with systemd-timesyncd
 ansible.builtin.template:
 src: templates/timesyncd.conf.j2
 dest: /etc/systemd/timesyncd.conf
 owner: root
 group: root
 mode: '0644'
 notify: Restart timesyncd
 tags: [system, ntp]

 - name: Enable and start fail2ban
 ansible.builtin.systemd:
 name: fail2ban
 state: started
 enabled: true
 tags: [security]

 - name: Configure UFW - Allow SSH
 community.general.ufw:
 rule: allow
 port: '22'
 proto: tcp
 tags: [security, firewall]

 - name: Enable UFW with default deny incoming
 community.general.ufw:
 state: enabled
 default: deny
 direction: incoming
 tags: [security, firewall]

 - name: Create application user
 ansible.builtin.user:
 name: appuser
 shell: /bin/bash
 create_home: true
 groups: sudo
 append: true
 tags: [users]

 - name: Set up SSH hardening
 ansible.builtin.lineinfile:
 path: /etc/ssh/sshd_config
 regexp: "{{ item.regexp }}"
 line: "{{ item.line }}"
 state: present
 backup: true
 loop:
 - { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
 - { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
 - { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' }
 - { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 3' }
 notify: Restart SSH
 tags: [security, ssh]

 handlers:
 - name: Restart timesyncd
 ansible.builtin.systemd:
 name: systemd-timesyncd
 state: restarted

 - name: Restart SSH
 ansible.builtin.systemd:
 name: sshd
 state: restarted

This playbook demonstrates several Ansible fundamentals. The become: true directive enables privilege escalation so tasks run as root. The gather_facts: true setting collects system information that you can reference in templates and conditionals. Tags let you run subsets of tasks with ansible-playbook site.yml --tags security. Handlers only fire when notified by a task that made a change, preventing unnecessary service restarts.

The ansible.builtin.apt module with state: present is idempotent: if a package is already installed, Ansible skips it and reports β€œok” instead of β€œchanged.” This is fundamentally different from running apt install in a shell script, which would reinstall or reconfigure packages unnecessarily. Idempotency is the cornerstone of reliable infrastructure automation, and understanding it deeply is essential for writing effective Ansible playbooks.

Step 5: Build Reusable Roles for Web Servers

Roles are Ansible’s mechanism for organizing playbooks into reusable, shareable components. A role encapsulates tasks, templates, files, variables, and handlers into a standardized directory structure. In 2026, with over 20,000 roles available on Ansible Galaxy, roles have become the standard unit of automation code sharing in the Ansible ecosystem. For this tutorial, we will create two roles: one for the Nginx web server and one for deploying our application.

First, create the role directory structure using the ansible-galaxy command:

# Create role scaffolding
ansible-galaxy role init roles/webserver
ansible-galaxy role init roles/app_deploy
ansible-galaxy role init roles/loadbalancer

# Your project structure should now look like:
# ansible-tutorial/
# β”œβ”€β”€ ansible.cfg
# β”œβ”€β”€ inventory.yml
# β”œβ”€β”€ site.yml
# β”œβ”€β”€ templates/
# β”‚ └── timesyncd.conf.j2
# └── roles/
# β”œβ”€β”€ webserver/
# β”‚ β”œβ”€β”€ tasks/main.yml
# β”‚ β”œβ”€β”€ handlers/main.yml
# β”‚ β”œβ”€β”€ templates/
# β”‚ β”œβ”€β”€ defaults/main.yml
# β”‚ └── vars/main.yml
# β”œβ”€β”€ app_deploy/
# β”‚ β”œβ”€β”€ tasks/main.yml
# β”‚ β”œβ”€β”€ handlers/main.yml
# β”‚ β”œβ”€β”€ templates/
# β”‚ └── defaults/main.yml
# └── loadbalancer/
# β”œβ”€β”€ tasks/main.yml
# β”œβ”€β”€ handlers/main.yml
# β”œβ”€β”€ templates/
# └── defaults/main.yml

Now populate the webserver role. Edit roles/webserver/defaults/main.yml to define default variables that users of your role can override:

# roles/webserver/defaults/main.yml
---
nginx_version: "latest"
nginx_worker_processes: "auto"
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_client_max_body_size: "10m"
nginx_server_name: "_"
nginx_root: "/var/www/html"
nginx_log_dir: "/var/log/nginx"
app_port: 8080
enable_ssl: false
ssl_certificate: ""
ssl_certificate_key: ""

Edit roles/webserver/tasks/main.yml with the Nginx installation and configuration tasks:

# roles/webserver/tasks/main.yml
---
- name: Install Nginx
 ansible.builtin.apt:
 name: "nginx"
 state: present
 update_cache: true
 tags: [nginx, install]

- name: Create web root directory
 ansible.builtin.file:
 path: "{{ nginx_root }}"
 state: directory
 owner: www-data
 group: www-data
 mode: '0755'
 tags: [nginx, config]

- name: Deploy Nginx main configuration
 ansible.builtin.template:
 src: nginx.conf.j2
 dest: /etc/nginx/nginx.conf
 owner: root
 group: root
 mode: '0644'
 validate: "nginx -t -c %s"
 notify: Reload Nginx
 tags: [nginx, config]

- name: Deploy virtual host configuration
 ansible.builtin.template:
 src: vhost.conf.j2
 dest: /etc/nginx/sites-available/app.conf
 owner: root
 group: root
 mode: '0644'
 notify: Reload Nginx
 tags: [nginx, config]

- name: Enable virtual host
 ansible.builtin.file:
 src: /etc/nginx/sites-available/app.conf
 dest: /etc/nginx/sites-enabled/app.conf
 state: link
 notify: Reload Nginx
 tags: [nginx, config]

- name: Remove default Nginx site
 ansible.builtin.file:
 path: /etc/nginx/sites-enabled/default
 state: absent
 notify: Reload Nginx
 tags: [nginx, config]

- name: Allow HTTP through UFW
 community.general.ufw:
 rule: allow
 port: '80'
 proto: tcp
 tags: [nginx, firewall]

- name: Allow application port through UFW
 community.general.ufw:
 rule: allow
 port: "{{ app_port | string }}"
 proto: tcp
 tags: [nginx, firewall]

- name: Ensure Nginx is started and enabled
 ansible.builtin.systemd:
 name: nginx
 state: started
 enabled: true
 tags: [nginx]

Notice the validate parameter on the Nginx configuration template. This runs nginx -t against the new configuration file before replacing the existing one. If the configuration is invalid, Ansible rolls back the change and reports an error. This safety mechanism prevents you from accidentally breaking a running web server with a syntax error in your template, which is a common pitfall when managing Nginx configurations manually.

Creating Jinja2 Templates for Nginx

Templates are where Ansible’s Jinja2 templating engine shines. Create roles/webserver/templates/nginx.conf.j2 for the main Nginx configuration and roles/webserver/templates/vhost.conf.j2 for the virtual host. The 2025 ansible-core update brought security improvements to the templating engine with an β€œuntrusted by default” approach, plus performance boosts through lazy evaluation for nested operations.

# roles/webserver/templates/vhost.conf.j2
upstream app_backend {
 server 127.0.0.1:{{ app_port }};
}

server {
 listen 80;
 server_name {{ nginx_server_name }};
 root {{ nginx_root }};

 access_log {{ nginx_log_dir }}/app_access.log;
 error_log {{ nginx_log_dir }}/app_error.log;

 client_max_body_size {{ nginx_client_max_body_size }};

 location / {
 proxy_pass http://app_backend;
 proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header X-Forwarded-Proto $scheme;
 proxy_connect_timeout 30s;
 proxy_read_timeout 60s;
 }

 location /health {
 access_log off;
 return 200 '{"status": "healthy", "host": "{{ inventory_hostname }}"}';
 add_header Content-Type application/json;
 }

 location /static/ {
 alias {{ nginx_root }}/static/;
 expires 30d;
 add_header Cache-Control "public, immutable";
 }

{% if enable_ssl %}
 listen 443 ssl;
 ssl_certificate {{ ssl_certificate }};
 ssl_certificate_key {{ ssl_certificate_key }};
 ssl_protocols TLSv1.2 TLSv1.3;
 ssl_ciphers HIGH:!aNULL:!MD5;
{% endif %}
}

The template uses Jinja2 variables wrapped in double curly braces, which Ansible replaces with actual values from your inventory and role defaults. The {% if enable_ssl %} block conditionally includes SSL configuration, demonstrating how templates can adapt to different environments without duplicating code. The {{ inventory_hostname }} variable is a built-in Ansible fact that automatically contains the current host’s name from the inventory.

Step 6: Create the Application Deployment Role

With the web server role in place, we need a role to deploy the actual application. This role handles pulling application code, installing dependencies, configuring the application, and managing the application process with systemd. This pattern works for any application whether it is a Python Flask app, a Node.js Express server, or a compiled Go binary.

# roles/app_deploy/tasks/main.yml
---
- name: Install application dependencies
 ansible.builtin.apt:
 name:
 - python3-venv
 - python3-dev
 - build-essential
 state: present
 tags: [app, dependencies]

- name: Create application directory
 ansible.builtin.file:
 path: "/opt/{{ app_name }}"
 state: directory
 owner: appuser
 group: appuser
 mode: '0755'
 tags: [app]

- name: Deploy application code
 ansible.builtin.copy:
 src: "app/"
 dest: "/opt/{{ app_name }}/"
 owner: appuser
 group: appuser
 mode: '0644'
 notify: Restart application
 tags: [app, deploy]

- name: Create Python virtual environment
 ansible.builtin.command:
 cmd: "python3 -m venv /opt/{{ app_name }}/venv"
 creates: "/opt/{{ app_name }}/venv/bin/activate"
 become_user: appuser
 tags: [app, dependencies]

- name: Install Python dependencies
 ansible.builtin.pip:
 requirements: "/opt/{{ app_name }}/requirements.txt"
 virtualenv: "/opt/{{ app_name }}/venv"
 become_user: appuser
 notify: Restart application
 tags: [app, dependencies]

- name: Deploy application configuration
 ansible.builtin.template:
 src: app_config.env.j2
 dest: "/opt/{{ app_name }}/.env"
 owner: appuser
 group: appuser
 mode: '0600'
 notify: Restart application
 tags: [app, config]

- name: Deploy systemd service file
 ansible.builtin.template:
 src: app.service.j2
 dest: "/etc/systemd/system/{{ app_name }}.service"
 owner: root
 group: root
 mode: '0644'
 notify:
 - Reload systemd
 - Restart application
 tags: [app, service]

- name: Ensure application is started and enabled
 ansible.builtin.systemd:
 name: "{{ app_name }}"
 state: started
 enabled: true
 tags: [app, service]

- name: Wait for application to be ready
 ansible.builtin.uri:
 url: "http://localhost:{{ app_port }}/health"
 status_code: 200
 timeout: 5
 register: health_check
 retries: 10
 delay: 3
 until: health_check.status == 200
 tags: [app, verify]

The creates parameter on the virtual environment task makes it idempotent: Ansible will skip the task if the venv already exists. The health check at the end uses the ansible.builtin.uri module with retries to wait for the application to start. This is a best practice pattern for deployment automation because it verifies the application is actually serving traffic before the playbook completes.

Create the systemd service template at roles/app_deploy/templates/app.service.j2:

# roles/app_deploy/templates/app.service.j2
[Unit]
Description={{ app_name }} application service
After=network.target

[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/{{ app_name }}
Environment=PATH=/opt/{{ app_name }}/venv/bin:/usr/bin
EnvironmentFile=/opt/{{ app_name }}/.env
ExecStart=/opt/{{ app_name }}/venv/bin/python app.py
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/{{ app_name }}/data
PrivateTmp=true

[Install]
WantedBy=multi-user.target

The systemd service file includes security hardening directives like NoNewPrivileges, ProtectSystem, and PrivateTmp. These restrict the application process to only the permissions it needs, following the principle of least privilege. This is considered essential practice for production deployments in 2026, especially with the 45% year-over-year increase in Ansible adoption driving automation into more security-sensitive environments.

Step 7: Configure the Load Balancer Role

The load balancer distributes incoming traffic across your web servers, providing high availability and scalability. This role configures Nginx as a reverse proxy with health checks and multiple load balancing algorithms. In production environments managing thousands of nodes, companies like Ensono have used Ansible to automate over 40 million tasks annually across 30,000 assets, saving 210,000 hours per year.

# roles/loadbalancer/tasks/main.yml
---
- name: Install Nginx on load balancer
 ansible.builtin.apt:
 name: nginx
 state: present
 update_cache: true
 tags: [lb, install]

- name: Deploy load balancer configuration
 ansible.builtin.template:
 src: lb_nginx.conf.j2
 dest: /etc/nginx/sites-available/loadbalancer.conf
 owner: root
 group: root
 mode: '0644'
 validate: "nginx -t -c %s"
 notify: Reload Nginx LB
 tags: [lb, config]

- name: Enable load balancer site
 ansible.builtin.file:
 src: /etc/nginx/sites-available/loadbalancer.conf
 dest: /etc/nginx/sites-enabled/loadbalancer.conf
 state: link
 notify: Reload Nginx LB
 tags: [lb, config]

- name: Remove default site
 ansible.builtin.file:
 path: /etc/nginx/sites-enabled/default
 state: absent
 notify: Reload Nginx LB
 tags: [lb, config]

- name: Allow HTTP through UFW on load balancer
 community.general.ufw:
 rule: allow
 port: '80'
 proto: tcp
 tags: [lb, firewall]

- name: Ensure Nginx is running on load balancer
 ansible.builtin.systemd:
 name: nginx
 state: started
 enabled: true
 tags: [lb]

 handlers:
 - name: Reload Nginx LB
 ansible.builtin.systemd:
 name: nginx
 state: reloaded

The load balancer template dynamically generates the upstream block by iterating over the hosts in the webservers group. Create roles/loadbalancer/templates/lb_nginx.conf.j2:

# roles/loadbalancer/templates/lb_nginx.conf.j2
upstream web_backends {
 least_conn;
{% for host in groups['webservers'] %}
 server {{ hostvars[host]['ansible_host'] }}:{{ hostvars[host]['http_port'] | default(80) }} max_fails=3 fail_timeout=30s;
{% endfor %}
}

server {
 listen 80;
 server_name {{ lb_domain | default('_') }};

 location / {
 proxy_pass http://web_backends;
 proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header X-Forwarded-Proto $scheme;

 # Timeout settings
 proxy_connect_timeout 10s;
 proxy_read_timeout 60s;
 proxy_send_timeout 60s;

 # Buffer settings
 proxy_buffering on;
 proxy_buffer_size 4k;
 proxy_buffers 8 4k;
 }

 location /lb-health {
 access_log off;
 return 200 '{"status": "healthy", "backends": {{ groups["webservers"] | length }}}';
 add_header Content-Type application/json;
 }
}

The {% for host in groups['webservers'] %} loop is a powerful Jinja2 pattern that dynamically discovers backend servers from the inventory. When you add a new web server to the webservers group in your inventory, the load balancer configuration automatically updates on the next playbook run. This dynamic discovery eliminates the error-prone process of manually editing load balancer configurations and is a key advantage of using Ansible for infrastructure automation.

Step 8: Assemble the Complete Playbook with Role Orchestration

Now update your site.yml to orchestrate all roles together. This is where you define the order of operations and which roles apply to which host groups. The playbook runs top to bottom, with each play executing on its target hosts before moving to the next play.

# site.yml - Complete orchestrated playbook
---
- name: Common configuration for all servers
 hosts: all
 become: true
 gather_facts: true
 tags: [common]

 vars:
 common_packages:
 - curl
 - wget
 - vim
 - htop
 - unzip
 - net-tools
 - python3-pip
 - ufw
 - fail2ban
 timezone: "America/New_York"

 tasks:
 - name: Update and install common packages
 ansible.builtin.apt:
 name: "{{ common_packages }}"
 state: present
 update_cache: true
 cache_valid_time: 3600

 - name: Set timezone
 community.general.timezone:
 name: "{{ timezone }}"

 - name: Enable UFW with SSH allowed
 community.general.ufw:
 rule: allow
 port: '22'
 proto: tcp

 - name: Enable UFW
 community.general.ufw:
 state: enabled
 default: deny
 direction: incoming

- name: Configure web servers
 hosts: webservers
 become: true
 tags: [webservers]

 vars:
 app_name: "mywebapp"
 app_port: 8080

 roles:
 - role: webserver
 - role: app_deploy

- name: Configure load balancer
 hosts: loadbalancers
 become: true
 tags: [loadbalancer]

 roles:
 - role: loadbalancer

- name: Verify deployment
 hosts: loadbalancers
 become: false
 tags: [verify]

 tasks:
 - name: Test load balancer endpoint
 ansible.builtin.uri:
 url: "http://{{ ansible_host }}/health"
 status_code: 200
 register: lb_health
 retries: 5
 delay: 3
 until: lb_health.status == 200

 - name: Display deployment status
 ansible.builtin.debug:
 msg: "Deployment complete. Load balancer at http://{{ ansible_host }} is healthy."

Run the complete playbook with verbose output to see every task execute:

# Run the full playbook
ansible-playbook site.yml -v

# Run only the webserver tasks
ansible-playbook site.yml --tags webservers

# Run in check mode (dry run) to preview changes
ansible-playbook site.yml --check --diff

# Expected output (abbreviated):
# PLAY [Common configuration for all servers] ****
# TASK [Gathering Facts] ****
# ok: [web1]
# ok: [web2]
# ok: [lb1]
#
# TASK [Update and install common packages] ****
# changed: [web1]
# changed: [web2]
# changed: [lb1]
#
# PLAY [Configure web servers] ****
# TASK [webserver : Install Nginx] ****
# changed: [web1]
# changed: [web2]
#
# ... (more tasks)
#
# PLAY RECAP ****
# lb1 : ok=12 changed=8 unreachable=0 failed=0 skipped=0
# web1 : ok=24 changed=18 unreachable=0 failed=0 skipped=0
# web2 : ok=24 changed=18 unreachable=0 failed=0 skipped=0
#
# Playbook run took 0 days, 0 hours, 2 minutes, 34 seconds

The --check --diff flags are your safety net. Check mode simulates the playbook without making changes, and diff mode shows exactly what would change in each file. Always run this before applying changes to production servers. This practice is especially important as 110 verified companies including Accenture, Wells Fargo, and HSBC rely on Ansible for critical infrastructure automation.

Step 9: Implement Variables and Vault for Secrets Management

Real infrastructure requires secrets: database passwords, API keys, SSL certificates, and other sensitive data. Ansible Vault encrypts these secrets so you can safely commit them to version control. This step adds environment-specific variables and encrypted secrets to your project, following the variable precedence hierarchy that Ansible uses to determine which value wins when the same variable is defined in multiple places.

Create the group_vars directory structure for environment-specific variables:

# Create group_vars directories
mkdir -p group_vars/all group_vars/webservers group_vars/loadbalancers

# group_vars/all/vars.yml - Shared non-secret variables
cat > group_vars/all/vars.yml << 'EOF'
---
app_name: mywebapp
app_environment: production
monitoring_enabled: true
log_retention_days: 30
backup_enabled: true
backup_schedule: "0 2 * * *"
EOF

# Create encrypted vault file for secrets
ansible-vault create group_vars/all/vault.yml
# You'll be prompted for a vault password, then enter:
# ---
# vault_db_password: "SuperSecret123!"
# vault_api_key: "sk-abc123def456"
# vault_ssl_passphrase: "CertPassphrase2026"

# To edit later:
ansible-vault edit group_vars/all/vault.yml

# To view without editing:
ansible-vault view group_vars/all/vault.yml

# Run playbook with vault password prompt
ansible-playbook site.yml --ask-vault-pass

# Or use a password file (don't commit this!)
echo "YourVaultPassword" > .vault_pass
chmod 600 .vault_pass
echo ".vault_pass" >> .gitignore
ansible-playbook site.yml --vault-password-file .vault_pass

The naming convention of prefixing vault variables with vault_ is a best practice that makes it immediately clear which variables are encrypted. In your tasks and templates, reference vault variables by mapping them to regular variable names in vars.yml:

# group_vars/webservers/vars.yml
---
# Map vault variables to application variables
db_password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"

# Web server specific settings
nginx_worker_processes: 2
nginx_worker_connections: 2048
app_port: 8080
app_workers: 4
app_threads: 2

# Health check settings
health_check_path: "/health"
health_check_interval: 30

Ansible’s variable precedence has 22 levels, from least to most specific. The most important to remember are: role defaults (lowest), inventory group vars, inventory host vars, playbook vars, role vars, extra vars via -e (highest). This hierarchy gives you fine-grained control over configuration across different environments. For example, you can override the app_workers variable for a single host by setting it in host_vars/web1/vars.yml without affecting any other server.

Step 10: Add Error Handling and Conditional Logic

Production playbooks need reliable error handling. Ansible provides several mechanisms for managing failures: block/rescue/always for try-catch-finally patterns, ignore_errors for non-critical tasks, and failed_when for custom failure conditions. This step adds these patterns to make your automation resilient.

# roles/app_deploy/tasks/deploy_with_rollback.yml
---
- name: Deploy application with rollback capability
 block:
 - name: Create backup of current deployment
 ansible.builtin.archive:
 path: "/opt/{{ app_name }}"
 dest: "/tmp/{{ app_name }}_backup_{{ ansible_date_time.iso8601_basic_short }}.tar.gz"
 format: gz
 when: ansible_facts['file'] is defined

 - name: Deploy new application code
 ansible.builtin.copy:
 src: "app/"
 dest: "/opt/{{ app_name }}/"
 owner: appuser
 group: appuser
 backup: true

 - name: Install updated dependencies
 ansible.builtin.pip:
 requirements: "/opt/{{ app_name }}/requirements.txt"
 virtualenv: "/opt/{{ app_name }}/venv"
 become_user: appuser

 - name: Restart application
 ansible.builtin.systemd:
 name: "{{ app_name }}"
 state: restarted

 - name: Verify application health after deploy
 ansible.builtin.uri:
 url: "http://localhost:{{ app_port }}/health"
 status_code: 200
 register: post_deploy_health
 retries: 5
 delay: 5
 until: post_deploy_health.status == 200

 rescue:
 - name: Deployment failed - rolling back
 ansible.builtin.debug:
 msg: "Deployment failed! Initiating rollback..."

 - name: Restore backup
 ansible.builtin.unarchive:
 src: "/tmp/{{ app_name }}_backup_{{ ansible_date_time.iso8601_basic_short }}.tar.gz"
 dest: "/"
 remote_src: true

 - name: Restart application with previous version
 ansible.builtin.systemd:
 name: "{{ app_name }}"
 state: restarted

 - name: Fail the play after rollback
 ansible.builtin.fail:
 msg: "Deployment failed and was rolled back. Check logs at /var/log/{{ app_name }}/"

 always:
 - name: Clean up old backups (keep last 5)
 ansible.builtin.shell: |
 ls -t /tmp/{{ app_name }}_backup_*.tar.gz | tail -n +6 | xargs -r rm
 changed_when: false
 ignore_errors: true

The block/rescue/always pattern works exactly like try/catch/finally in programming languages. If any task in the block section fails, execution jumps to the rescue section, which restores the backup and restarts the old version. The always section runs regardless of success or failure, cleaning up old backup files. This rollback pattern is critical for zero-downtime deployments and is used extensively by organizations like BankNXT, which cut deployment times by 40% using Ansible Automation Platform.

Add conditional logic for multi-platform support:

# Conditional tasks based on OS family
- name: Install packages on Debian/Ubuntu
 ansible.builtin.apt:
 name: "{{ debian_packages }}"
 state: present
 when: ansible_os_family == "Debian"

- name: Install packages on RHEL/CentOS
 ansible.builtin.dnf:
 name: "{{ rhel_packages }}"
 state: present
 when: ansible_os_family == "RedHat"

# Conditional based on custom variable
- name: Enable SSL configuration
 ansible.builtin.template:
 src: ssl.conf.j2
 dest: /etc/nginx/conf.d/ssl.conf
 when: enable_ssl | bool
 notify: Reload Nginx

# Conditional with multiple conditions
- name: Apply production-only hardening
 ansible.builtin.include_tasks: hardening.yml
 when:
 - app_environment == "production"
 - ansible_virtualization_type != "docker"

The when clause evaluates Jinja2 expressions. You can use Ansible facts like ansible_os_family, custom variables, and complex boolean logic. The | bool filter ensures string values like β€œtrue” or β€œyes” are properly evaluated as booleans. Always test conditional logic with --check mode before running against production servers.

Common Pitfalls and How to Avoid Them

After helping hundreds of teams adopt Ansible, certain mistakes appear repeatedly. Understanding these pitfalls before you encounter them will save you hours of debugging and prevent potential outages. Here are the most common issues and their solutions.

Pitfall 1: YAML indentation errors. YAML is whitespace-sensitive, and a single wrong indent can change the meaning of your playbook entirely. A task indented one level too deep becomes a property of the previous task instead of a separate task. Always use spaces, never tabs, and configure your editor to show whitespace characters. The yamllint tool catches these errors before Ansible does.

Pitfall 2: Not quoting variable-only values. When a YAML value starts with {{, YAML parsers interpret it as a dictionary. Always wrap variable-only values in quotes: write port: "{{ app_port }}" instead of port: {{ app_port }}. This is Ansible’s most frequently asked question and the source of countless β€œWe could not find a matching template” errors.

Pitfall 3: Running tasks that are not idempotent. Using ansible.builtin.command or ansible.builtin.shell without guards makes your playbook non-idempotent. Always add creates, removes, or when conditions to command tasks so they skip when the desired state already exists. Better yet, use purpose-built modules like ansible.builtin.apt or ansible.builtin.file that are idempotent by design.

Pitfall 4: Forgetting to use become for privileged tasks. Package installation, service management, and file operations in system directories require root privileges. If a task silently fails or reports β€œpermission denied,” check whether you need become: true. Set it at the play level for plays where most tasks need root, and use become: false on individual tasks that should run as the connecting user.

Pitfall 5: Hardcoding values instead of using variables. Every value that might change between environments should be a variable. IP addresses, port numbers, file paths, package versions, and usernames should all come from variables. Use defaults/main.yml in roles for sensible defaults and let inventory or group_vars override them per environment.

Pitfall 6: Not using handlers for service restarts. Placing service restart tasks directly after configuration changes causes unnecessary restarts when the configuration has not actually changed. Handlers only fire when notified, and they only fire once even if notified multiple times during a play. This prevents the web server from restarting five times when five configuration files change.

Pitfall 7: Ignoring the --check --diff safety net. Every playbook should be testable in check mode. Tasks that use command or shell will be skipped in check mode unless you add check_mode: false, which can cause dependent tasks to fail. Design your playbooks to work cleanly in check mode by using registered variables and conditional checks.

Troubleshooting Guide

Even well-written playbooks encounter issues. This troubleshooting guide covers the most common errors you will face during Ansible automation and provides tested solutions for each one. Bookmark this section as a reference for your future Ansible projects.

ErrorCauseSolution
UNREACHABLE! SSH connection errorSSH key not found, wrong user, or firewall blocking port 22Test with ssh -vvv user@host, verify key permissions are 600, check UFW/iptables rules
Permission denied (publickey)SSH key not authorized on target or wrong key pathRun ssh-copy-id -i ~/.ssh/id_ed25519 user@host, verify ~/.ssh/authorized_keys on target
MODULE FAILURE - No module namedPython not installed on managed node or wrong interpreter pathSet ansible_python_interpreter: /usr/bin/python3 in inventory, install python3 on target
The field 'args' has an invalid valueUnquoted Jinja2 variable at the start of a YAML valueWrap the value in quotes: "{{ variable }}" instead of {{ variable }}
AnsibleUndefinedVariableVariable referenced but never defined in any scopeCheck spelling, verify variable is in the correct group_vars/host_vars file, use {{ var | default('fallback') }}
Failed to connect to the host via ssh: Host key verification failedHost key changed or not in known_hostsSet host_key_checking = False in ansible.cfg or accept the key manually first
ERROR! Attempting to decrypt but no vault secrets foundPlaybook references vault-encrypted files but no vault password providedAdd --ask-vault-pass or --vault-password-file .vault_pass to the command
Timeout (12s) waiting for privilege escalation promptsudo requires a password but none was providedAdd --ask-become-pass or configure passwordless sudo for the ansible user
ERROR! conflicting action statementsTwo module names in the same task (often from bad YAML indentation)Check indentation. Each task should have exactly one module. Use yamllint to validate
fatal: [host]: FAILED! => changed=false, msg=No package matching foundPackage name differs between OS versions or apt cache is staleAdd update_cache: true to the apt task, verify the package name with apt search

Debugging technique 1: Increase verbosity. Run playbooks with -v for basic verbose output, -vv for connection info, -vvv for task plugin details, or -vvvv for complete SSH debug output. Start with -vv and increase as needed. The verbose output shows exactly which SSH command Ansible is running, which module arguments it is passing, and what the remote system returns.

Debugging technique 2: Use the debug module. Insert ansible.builtin.debug tasks to print variable values at any point in your playbook. Combine with register to capture and inspect task output: register the output of a task, then use debug: var=result to display it. This is invaluable for understanding why conditional logic is not behaving as expected.

Debugging technique 3: Run on a single host. When debugging, limit execution to one host with --limit web1. This speeds up feedback loops and prevents errors from affecting multiple servers. Combine with --start-at-task "Task Name" to skip directly to the problematic task instead of re-running the entire playbook.

Advanced Tips for Production Ansible

Once you have the basics working, these advanced techniques will take your Ansible automation to production grade. These tips come from real-world experience managing infrastructure at scale and reflect best practices adopted by organizations running Ansible Automation Platform 2.6 in enterprise environments.

Use ansible-lint for code quality. Install ansible-lint and run it against your playbooks before every commit. It catches common antipatterns like using shell when a module exists, missing name attributes on tasks, deprecated module usage, and incorrect FQCN (Fully Qualified Collection Names). In 2026, ansible-lint integrates directly with VS Code through the Red Hat Ansible extension for real-time feedback.

Implement molecule for role testing. Molecule is the standard testing framework for Ansible roles. It spins up a Docker container or cloud instance, runs your role against it, and verifies the result with Testinfra or Ansible assertions. Every role you write should have a molecule test scenario. This catches bugs before they reach production and enables confident refactoring.

Use dynamic inventory for cloud environments. If your infrastructure runs on AWS, Azure, or Google Cloud, stop maintaining static inventory files. Ansible’s cloud inventory plugins automatically discover instances by tags, regions, and other metadata. For AWS, the amazon.aws.aws_ec2 plugin generates inventory from your running EC2 instances. Google adopted Ansible Automation Platform in 2025 specifically for orchestrating automation in complex virtualized environments using dynamic inventory.

# aws_ec2.yml - Dynamic inventory plugin for AWS
plugin: amazon.aws.aws_ec2
regions:
 - us-east-1
 - us-west-2
filters:
 tag:Environment: production
 instance-state-name: running
keyed_groups:
 - key: tags.Role
 prefix: role
 - key: placement.region
 prefix: region
hostnames:
 - private-ip-address
compose:
 ansible_host: private_ip_address
 ansible_user: "'ubuntu'"

Use Ansible Lightspeed for AI-assisted automation. Released with Ansible Automation Platform 2.5 and expanded in 2.6, Ansible Lightspeed uses AI to generate task recommendations and complete playbooks from natural language descriptions. With the 2.6 release adding bring-your-own-model support for Red Hat AI, OpenAI, and Azure OpenAI, teams can now choose their preferred AI backend. While not a replacement for understanding Ansible fundamentals, Lightspeed accelerates playbook development significantly.

Implement callback plugins for observability. The profile_tasks callback shows execution time per task, while the json callback outputs structured data perfect for ingestion into monitoring systems. For enterprise environments, the ara callback records playbook runs in a database with a web UI for auditing and troubleshooting historical executions.

Complete Project Structure and File Reference

Here is the complete project structure with every file from this Ansible tutorial. This represents a production-ready project layout that follows Ansible best practices for 2026. You can use this structure as a template for your own projects.

# Complete project tree
ansible-tutorial/
β”œβ”€β”€ ansible.cfg # Project configuration
β”œβ”€β”€ inventory.yml # Static inventory
β”œβ”€β”€ site.yml # Main orchestration playbook
β”œβ”€β”€ requirements.yml # Galaxy collection dependencies
β”œβ”€β”€ .vault_pass # Vault password (gitignored)
β”œβ”€β”€ .gitignore # Git ignore rules
β”‚
β”œβ”€β”€ group_vars/
β”‚ β”œβ”€β”€ all/
β”‚ β”‚ β”œβ”€β”€ vars.yml # Shared variables
β”‚ β”‚ └── vault.yml # Encrypted secrets
β”‚ β”œβ”€β”€ webservers/
β”‚ β”‚ └── vars.yml # Web server variables
β”‚ └── loadbalancers/
β”‚ └── vars.yml # Load balancer variables
β”‚
β”œβ”€β”€ host_vars/
β”‚ └── web1/
β”‚ └── vars.yml # Host-specific overrides
β”‚
β”œβ”€β”€ roles/
β”‚ β”œβ”€β”€ webserver/
β”‚ β”‚ β”œβ”€β”€ defaults/main.yml # Default variables
β”‚ β”‚ β”œβ”€β”€ tasks/main.yml # Nginx tasks
β”‚ β”‚ β”œβ”€β”€ handlers/main.yml # Service handlers
β”‚ β”‚ β”œβ”€β”€ templates/
β”‚ β”‚ β”‚ β”œβ”€β”€ nginx.conf.j2 # Main nginx config
β”‚ β”‚ β”‚ └── vhost.conf.j2 # Virtual host config
β”‚ β”‚ └── meta/main.yml # Role metadata
β”‚ β”‚
β”‚ β”œβ”€β”€ app_deploy/
β”‚ β”‚ β”œβ”€β”€ defaults/main.yml
β”‚ β”‚ β”œβ”€β”€ tasks/
β”‚ β”‚ β”‚ β”œβ”€β”€ main.yml # Deployment tasks
β”‚ β”‚ β”‚ └── deploy_with_rollback.yml
β”‚ β”‚ β”œβ”€β”€ handlers/main.yml
β”‚ β”‚ β”œβ”€β”€ templates/
β”‚ β”‚ β”‚ β”œβ”€β”€ app.service.j2 # Systemd service
β”‚ β”‚ β”‚ └── app_config.env.j2 # App environment
β”‚ β”‚ β”œβ”€β”€ files/
β”‚ β”‚ β”‚ └── app/ # Application source code
β”‚ β”‚ └── meta/main.yml
β”‚ β”‚
β”‚ └── loadbalancer/
β”‚ β”œβ”€β”€ defaults/main.yml
β”‚ β”œβ”€β”€ tasks/main.yml
β”‚ β”œβ”€β”€ handlers/main.yml
β”‚ β”œβ”€β”€ templates/
β”‚ β”‚ └── lb_nginx.conf.j2 # LB config template
β”‚ └── meta/main.yml
β”‚
└── templates/
 └── timesyncd.conf.j2 # NTP configuration

Create a requirements.yml file to pin your collection dependencies:

# requirements.yml - Ansible Galaxy dependencies
---
collections:
 - name: ansible.posix
 version: ">=1.5.0"
 - name: community.general
 version: ">=8.0.0"
 - name: amazon.aws
 version: ">=7.0.0"

# Install with:
# ansible-galaxy collection install -r requirements.yml

This project structure separates concerns clearly: inventory defines the infrastructure, group_vars and host_vars customize behavior per scope, roles encapsulate reusable automation, and the main playbook orchestrates everything. When your team grows, this structure scales naturally. New team members can understand the project by reading the inventory and site.yml, then diving into individual roles as needed.

Ansible vs Other IaC Tools: When to Choose Ansible

Understanding where Ansible fits in the infrastructure automation landscape helps you make the right tool choices. Ansible is not the only option, and in many cases, the best solution combines multiple tools. Here is how Ansible compares to other popular infrastructure-as-code tools in 2026.

FeatureAnsibleTerraformPuppetChef
ArchitectureAgentless (SSH)Agentless (API)Agent-basedAgent-based
LanguageYAML/Jinja2HCLPuppet DSLRuby DSL
Primary UseConfiguration management, deploymentCloud provisioningConfiguration enforcementConfiguration management
State ManagementStatelessState fileCatalog-basedNode objects
Learning CurveLow (YAML)Medium (HCL)High (DSL)High (Ruby)
Cloud SupportModules for all cloudsNative cloud providersCloud pluginsCloud cookbooks
2026 Adoption Trend45% YoY growthDominant for IaCDecliningDeclining
Enterprise PlatformAAP 2.6 with AITerraform Cloud/EnterprisePuppet EnterpriseChef Automate

Ansible excels at configuration management and application deployment. Its agentless architecture means zero software to install on managed nodes, which is crucial in environments where installing agents is restricted by security policies. Terraform, covered in our thorough Terraform AWS tutorial, excels at cloud resource provisioning with its declarative state management. The two tools complement each other perfectly: use Terraform to create your infrastructure, then use Ansible to configure it.

For containerized environments, Ansible works alongside Docker and Kubernetes. Our Docker Compose tutorial covers container orchestration in detail, and our Docker vs Kubernetes comparison helps you decide which container platform suits your needs. Ansible’s community.docker and kubernetes.core collections provide native integration with both platforms, letting you manage containers alongside traditional infrastructure from the same automation framework.

Performance Optimization and Scaling

As your infrastructure grows, playbook execution time becomes a concern. An Ansible playbook managing 10 servers in 2 minutes could take 20 minutes at 100 servers and over an hour at 1,000. These optimization techniques keep execution fast even at scale, drawing from lessons learned by organizations like Turkcell, which achieved zero-touch provisioning with Ansible for 5G workloads handling over 15 terabits per second of traffic.

Enable pipelining. As configured in our ansible.cfg, pipelining reduces SSH operations from 5 per task to 2 by piping the module directly over the SSH connection instead of copying it to a temporary file. This alone can reduce execution time by 30-40%.

Increase forks for parallel execution. The default of 5 forks means Ansible manages only 5 hosts at a time. For larger inventories, increase this to 20, 50, or even 100 depending on your control node’s resources. Each fork uses approximately 20-30MB of RAM, so a control node with 8GB can comfortably run 50 forks.

Use free strategy for independent tasks. The default linear strategy waits for all hosts to finish each task before moving to the next. The free strategy lets fast hosts run ahead, which is useful when hosts have different performance characteristics. Set it with strategy: free at the play level.

Cache facts to avoid repeated gathering. Fact gathering is the slowest part of most playbooks. Enable fact caching with Redis or JSON files so that facts are gathered once and reused across subsequent runs:

# Add to ansible.cfg for JSON fact caching
[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
fact_caching_timeout = 86400

Use async and poll for long-running tasks. Tasks like package upgrades or large file transfers can run asynchronously, freeing the connection for other work. Set async: 300 and poll: 0 to fire-and-forget, then use async_status to check completion later. This is particularly effective for tasks that run identically on many hosts.

Related Coverage

Explore more infrastructure automation and DevOps tutorials on Tech Insider:

Frequently Asked Questions

What is the difference between ansible-core and the Ansible package?

The ansible-core package contains the Ansible engine, command-line tools, and built-in modules. The ansible package includes ansible-core plus hundreds of community-maintained collections from Ansible Galaxy. For most users, installing the full ansible package is recommended because it provides modules for cloud providers, networking equipment, databases, and more without needing to install each collection separately. The final release of the Ansible 11.x series was version 11.13.0 in December 2025.

Can Ansible manage Windows servers?

Yes. While the Ansible control node must run on Linux or macOS, it can manage Windows hosts over WinRM or SSH. The ansible.windows collection provides modules for Windows-specific tasks like managing IIS, Windows features, registry entries, and Active Directory. Set ansible_connection: winrm in your inventory for Windows hosts. Windows support has improved significantly in 2025-2026 with better PowerShell module integration.

How does Ansible Vault compare to HashiCorp Vault?

Ansible Vault encrypts files at rest within your project, making it suitable for secrets that are committed to version control. HashiCorp Vault is a separate secrets management platform that provides dynamic secret generation, access control, and audit logging. For small to medium projects, Ansible Vault is sufficient. For enterprise environments with complex secret management requirements, use HashiCorp Vault alongside Ansible via the community.hashi_vault collection to dynamically retrieve secrets during playbook execution.

What is the maximum number of hosts Ansible can manage?

There is no hard limit. Ansible has been tested managing tens of thousands of hosts in production. Ensono automates over 40 million tasks annually across 30,000 assets using Ansible Automation Platform. Performance depends on your control node resources, network bandwidth, and playbook complexity. For very large environments (1,000+ hosts), use execution environments with Ansible Automation Platform, increase forks, enable fact caching, and consider splitting your inventory into smaller operational groups.

Should I use Ansible or Terraform?

Use both. Terraform excels at provisioning cloud infrastructure (creating VMs, networks, databases), while Ansible excels at configuring those resources after creation. Terraform manages the lifecycle of infrastructure through state files, while Ansible is stateless and focuses on ensuring the correct configuration exists. Many teams use Terraform to create their cloud infrastructure on AWS, Azure, or GCP, then use Ansible to configure the operating system, install software, and deploy applications.

How do I test Ansible playbooks before running them in production?

Use a multi-layered testing approach. First, run ansible-lint and yamllint for static analysis. Second, use --check --diff mode for dry runs against production. Third, use Molecule with Docker containers for automated role testing. Fourth, maintain a staging environment that mirrors production and run playbooks there first. Fifth, use --limit to roll out changes to a single host before the full fleet. This graduated approach catches issues at every stage before they reach production.

What is Ansible Lightspeed and should I use it?

Ansible Lightspeed is Red Hat’s AI-powered coding assistant for Ansible, introduced with Ansible Automation Platform 2.5 and expanded in 2.6. It generates task recommendations from natural language descriptions and autocompletes playbook code. The 2.6 release added bring-your-own-model support for Red Hat AI, OpenAI, and Azure OpenAI. It is useful for accelerating playbook development, especially for common patterns, but should not replace understanding of Ansible fundamentals. Available through Red Hat Ansible Automation Platform subscriptions.

How do I handle different environments (dev, staging, production) with Ansible?

Use separate inventory files per environment: inventory/dev.yml, inventory/staging.yml, and inventory/production.yml. Shared variables go in group_vars/all/, while environment-specific overrides go in each inventory’s corresponding group_vars. Run playbooks against a specific environment with -i inventory/staging.yml. This pattern keeps your playbooks and roles identical across environments while allowing configuration to differ where needed, such as instance sizes, replica counts, and feature flags.

πŸ‘ Marcus Chen

Marcus Chen

Senior Tech Reporter

Marcus Chen is a Senior Tech Reporter at Tech Insider covering cloud computing, enterprise software, and the business of technology. Before joining TI, he spent five years at ZDNet covering digital transformation across European enterprises and three years at The Register reporting on cloud infrastructure. Marcus is known for his deep dives into cloud cost optimization and multi-cloud strategy. He holds a degree in Computer Science from Imperial College London and speaks regularly at KubeCon and CloudNative events.

View all articles
πŸ‘ Tech Insider
Tech
Insider

Tech Insider delivers in-depth coverage of the technologies shaping the future: AI, cybersecurity, cloud computing, hardware, and the trends that matter.

Company

Explore

Categories

Β© 2026 Tech Insider Media AB. All rights reserved.