# Install Ansible and Write Your First Playbook: Inventory, Modules and Roles

> Install Ansible with pipx, define an inventory, run ad hoc commands, write an idempotent playbook with variables, handlers and templates, and organize it in a role.

- Source: https://www.itwonderlab.com/install-ansible-first-playbook/
- Published: 2026-09-10
- Updated: 2026-09-10
- Author: Javier Ruiz Jiménez (https://www.javierruizjimenez.com/)
- Site: IT Wonder Lab (https://www.itwonderlab.com/)

---

## What you will build

[Ansible](https://www.itwonderlab.com/ansible/) connects to servers over SSH and applies a desired state described in YAML. You need nothing installed on the servers except Python and SSH. In this tutorial you install Ansible, write an inventory, run a command on all hosts and write a playbook that installs and configures [NGINX](https://www.itwonderlab.com/nginx/).

For test servers, create three VMs with [Vagrant and VirtualBox](https://www.itwonderlab.com/install-vagrant-virtualbox/) or use EC2 instances created with [Terraform](https://www.itwonderlab.com/terraform-ansible-aws-howto/).

## Install

On the control machine (your laptop or a bastion). `pipx` keeps Ansible isolated:

```bash
sudo apt update && sudo apt install -y pipx
pipx ensurepath
pipx install --include-deps ansible
ansible --version
```

On macOS: `brew install ansible`. Windows is supported only as the control node through WSL 2.

## Inventory

The inventory lists the hosts, grouped:

```ini title="inventory.ini"
[web]
node1 ansible_host=192.168.56.11
node2 ansible_host=192.168.56.12

[db]
node3 ansible_host=192.168.56.13

[all:vars]
ansible_user=vagrant
ansible_ssh_private_key_file=~/.ssh/id_ed25519
```

For cloud servers that change all the time, see [dynamic inventory](https://www.itwonderlab.com/ansible-dynamic-inventory/). Authentication uses SSH keys, as in [public key authentication](https://www.itwonderlab.com/public-key-authentication/).

## Ad hoc commands

```bash
ansible all -i inventory.ini -m ping
ansible web -i inventory.ini -m command -a "uptime"
ansible web -i inventory.ini -b -m apt -a "name=htop state=present update_cache=true"
```

`-m` selects the **module**, `-a` its arguments and `-b` (become) uses sudo.

## Your first playbook

```yaml title="web.yml"
- name: Configure web servers
  hosts: web
  become: true
  vars:
    site_title: "Hello from Ansible"

  tasks:
    - name: Install NGINX
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true

    - name: Publish the home page
      ansible.builtin.template:
        src: index.html.j2
        dest: /var/www/html/index.html
        mode: "0644"
      notify: Reload NGINX

    - name: Make sure NGINX is running and enabled
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Reload NGINX
      ansible.builtin.service:
        name: nginx
        state: reloaded
```

```html title="templates/index.html.j2"
<h1>{{ site_title }}</h1>
<p>Served by {{ inventory_hostname }}</p>
```

Run it:

```bash
ansible-playbook -i inventory.ini web.yml
```

The result shows `changed` for what Ansible modified. Run it a second time: everything is `ok` and nothing changes. That is **idempotence**, the main property of a good playbook. Use `--check --diff` to preview the changes without applying them.

## Handlers, variables and conditions

- A **handler** runs once at the end, and only if a task notified it (the reload happens only when the page changed).
- Variables come from the playbook, the inventory (`group_vars/` and `host_vars/`) or `-e name=value`.
- `when: ansible_facts['os_family'] == "Debian"` restricts a task. `loop:` repeats it.
- Encrypt secrets with `ansible-vault encrypt_string`.

## Move it to a role

When a playbook grows, split it into a role:

```bash
ansible-galaxy init roles/nginx
```

Put the tasks in `roles/nginx/tasks/main.yml`, the template in `roles/nginx/templates/`, the handler in `roles/nginx/handlers/main.yml` and the defaults in `roles/nginx/defaults/main.yml`. Then the playbook becomes:

```yaml
- hosts: web
  become: true
  roles:
    - nginx
```

Follow the [roles best practices](https://www.itwonderlab.com/ansible-roles-best-practices/) and the [playbook structure](https://www.itwonderlab.com/ansible-playbook-structure-best-practices/), and keep several [environments](https://www.itwonderlab.com/ansible-multiple-environment-best-practices/) with separate inventories.

## Next steps

Build a complete Kubernetes cluster with the [Ansible and Vagrant tutorial](https://www.itwonderlab.com/ansible-kubernetes-vagrant-tutorial/).
