Skip to content

Ansible Playbooks — Automation & Configuration Management Guide

DodaTech Updated 2026-06-24 6 min read

In this tutorial, you'll learn about Ansible Playbooks. 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 server configuration through tasks, handlers, and roles, enabling idempotent Configuration Management across thousands of servers without agent software.

What You'll Learn

Why It Matters

Configuring servers manually does not scale. When you need to update NGINX config on 200 web servers or rotate certificates across all database hosts, manual processes guarantee mistakes. Ansible playbooks execute these tasks consistently and idempotently. DodaTech uses Ansible to manage 500+ servers across dev, staging, and production — applying OS patches, updating Durga Antivirus Pro agents, and verifying Compliance with a single command.

Real-World Use

DodaZIP's release Process triggers an Ansible playbook that deploys application artifacts to 50 web servers in rolling fashion — taking each server out of the load balancer pool, updating the application, running smoke tests, and returning it to service — with zero downtime.

flowchart TD
    A[Control Node] --> B[Inventory: production]
    A --> C[Playbook: site.yml]
    B --> D[Web Servers]
    B --> E[API Servers]
    B --> F[Database Servers]
    C --> G[Play 1: All hosts - Base config]
    C --> H[Play 2: Web - Deploy app]
    C --> I[Play 3: DB - Backup config]
    G --> J[Package updates]
    G --> K[Monitoring agents]
    H --> L[Rolling deploy]
    I --> M[PostgreSQL config]
    style A fill:#EE0000,color:#fff
    style C fill:#EE0000,color:#fff
â„šī¸ Info

Prerequisites: Basic Linux administration, SSH access to target servers, and Ansible installed on the control node.

Installation

# Install Ansible on the control node
pip install ansible

# Verify installation
ansible --version

# Expected output:
# ansible [core 2.16.0]
#   config file = /etc/ansible/ansible.cfg
#   configured module search path = ['/home/ansible/.ansible/plugins/modules']
#   ansible python module location = /home/ansible/.local/lib/python3.11/site-packages/ansible
#   python version = 3.11.4 (main, Jun  7 2026, 10:23:00) [GCC 12.3.0]

# Test connectivity
ansible all -i inventory/production.ini -m ping

Inventory Management

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

[api]
api-01 ansible_host=10.0.2.10
api-02 ansible_host=10.0.2.11

[database]
db-primary ansible_host=10.0.3.10
db-replica-01 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

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

- name: Deploy web application
  hosts: web
  become: yes
  vars:
    app_version: "2.5.0"
    app_port: 3000

  tasks:
    - name: Create app directory
      file:
        path: /opt/dodatech
        state: directory
        owner: deploy
        group: deploy
        mode: 0755

    - name: Copy application artifact
      copy:
        src: "artifacts/dodazip-v{{ app_version }}.tar.gz"
        dest: "/opt/dodatech/dodazip-v{{ app_version }}.tar.gz"
      notify: restart app

    - name: Extract application
      unarchive:
        src: "/opt/dodatech/dodazip-v{{ app_version }}.tar.gz"
        dest: /opt/dodatech
        remote_src: yes
        owner: deploy
        group: deploy

    - name: Configure application
      template:
        src: templates/app-config.j2
        dest: /opt/dodatech/.env
      notify: restart app

    - name: Ensure app service is running
      service:
        name: dodazip
        state: started
        enabled: yes

  handlers:
    - name: restart app
      service:
        name: dodazip
        state: restarted

Jinja2 Templates

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

{% 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
# templates/app-config.j2
# DodaTech Application Configuration
PORT={{ app_port }}
NODE_ENV=production
API_URL=https://api.dodatech.com
DB_HOST={{ hostvars['db-primary']['ansible_default_ipv4']['address'] }}
LOG_LEVEL=info
APP_VERSION={{ app_version }}

Idempotency Patterns

# Idempotent shell command with creates
- name: Extract archive idempotently
  unarchive:
    src: app-v2.tar.gz
    dest: /opt/app/
  # unarchive module is natively idempotent

# Making shell commands idempotent
- name: Add GPG key (only if not present)
  shell: |
    apt-key list | grep -q "DodaTech" || \
      curl -fsSL https://repo.dodatech.com/gpg | apt-key add -

# Using handler pattern for service restarts
- name: Update configuration
  template:
    src: config.j2
    dest: /etc/app/config.yml
  notify: restart app
  # Handler runs only when config actually changes

Tags for Selective Execution

- name: Security hardening
  hosts: all
  become: yes
tags: security

  tasks:
    - name: Disable root SSH login
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^PermitRootLogin'
        line: 'PermitRootLogin no'
      notify: restart sshd
tags: ssh

    - name: Configure fail2ban
      template:
        src: fail2ban.local.j2
        dest: /etc/fail2ban/jail.local
tags: fail2ban

    - name: Install UFW
      package:
        name: ufw
        state: present
tags: firewall

- name: Install monitoring agents
  hosts: all
  become: yes
tags: monitoring

  tasks:
    - name: Deploy Prometheus node exporter
      template:
        src: node_exporter.service.j2
        dest: /etc/systemd/system/node_exporter.service
tags: prometheus

    - name: Deploy Filebeat
      template:
        src: filebeat.yml.j2
        dest: /etc/filebeat/filebeat.yml
tags: filebeat
# Run only security tasks
ansible-playbook site.yml --tags security

# Run all tasks except monitoring
ansible-playbook site.yml --skip-tags monitoring

Common Configuration Mistakes

  1. Playbook fails with SSH timeout: The control node cannot reach the target host on port 22. Check security groups and use <a href="/devops/ansible/">Ansible</a> hostname -m ping to test connectivity first.

  2. Using command/shell breaks idempotency: Ansible cannot detect if a shell command has already run. Use creates or when conditions, or prefer dedicated modules like apt, copy, template.

  3. Variable precedence confusion: Ansible has 22 variable precedence levels. A variable in group_vars/all is overridden by host_vars/hostname, which is overridden by --extra-vars. Use <a href="/devops/ansible/">Ansible</a>-inventory --vars to debug.

  4. Handlers not running when expected: Handlers run at the end of the play, not immediately after the task. If a later task depends on the handler action, use meta: flush_handlers.

  5. Becoming root without passwordless sudo: If the remote user needs a sudo password, add <a href="/devops/ansible/">Ansible</a>_become_password to inventory or use --ask-become-pass.

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. Ansible modules check current state before applying changes, allowing safe re-runs.

  2. How do handlers differ from regular tasks? Answer: Handlers are special tasks that run only when notified by a task change. They run once at the end of the play, preventing unnecessary service restarts.

  3. What is the purpose of gather_facts? Answer: Fact gathering collects system information (OS, IP, memory, disks) from target hosts into <a href="/devops/ansible/">Ansible</a>_facts for conditional execution and template variables.

  4. How does delegate_to change task execution? Answer: By default tasks run on the target host. delegate_to runs a task on a different host (e.g., adding a server to a load balancer pool before deploying).

Challenge

Create an Ansible playbook that bootstraps a complete web stack on a fresh Ubuntu 24.04 server: install PostgreSQL 16 with database and user, install and configure Nginx as reverse proxy, deploy a Node.js app from Git, configure systemd for the app, set up UFW firewall (SSH, HTTP, HTTPS), install fail2ban, harden SSH (disable root login, password auth), and set up SSL with Let's Encrypt.

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

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro