Ansible Basics: Inventories, Playbooks and Idempotence

DevOps 9 min readPublished 26 September 2026

Quick answer

Learn Ansible basics through a practical Linux server lab. Build inventories, write playbooks, use handlers and troubleshoot common automation errors.

Ansible automates server configuration, application deployment and routine administration from a central control node. This guide explains Ansible basics through a practical lab covering inventories, ad-hoc commands, playbooks, variables, handlers and idempotent execution.

These skills are commonly used with Linux and cloud infrastructure. Learners who want structured practice with automation, AWS and CI/CD can explore the AWS DevOps course.

What is Ansible and how does it work?

Ansible is an open-source automation tool that describes the required state of systems in YAML files called playbooks. It normally connects to Linux managed nodes over SSH, runs modules on those nodes and returns the results without requiring a permanently installed Ansible agent.

A basic Ansible environment has these components:

ComponentPurpose
Control nodeThe computer where Ansible is installed and commands are started
Managed nodeA server, virtual machine or network device controlled by Ansible
InventoryA list or dynamic source of managed hosts and groups
ModuleCode that performs a task, such as installing a package
PlaybookA YAML file containing one or more plays and tasks
CollectionPackaged modules, plugins and roles for a platform or vendor

Diagram in words: Imagine a laptop called control on the left. SSH connections run from that laptop to three Ubuntu servers on the right: web1, web2 and db1. The control node reads an inventory, selects a host group, transfers module code, receives JSON results and then moves to the next task.

Ansible usually requires Python on POSIX managed nodes because many modules execute through Python. The ansible.builtin.raw module can run an initial command without Python when bootstrapping a minimal system. Windows management uses WinRM or SSH with Windows-specific modules rather than the standard Linux workflow.

How do you install Ansible on a control node?

Install Ansible on a supported Linux or macOS control node, or use a Linux environment such as WSL for a Windows workstation. A Python virtual environment or pipx keeps the Ansible installation separate from operating-system Python packages.

On Ubuntu, install pipx and then install the complete Ansible community package:

sudo apt update
sudo apt install -y pipx
pipx ensurepath
pipx install --include-deps ansible

Start a new shell if the command path was updated. Verify the installation:

ansible --version
ansible-playbook --version

The output shows the Ansible Core version, Python version, module paths and configuration file in use. The ansible package includes ansible-core and a set of community collections; ansible-core provides the command-line tools and built-in collection.

Create a lab directory:

mkdir -p ~/ansible-lab
cd ~/ansible-lab

The control node must be able to resolve or reach each managed node. Before using Ansible, confirm ordinary SSH access:

ssh ubuntu@192.0.2.21

The address 192.0.2.21 belongs to a documentation range. Replace it with the private IP address or resolvable hostname of your lab server.

What is an Ansible inventory?

An inventory identifies managed hosts and organises them into groups such as web servers, database servers or production systems. It can be a static INI or YAML file, or it can be generated dynamically from services such as AWS.

Create an INI inventory named inventory.ini:

[web]
web1 ansible_host=192.0.2.21
web2 ansible_host=192.0.2.22

[database]
db1 ansible_host=192.0.2.31

[linux:children]
web
database

[linux:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=~/.ssh/lab_key

This inventory creates two direct groups, web and database. The linux parent group includes both through the children relationship, and its variables apply to all three hosts.

Inspect how Ansible interprets the file:

ansible-inventory -i inventory.ini --graph
ansible-inventory -i inventory.ini --list

Expected graph structure:

@all:
  |--@ungrouped:
  |--@linux:
  |  |--@web:
  |  |  |--web1
  |  |  |--web2
  |  |--@database:
  |  |  |--db1

Inventory aliases such as web1 are Ansible names. The ansible_host variable tells Ansible which IP address or DNS name to contact.

For changing cloud environments, a dynamic inventory plugin can discover instances through an API. For example, the amazon.aws.aws_ec2 inventory plugin can group EC2 instances by tags, but it requires the amazon.aws collection, suitable AWS credentials and Python AWS SDK packages. First understand static inventories, then connect the workflow to concepts in Amazon EC2 instances, AMIs and security groups.

How do ad-hoc Ansible commands work?

An ad-hoc command runs one module directly against a host pattern and is useful for quick checks or one-time operations. Repeatable configuration should normally be stored in a playbook so that it can be reviewed and version-controlled.

Test connectivity to the web group:

ansible web -i inventory.ini -m ansible.builtin.ping

A successful result resembles:

web1 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

The Ansible ping module is not an ICMP ping. It checks whether Ansible can connect, find a usable Python interpreter and execute a small module on the managed node.

Collect operating-system information:

ansible linux -i inventory.ini -m ansible.builtin.setup \
  -a 'filter=ansible_distribution*'

Check uptime using the command module:

ansible linux -i inventory.ini -m ansible.builtin.command \
  -a 'uptime'

Use --become when a task needs privilege escalation:

ansible web -i inventory.ini --become \
  -m ansible.builtin.apt -a 'name=nginx state=present update_cache=yes'

Do not use ansible.builtin.shell unless shell features such as pipes, redirects or variable expansion are required. The command module avoids interpretation by a shell and therefore reduces quoting and security problems.

What is an Ansible playbook?

A playbook is a YAML document that maps host groups to ordered tasks. Each task calls a module with arguments that describe an action or required state.

Create webserver.yml:

---
- name: Configure web servers
  hosts: web
  become: true
  gather_facts: true

  vars:
    web_package: nginx
    web_service: nginx

  tasks:
    - name: Install Nginx
      ansible.builtin.apt:
        name: "{{ web_package }}"
        state: present
        update_cache: true
        cache_valid_time: 3600

    - name: Publish training page
      ansible.builtin.copy:
        dest: /var/www/html/index.html
        owner: root
        group: root
        mode: '0644'
        content: |
          Ansible managed this server.
      notify: Restart Nginx

    - name: Ensure Nginx is enabled and running
      ansible.builtin.service:
        name: "{{ web_service }}"
        state: started
        enabled: true

  handlers:
    - name: Restart Nginx
      ansible.builtin.service:
        name: "{{ web_service }}"
        state: restarted

Check YAML and basic playbook syntax before execution:

ansible-playbook -i inventory.ini webserver.yml --syntax-check

Run the playbook:

ansible-playbook -i inventory.ini webserver.yml

The play starts on hosts in the web group. Facts provide information such as the distribution, interfaces and memory; the tasks then install Nginx, copy content and ensure the service is enabled and running.

Fully qualified collection names such as ansible.builtin.copy make it clear which module is used. Module documentation and examples can be viewed locally:

ansible-doc ansible.builtin.copy
ansible-doc ansible.builtin.apt

What does idempotent automation mean?

An idempotent task moves a system to the requested state without making unnecessary changes when that state already exists. Running the same well-designed playbook twice should leave the second run mostly or entirely unchanged.

For example, this task does not reinstall Nginx every time:

- name: Ensure Nginx is installed
  ansible.builtin.apt:
    name: nginx
    state: present

The module inspects package state. If Nginx is absent, the result is changed; if it is already installed, the result is ok.

Typical result colours and states are:

ResultMeaning
okThe task succeeded and made no change
changedThe task succeeded and changed the managed node
failedThe task could not complete
skippedA condition or host state prevented execution
unreachableAnsible could not connect to the host

Run the example twice and compare the recap:

PLAY RECAP
web1 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web2 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Idempotence is not automatic for every task. A command such as the following appends the same line on every run:

- name: Append a line using a shell
  ansible.builtin.shell: echo 'APP_MODE=production' >> /etc/environment

Use a state-aware module instead:

- name: Set application mode once
  ansible.builtin.lineinfile:
    path: /etc/environment
    regexp: '^APP_MODE='
    line: APP_MODE=production
    create: true
    mode: '0644'

When a command is unavoidable, creates or removes can provide a simple guard:

- name: Initialise application data once
  ansible.builtin.command: /opt/app/bin/init-data
  args:
    creates: /opt/app/data/.initialised

The command runs only if the marker path does not exist. The command itself must reliably create that marker after successful initialisation.

Why are handlers useful in playbooks?

Handlers run only when notified by a task that reports a change. They prevent unnecessary service restarts and normally execute once at the end of the play, even if multiple tasks notify the same handler.

In the web server example, changing index.html notifies Restart Nginx. An unchanged file returns ok, so no notification is queued. For static page content, a restart is not technically necessary, but the example clearly demonstrates handler behaviour; configuration files such as /etc/nginx/nginx.conf are a more typical use.

If a handler must run before later tasks, flush queued handlers explicitly:

- name: Apply queued handlers now
  ansible.builtin.meta: flush_handlers

Use this only when following tasks depend on the restarted or reloaded service. Otherwise, allowing handlers to run at the end reduces disruption.

How can you test a playbook safely?

Use syntax checking, check mode, diff output and a limited host selection before applying a playbook widely. Check mode predicts many changes, but it cannot perfectly simulate modules or commands that do not support it.

Start with these commands:

ansible-playbook -i inventory.ini webserver.yml --syntax-check
ansible-playbook -i inventory.ini webserver.yml --check
ansible-playbook -i inventory.ini webserver.yml --check --diff
ansible-playbook -i inventory.ini webserver.yml --limit web1

--diff can reveal previous and proposed file content. Avoid using it where files contain passwords, tokens or private keys, because sensitive data may appear in terminal output or CI logs.

A practical promotion sequence is:

  1. Validate syntax.
  2. Run --check --diff in a non-production lab.
  3. Apply to one test host with --limit.
  4. Verify the service and application response.
  5. Apply to the remaining group.
  6. Run the playbook again and inspect idempotence.

Playbooks can also be stored in Git and launched from CI/CD. The Jenkins declarative pipeline guide explains pipeline stages and agents that can be used around validation and deployment jobs.

How do you troubleshoot common Ansible errors?

Troubleshoot Ansible by separating inventory, network, SSH, privilege and module problems. Use verbose output and test the underlying connection directly instead of changing several playbook settings at once.

Host is unreachable

Example:

UNREACHABLE! => Failed to connect to the host via ssh

Check the resolved inventory data and test SSH:

ansible-inventory -i inventory.ini --host web1
ssh -i ~/.ssh/lab_key ubuntu@192.0.2.21
ansible web1 -i inventory.ini -m ansible.builtin.ping -vvv

Verify the IP address, route, security group or firewall, SSH username and private-key permissions. An SSH private key commonly requires restrictive permissions:

chmod 600 ~/.ssh/lab_key

Python interpreter is missing

A minimal target may not have Python installed. Bootstrap it on Ubuntu with the raw module:

ansible web1 -i inventory.ini -m ansible.builtin.raw \
  -a 'sudo apt-get update && sudo apt-get install -y python3'

After installation, run the ping module again. Set ansible_python_interpreter only when interpreter discovery selects the wrong path.

Sudo requires a password

If the remote account is allowed to use sudo but needs a password, prompt for it:

ansible-playbook -i inventory.ini webserver.yml --ask-become-pass

In managed environments, configure privilege escalation according to security policy rather than embedding a sudo password in plain-text inventory files. Ansible Vault can encrypt variable files, but access should still follow least-privilege principles.

YAML syntax or indentation fails

YAML uses spaces and indentation to represent structure. Do not use tabs, and align module arguments beneath the module name.

Run:

ansible-playbook -i inventory.ini webserver.yml --syntax-check

If the reported line looks correct, inspect the lines immediately above it. An unclosed quote or incorrectly indented list item can cause the parser to report a later line.

A task changes on every run

Run the playbook with --diff and inspect the module result using -vv. Common causes include timestamps embedded in generated files, random values, unconditional shell commands and templates whose source changes every execution.

Replace shell logic with modules such as copy, template, file, user, package, service or lineinfile. If change is expected, document why rather than hiding it with changed_when: false.

What should you learn after these Ansible basics?

After inventories and playbooks, learn variable precedence, templates, roles, Ansible Vault, collections and dynamic cloud inventory. Then practise testing, source control and CI/CD integration so that automation is reviewable and safe to operate.

A useful next lab is to convert the Nginx play into a role with tasks, handlers, templates and defaults directories. You can then create separate development and production inventories while reusing the same role.

Summary

Ansible uses a control node, inventory and modules to manage remote systems, usually through SSH. Playbooks define repeatable tasks, while state-aware modules and handlers help make automation idempotent and avoid unnecessary changes.

For reliable automation, validate syntax, use check mode carefully, test on a limited host and run the playbook twice. When troubleshooting, confirm inventory values, direct SSH access, Python availability and privilege escalation in that order.

To practise Ansible with AWS, Linux, Git, Jenkins and deployment workflows, enquire about batch details for the AWS DevOps course.

*Reviewed by Network Rhinos DevOps trainers.*

Frequently asked questions

Does Ansible require an agent on managed Linux servers?

No. Ansible normally connects to Linux managed nodes over SSH and does not require a permanently running agent. Most Ansible modules require a usable Python interpreter on the managed node.

What is the difference between an inventory and a playbook?

An inventory defines the hosts, groups and connection variables that Ansible can use. A playbook defines the tasks and desired configuration that Ansible applies to selected inventory hosts.

Why is idempotence important in Ansible?

Idempotence allows a playbook to be run repeatedly without making unnecessary changes. It makes automation safer, easier to audit and more suitable for correcting configuration drift.

What is the difference between the command and shell modules?

The command module runs a program without processing it through a shell. The shell module supports pipes, redirects and other shell features, but it should be used only when those features are required.

Can Ansible check a playbook without changing servers?

Yes. Run `ansible-playbook playbook.yml --check` to request check mode, and add `--diff` to inspect supported file changes. The result is a prediction because not every module or command can be fully simulated.

Can Ansible manage AWS EC2 instances?

Yes. Ansible can configure EC2 operating systems over SSH and use AWS collections to interact with AWS APIs. The `amazon.aws.aws_ec2` dynamic inventory plugin can also discover instances using attributes and tags.

Related articles

Train with Network Rhinos

Hands-on CCNA, CCNP, AWS, Azure, DevOps and cybersecurity training in Chennai & Bangalore, with placement support. Talk to our team or attend a free demo class.