Write Idempotent Playbooks with Common Modules

AnsibleBeginner
Practice Now

Introduction

Idempotence means a playbook can be run repeatedly and only changes the system when the desired state is missing or different.

In this lab, you will use common Ansible modules to create directories and configuration files for host aliases, then run the playbook twice to prove the second run is stable.

Create an Idempotence Practice Project

In this step, you will create a small Ansible project with two local managed host aliases. Each alias writes to its own directory so you can see host-specific results while staying inside one RHEL 10 VM.

Start from the default LabEx workspace.

cd /home/labex/project

The option -p tells mkdir to create parent directories when needed and to succeed if a directory already exists.

mkdir -p rhce-ansible/{inventory/group_vars,playbooks,managed,reports}
cd rhce-ansible

Create project defaults that point Ansible at your inventory and keep retry files disabled.

cat > ansible.cfg <<'EOF'
[defaults]
inventory = ./inventory/hosts
host_key_checking = False
retry_files_enabled = False
EOF

The managed directory will hold the files produced by the playbook, while reports will hold saved playbook output. Keeping those separate makes it easier to compare desired content with Ansible's run evidence.

Define Local Managed Hosts and Variables

In this step, you will create an inventory and group variables. The aliases use ansible_connection=local, which means Ansible targets named hosts while executing safely on this VM. Before running commands in this step, return to the Ansible project directory. This keeps the commands stable even when LabEx runs each step separately.

cd /home/labex/project/rhce-ansible

Create two app host aliases in one group.

cat > inventory/hosts <<'EOF'
[app]
app1 ansible_connection=local
app2 ansible_connection=local
EOF

Group variables describe the desired state shared by every host in the group.

cat > inventory/group_vars/app.yml <<'EOF'
app_owner: labex
app_environment: training
app_message: Managed by Ansible common modules
EOF

Confirm that Ansible can see the two hosts.

ansible app --list-hosts

The output should list app1 and app2. This check is useful before writing a playbook because a play targeting app can only affect hosts that inventory resolves into that group.

Write a Desired-State Playbook

In this step, you will write a playbook that uses file, copy, and lineinfile. These modules describe the final state instead of manually running shell commands. Before running commands in this step, return to the Ansible project directory. This keeps the commands stable even when LabEx runs each step separately.

cd /home/labex/project/rhce-ansible

Create the playbook. The paths include {{ inventory_hostname }} so each alias gets its own output directory.

cat > playbooks/common-modules.yml <<'EOF'
---
- name: Configure app host directories with common modules
  hosts: app
  gather_facts: false
  tasks:
    - name: Ensure host directory exists
      ansible.builtin.file:
        path: "/home/labex/project/rhce-ansible/managed/{{ inventory_hostname }}"
        state: directory
        mode: '0755'

    - name: Install base application configuration
      ansible.builtin.copy:
        dest: "/home/labex/project/rhce-ansible/managed/{{ inventory_hostname }}/app.conf"
        content: |
          owner={{ app_owner }}
          environment={{ app_environment }}
          message={{ app_message }}
        mode: '0644'

    - name: Ensure managed message is present
      ansible.builtin.lineinfile:
        path: "/home/labex/project/rhce-ansible/managed/{{ inventory_hostname }}/app.conf"
        line: "message={{ app_message }}"
        create: true
        mode: '0644'
EOF

The file task creates the directory, the copy task writes the complete configuration, and the lineinfile task demonstrates how to enforce a single line. These modules compare current state with desired state, which is why they can be idempotent.

Use syntax check before running a new playbook.

ansible-playbook playbooks/common-modules.yml --syntax-check

Run the Playbook and Inspect the Files

In this step, you will run the desired-state playbook for the first time. The first run should report changes because the target files do not exist yet. Before running commands in this step, return to the Ansible project directory. This keeps the commands stable even when LabEx runs each step separately.

cd /home/labex/project/rhce-ansible

Run the playbook and save the output for later comparison.

ansible-playbook playbooks/common-modules.yml | tee reports/first-run.txt

The first run should report changes because the host directories and app.conf files are new. tee lets you watch the run while also saving the same output for the idempotence check.

Inspect the host-specific files that Ansible created.

cat managed/app1/app.conf managed/app2/app.conf

Both files should contain the same owner, environment, and message values, but they live under different host alias directories. That shows the playbook applied the same desired state to each inventory host.

Run the Playbook Again to Prove Idempotence

In this step, you will run the same playbook again. Because the files already match the desired state, Ansible should report zero changes for both hosts. Before running commands in this step, return to the Ansible project directory. This keeps the commands stable even when LabEx runs each step separately.

cd /home/labex/project/rhce-ansible

Run the playbook a second time and save the output.

ansible-playbook playbooks/common-modules.yml | tee reports/second-run.txt

Look for changed=0 in the recap lines.

grep -E 'app[12].*changed=0' reports/second-run.txt

changed=0 on the second run is the key idempotence signal. It means the modules inspected the existing files and found that no repair was needed.

Summary

You built an Ansible project, used common modules to describe desired state, ran the playbook, and proved idempotence with a second run that reported no changes.