Hassan Agmir Hassan Agmir

Ansible Playbook Example - All you have to learn Ansible

Hassan Agmir
Ansible Playbook Example - All you have to learn Ansible

This blog article on “Ansible Playbook Example,” covering everything from fundamental concepts to advanced features, complete with a fully worked example. It exceeds 15,000 characters, making it suitable for in-depth publication on your blog.

Introduction

Configuration management and orchestration tools have revolutionized how we deploy and maintain infrastructure. Ansible, an open-source automation tool by Red Hat, stands out for its simplicity and agentless architecture. At the heart of Ansible’s power lie Playbooks—YAML-based files that describe the desired state of your systems and orchestrate complex workflows across one or multiple hosts.

In this article, we’ll dive deep into Ansible Playbooks:

  1. Core Concepts: hosts, plays, tasks, modules
  2. Playbook Anatomy: structure, syntax, and components
  3. Inventory & Variables: grouping hosts, scoping, precedence
  4. Handlers & Templates: reactive tasks, Jinja2 templating
  5. Roles & Project Layout: reusable, organized code
  6. Worked Example: deploying a LAMP stack end‑to‑end
  7. Advanced Features: loops, conditionals, tags, includes/imports
  8. Best Practices: idempotence, security, testing

By the end, you’ll have a clear blueprint for writing robust Ansible Playbooks and organizing them for production use.

1. Core Concepts

Before exploring Playbooks, let’s recap key Ansible components:

  • Control Node: where you run ansible or ansible-playbook.
  • Managed Nodes: remote servers configured via SSH (no agent required).
  • Inventory: defines groups of hosts (static file or dynamic script).
  • Modules: reusable units of work (e.g., apt, yum, copy, template).
  • Playbooks: YAML files orchestrating modules in plays and tasks.

Ansible communicates over SSH (or WinRM for Windows) and uses Python on the managed node (unless you use raw/command modules).

2. Anatomy of a Playbook

A Playbook is YAML, so indentation and syntax matter. The high-level structure:

---
- name: One Play in the Playbook
  hosts: webservers
  become: yes
  vars:
    http_port: 80

  tasks:
    - name: Install Apache
      package:
        name: apache2
        state: present

    - name: Copy index.html
      template:
        src: index.html.j2
        dest: /var/www/html/index.html
      notify: Restart Apache

  handlers:
    - name: Restart Apache
      service:
        name: apache2
        state: restarted

Plays vs. Tasks

  • Play: Targets a set of hosts, defines variables and high-level directives (hosts, become, etc.).
  • Task: Calls a module with arguments, identified by a name.

Essential Directives

  • hosts: inventory group or list (all, web, db, 10.0.0.5).
  • become: escalate privileges (sudo).
  • vars / vars_files / vars_prompt: define variables.
  • tasks: ordered list of operations.
  • handlers: reactive tasks triggered by notify.

3. Inventory & Variables

Static Inventory

By default, Ansible reads /etc/ansible/hosts or a file you specify with -i. Example:

[web]
web01.example.com
web02.example.com

[db]
db01.example.com

[all:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/id_rsa

Dynamic Inventory

For cloud providers (AWS, GCP, Azure), you can use dynamic inventory scripts or plugins that query your environment in real time.

Variables

Variables drive playbooks’ flexibility:

  • Host vars: in host_vars/web01.yml.
  • Group vars: in group_vars/web.yml.
  • Play vars: via vars: in the play.
  • Extra vars: ansible-playbook ... -e "version=2.3" (highest precedence).

Precedence (low→high): inventory defaults → inventory vars → group_vars → host_vars → play vars → extra vars.

4. Handlers & Templates

Handlers

Handlers are tasks that only run when notified. Use them to restart services only if configuration changes:

tasks:
  - name: Deploy config file
    template:
      src: foo.conf.j2
      dest: /etc/foo/foo.conf
    notify: Reload Foo

handlers:
  - name: Reload Foo
    service:
      name: foo
      state: reloaded

Templates

Ansible uses Jinja2 for templating. Example index.html.j2:

<!DOCTYPE html>
<html>
<head>
  <title>{{ site_title }}</title>
</head>
<body>
  <h1>Welcome to {{ inventory_hostname }}</h1>
  <p>Deployed at {{ ansible_date_time.iso8601 }}</p>
</body>
</html>

5. Roles & Project Layout

When your playbooks grow, adopt roles for modularity. Standard layout:

site.yml
inventory/
  production
  staging
group_vars/
  all.yml
  webservers.yml
host_vars/
  web01.yml
roles/
  common/
    tasks/
      main.yml
    handlers/
      main.yml
    templates/
      motd.j2
    files/
      bar.txt
    vars/
      main.yml
    defaults/
      main.yml
    meta/
      main.yml
  webserver/
    ...

Include a role in a play:

- hosts: web
  roles:
    - common
    - webserver

6. Worked Example: Deploying a LAMP Stack

Let’s write a complete playbook that:

  1. Installs Apache, MySQL, PHP (LAMP)
  2. Sets up a virtual host with a template
  3. Deploys a sample PHP app from Git
  4. Secures MySQL
  5. Ensures services are running

6.1 Inventory (inventory/production)

[webservers]
web01.example.com ansible_ssh_user=deploy

6.2 Variables (group_vars/webservers.yml)

---
site_title: "Hassan Agmir LAMP Site"
app_repo: "https://github.com/example/myapp.git"
mysql_root_password: "S3cur3P@ssw0rd"
vhost_conf: /etc/apache2/sites-available/myapp.conf
doc_root: /var/www/myapp

6.3 Playbook (site.yml)

---
- name: Configure LAMP servers
  hosts: webservers
  become: yes
  vars_files:
    - group_vars/webservers.yml

  pre_tasks:
    - name: Ensure apt cache is up to date
      apt:
        update_cache: yes
        cache_valid_time: 3600

  roles:
    - role: common      # installs utilities, sets timezone, etc.

  tasks:
    - name: Install LAMP packages
      apt:
        name:
          - apache2
          - mysql-server
          - php
          - libapache2-mod-php
          - php-mysql
          - git
        state: present

    - name: Clone application repository
      git:
        repo: "{{ app_repo }}"
        dest: "{{ doc_root }}"
        version: head

    - name: Deploy Apache virtual host
      template:
        src: vhost.j2
        dest: "{{ vhost_conf }}"
      notify: Reload Apache

    - name: Enable site
      command: a2ensite myapp.conf creates=/etc/apache2/sites-enabled/myapp.conf
      notify: Reload Apache

    - name: Disable default site
      command: a2dissite 000-default.conf removes=/etc/apache2/sites-enabled/000-default.conf
      notify: Reload Apache

    - name: Ensure document root is owned by www-data
      file:
        path: "{{ doc_root }}"
        owner: www-data
        group: www-data
        recurse: yes

    - name: Secure MySQL installation
      mysql_secure_installation:
        login_user: root
        login_password: ""
        root_password: "{{ mysql_root_password }}"
        remove_anonymous_user: yes
        disallow_root_login_remotely: yes
        remove_test_db: yes
      changed_when: mysql_root_password is defined

    - name: Ensure Apache is running
      service:
        name: apache2
        state: started
        enabled: yes

    - name: Ensure MySQL is running
      service:
        name: mysql
        state: started
        enabled: yes

  handlers:
    - name: Reload Apache
      service:
        name: apache2
        state: reloaded

6.4 Virtual Host Template (roles/webserver/templates/vhost.j2)

<VirtualHost *:80>
    ServerName {{ inventory_hostname }}
    DocumentRoot {{ doc_root }}

    <Directory "{{ doc_root }}">
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/myapp_error.log
    CustomLog ${APACHE_LOG_DIR}/myapp_access.log combined
</VirtualHost>

7. Advanced Features

7.1 Loops

- name: Install multiple packages
  apt:
    name: "{{ item }}"
    state: present
  loop:
    - htop
    - curl
    - unzip

7.2 Conditionals

- name: Only on Ubuntu
  debug:
    msg: "Running on Ubuntu"
  when: ansible_facts['os_family'] == "Debian"

7.3 Tags

Run only specific tasks:

ansible-playbook site.yml --tags "install,deploy"

In playbook:

- name: Install dependencies
  apt: ...
  tags: dependencies

7.4 Includes & Imports

  • import_playbook: static include of another playbook.
  • include_tasks: dynamic inclusion based on conditions.

8. Best Practices

  1. Idempotence: ensure tasks can run multiple times without side-effects.
  2. Use Roles: modularize common functionality.
  3. Secrets Management: use ansible-vault to encrypt passwords and keys.
  4. Linting: integrate ansible-lint in CI pipelines to catch errors and enforce style.
  5. Testing: use Molecule to test roles/playbooks in isolated environments (Docker, Vagrant).
  6. Documentation: document variables, default values, and prerequisites.

9. Testing with Molecule

Molecule simplifies role testing:

  1. Install: pip install molecule[docker].
  2. Initialize: molecule init role webserver --driver-name docker.
  3. Write tests in molecule/default/converge.yml.
  4. Run: molecule test.

This ensures your role works as expected before deploying to production.

Conclusion & Next Steps

Ansible Playbooks offer a declarative, human-readable approach to automation. By mastering:

  • Playbook structure and YAML syntax
  • Inventories, variables, and handlers
  • Roles for modular code
  • Advanced features like loops and conditionals
  • Best practices around idempotence, security, and testing

—You’ll be well-equipped to manage infrastructure at scale.

Next, explore:

  • Ansible Tower / AWX for a web UI and RBAC.
  • Dynamic inventories for auto-discovery.
  • Custom modules in Python for specialized tasks.

Hassan ❤️ YOU!

Subscribe to my Newsletters

Stay updated with the latest programming tips, tricks, and IT insights! Join my community to receive exclusive content on coding best practices.

© Copyright 2025 by Hassan Agmir . Built with ❤ by Me