Skip to content

Ansible Playbooks for Configuration Management — Complete Automation Guide

DodaTech Updated 2026-06-22 8 min read

In this tutorial, you'll learn about Ansible Playbooks for Configuration Management. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Ansible playbooks are YAML-based automation scripts that define desired server states through tasks, roles, and inventory management, enabling Configuration Management, application deployment, and Orchestration across thousands of servers without requiring agent software.

What You'll Learn

Why It Matters

Manually configuring servers is error-prone and does not scale beyond a handful of machines. When you need to update the NGINX configuration on 200 web servers, apply a security patch to all database hosts, or onboard a new developer workstation, doing it manually guarantees mistakes. Ansible playbooks execute these tasks consistently, idempotently, and with full auditability -- every run is logged, every change is visible.

Real-World Use

DodaTech uses Ansible to manage 500+ servers across development, staging, and production environments. A single <a href="/devops/ansible/">ansible</a>-playbook site.yml --limit production command applies OS patches, updates Durga Antivirus Pro scan agents, rotates TLS certificates, and verifies compliance -- all in under 15 minutes with zero manual intervention.

flowchart TD
    A["Control Node"] --> B["Inventory: production"]
    A --> C["Playbook: site.yml"]
    B --> D["Web Servers (50 hosts)"]
    B --> E["API Servers (30 hosts)"]
    B --> F["Database Servers (10 hosts)"]
    C --> G["Play 1: All hosts"]
    C --> H["Play 2: Web servers"]
    C --> I["Play 3: Database servers"]
    G --> J["System patching"]
    G --> K["Users & SSH keys"]
    G --> L["Monitoring agents"]
    H --> M["Deploy NGINX config"]
    H --> N["Deploy SSL certs"]
    I --> O["Deploy PostgreSQL config"]
    style A fill:#EE0000,color:#fff
    style C fill:#EE0000,color:#fff
â„šī¸ Info

Prerequisites: Linux administration basics, SSH access to target servers, and Ansible installed on the control node (pip install <a href="/devops/ansible/">ansible</a> or apt install <a href="/devops/ansible/">ansible</a>).

Inventory Management

Inventory defines the servers Ansible manages. It can be static (INI/YAML files) or dynamic (cloud API queries).

# inventory/production.ini
[web]
web-01.example.com ansible_host=10.0.1.10
web-02.example.com ansible_host=10.0.1.11
web-03.example.com ansible_host=10.0.1.12

[api]
api-01.example.com ansible_host=10.0.2.10
api-02.example.com ansible_host=10.0.2.11

[database]
db-primary.example.com ansible_host=10.0.3.10
db-replica-01.example.com ansible_host=10.0.3.11

[production:children]
web
api
database

[production:vars]
ansible_user=deploy
ansible_ssh_private_key_file=/home/ansible/.ssh/production.pem

Expected behavior: Ansible connects to each host using SSH as the deploy user with the specified private key. The [production:children] group aggregates all sub-groups, allowing targeting with <a href="/devops/ansible/">ansible</a> -i inventory/production.ini production.

Core Playbook Structure

# site.yml
---
- name: Apply base configuration to all servers
  hosts: all
  become: yes
  vars:
    ntp_servers:
      - 0.pool.ntp.org
      - 1.pool.ntp.org
    ssh_port: 22
  tasks:
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600
      when: ansible_os_family == "Debian"

    - name: Install essential packages
      apt:
        name:
          - htop
          - curl
          - wget
          - git
          - ufw
          - fail2ban
        state: present

    - name: Configure NTP
      template:
        src: templates/ntp.conf.j2
        dest: /etc/ntp.conf
      notify: restart ntp

  handlers:
    - name: restart ntp
      service:
        name: ntp
        state: restarted

Expected behavior: The playbook runs on all inventory hosts. It updates the apt cache (only on Debian/Ubuntu systems), installs essential packages idempotently, and configures NTP using a Jinja2 template. If the NTP configuration changes, the notify triggers the handler to restart the NTP service.

# templates/ntp.conf.j2
# Managed by Ansible -- do not edit manually
driftfile /var/lib/ntp/ntp.drift
statistics loopstats peerstats clockstats
filegen loopstats file loopstats type day enable
filegen peerstats file peerstats type day enable
filegen clockstats file clockstats type day enable

{% for server in ntp_servers %}
server {{ server }} iburst
{% endfor %}

restrict -4 default kod notrap nomodify nopeer noquery
restrict -6 default kod notrap nomodify nopeer noquery
restrict 127.0.0.1
restrict ::1

Roles for Reusability

Roles organize playbooks into reusable components with a standard directory structure.

# roles/nginx/tasks/main.yml
---
- name: Add Nginx repository
  apt_repository:
    repo: "deb http://nginx.org/packages/{{ ansible_distribution | lower }}/ {{ ansible_distribution_release }} nginx"
    state: present
  when: ansible_os_family == "Debian"

- name: Install Nginx
  apt:
    name: nginx
    state: present

- name: Deploy configuration
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: reload nginx

- name: Deploy virtual host
  template:
    src: vhost.conf.j2
    dest: /etc/nginx/conf.d/{{ app_name }}.conf
  notify: reload nginx

- name: Ensure Nginx is running
  service:
    name: nginx
    state: started
    enabled: yes
# site.yml using roles
---
- name: Configure web servers
  hosts: web
  become: yes
  vars:
    app_name: durga-api
    domain: api.dodatech.com
    ssl_cert_path: /etc/ssl/certs/dodatech.pem
  roles:
    - common
    - nginx
    - nodejs
    - deploy

- name: Configure database servers
  hosts: database
  become: yes
  vars:
    postgres_version: 16
    postgres_max_connections: 200
  roles:
    - common
    - <a href="/databases/postgresql/">PostgreSQL</a>

Expected behavior: The web group servers receive the common, nginx, nodejs, and deploy roles in sequence. Each role has its own tasks, handlers, templates, and default variables. The database group servers get common and <a href="/databases/postgresql/">PostgreSQL</a>. Roles are reusable across different playbooks and projects.

Ansible Vault for Secrets

Encrypt sensitive data like passwords and API keys using Ansible Vault.

# group_vars/production/vault.yml
---
vault_db_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          66386439653236336...<encrypted content>
vault_api_key: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          386239343361363...<encrypted content>
# Create an encrypted variable file
ansible-vault create group_vars/production/vault.yml

# Edit an existing vault file
ansible-vault edit group_vars/production/vault.yml

# Run playbook with vault password file
ansible-playbook site.yml --vault-password-file .vault_pass

Common Errors

  1. Playbook fails with "SSH connection timeout": The control node cannot reach the target host on port 22. Check security group rules, network ACLs, and that the target host is running. Use <a href="/devops/ansible/">ansible</a> hostname -m ping -i inventory.ini to test connectivity first.

  2. Idempotency issues with shell/command modules: Using command or shell modules breaks idempotency because Ansible cannot detect whether the command has already been applied. Use creates or when conditions to make command-based tasks idempotent, or prefer dedicated modules (apt, copy, template, systemd).

  3. Variable precedence confusion: Ansible has 22 levels of variable precedence. A variable defined in group_vars/all is overridden by host_vars/hostname, which is overridden by --extra-vars. Unexpected variable values are often due to forgetting this hierarchy. Use <a href="/devops/ansible/">ansible</a>-inventory --vars to debug.

  4. Becoming root without passwordless sudo: If the remote user requires a password for sudo, add <a href="/devops/ansible/">Ansible</a>_become_password to the inventory or use --ask-become-pass. Without this, tasks that require become: yes fail with "Missing sudo password".

  5. Handlers not running when expected: Handlers run at the end of the play, not immediately after the task that notifies them. If a later task depends on the handler action (e.g., a configuration change followed immediately by a service start), use meta: flush_handlers to force handler execution at a specific point.

Practice Questions

  1. What is idempotency in Ansible and why is it important? Answer: Idempotency means running the same playbook multiple times produces the same result without side effects. Ansible modules are designed to be idempotent -- they check the current state before applying changes. This allows safe re-runs and ensures the system converges to the desired state regardless of the starting point.

  2. How do Ansible roles differ from a flat tasks structure? Answer: Roles encapsulate related tasks, handlers, templates, variables, and defaults into a reusable unit with a standard directory layout. Flat tasks work for simple playbooks but become unmanageable as complexity grows. Roles enable sharing across teams and projects.

  3. What is the purpose of gather_facts: yes in a play? Answer: Fact gathering collects system information (OS, IP addresses, memory, disk, CPU) from target hosts into the <a href="/devops/ansible/">Ansible</a>_facts variable. These facts are used for conditional task execution (when: ansible_os_family == "Debian"), template variables, and system-aware automation.

  4. How does the delegate_to directive change task execution? Answer: By default, tasks run on the target host. delegate_to runs a task on a different host (usually the control node or a management server). This is useful for adding a host to a load balancer pool before deploying, or registering DNS records on the DNS server from the web server's play.

Challenge

Create an Ansible playbook that bootstraps a complete web application stack on a fresh Ubuntu 22.04 server: install and configure PostgreSQL 16 with a database and user for the application, install and configure Nginx as a reverse proxy, deploy a Node.js application from a Git repository, configure systemd for the Node.js app, set up UFW firewall rules allowing only SSH, HTTP, and HTTPS, install and configure fail2ban, harden SSH by disabling root login and password authentication, and set up SSL with Let's Encrypt using certbot.

Mini Project

Build a complete Ansible automation suite for a multi-tier environment: create inventory files for dev, staging, and production with appropriate group variables, write roles for common (users, SSH keys, packages, firewall, NTP), nginx (install, configure vhosts, SSL), <a href="/databases/postgresql/">PostgreSQL</a> (install, configure, users, databases, Replication for production), nodejs (install via nvm, deploy app, configure systemd), and monitoring (install and configure Prometheus node exporter and Filebeat). Create a main playbook (site.yml) that applies roles based on group membership, encrypt all secrets with Ansible Vault, use Jinja2 templates for all configuration files, include a comprehensive README.md with usage instructions, and test the entire suite by provisioning a fresh server from scratch.

Resource Description
Terraform IaC Provisioning with Terraform
CI/CD Pipelines Automating Ansible runs
Linux Administration Server management basics
Git Workflows Version control for playbooks

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro