Saturday, 6 December 2025

Ansible 12. Variables - list variables - some demonstration

Below Ansible playbook, named ansible-variable-list.yml, performs a check on the target servers to determine if a specific list of operating system users exists.

[oracle@oel01db ansible-project]$ cat ./playbooks/ansible-variable-list.yml

---

- name: Check for existence of specified users

  hosts: db_servers 

  vars:

    users: ["tom", "mysql", "oracle", "postgres"]

  tasks:

    - name: Check if user exists using the 'id' command

      # The loop iterates over the 'users' list

      ansible.builtin.command: "id -u {{ item }}"

      # Register the output of the command (success or failure)

      register: user_check

      # Ensure the task doesn't fail if the user is not found (command returns non-zero)

      ignore_errors: true

      loop: "{{ users }}"

    - name: Report on user existence

      ansible.builtin.debug:

        msg: "User '{{ item.item }}' {{ 'EXISTS' if item.rc == 0 else 'DOES NOT EXIST' }} on {{ inventory_hostname }}"

      # Loop over the results registered from the previous task

      loop: "{{ user_check.results }}"

      # Only show results for the users we checked

      loop_control:

       label: "{{ item.item }}"

[oracle@oel01db ansible-project]$ ansible-playbook -i ./inventory/hosts ./playbooks/ansible-variable-list.yml

PLAY [Check for existence of specified users] ***********************************************************************************************************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************************************************************************************************************************************

ok: [192.168.0.156]

TASK [Check if user exists using the 'id' command] ******************************************************************************************************************************************************************************************

failed: [192.168.0.156] (item=tom) => {"ansible_loop_var": "item", "changed": true, "cmd": ["id", "-u", "tom"], "delta": "0:00:00.009775", "end": "2025-12-05 20:44:23.561919", "item": "tom", "msg": "non-zero return code", "rc": 1, "start": "2025-12-05 20:44:23.552144", "stderr": "id: tom: no such user", "stderr_lines": ["id: tom: no such user"], "stdout": "", "stdout_lines": []}

failed: [192.168.0.156] (item=mysql) => {"ansible_loop_var": "item", "changed": true, "cmd": ["id", "-u", "mysql"], "delta": "0:00:00.009717", "end": "2025-12-05 20:44:24.328979", "item": "mysql", "msg": "non-zero return code", "rc": 1, "start": "2025-12-05 20:44:24.319262", "stderr": "id: mysql: no such user", "stderr_lines": ["id: mysql: no such user"], "stdout": "", "stdout_lines": []}

changed: [192.168.0.156] => (item=oracle)

changed: [192.168.0.156] => (item=postgres)

...ignoring

TASK [Report on user existence] *************************************************************************************************************************************************************************************************************

ok: [192.168.0.156] => (item=tom) => {

    "msg": "User 'tom' DOES NOT EXIST on 192.168.0.156"

}

ok: [192.168.0.156] => (item=mysql) => {

    "msg": "User 'mysql' DOES NOT EXIST on 192.168.0.156"

}

ok: [192.168.0.156] => (item=oracle) => {

    "msg": "User 'oracle' EXISTS on 192.168.0.156"

}

ok: [192.168.0.156] => (item=postgres) => {

    "msg": "User 'postgres' EXISTS on 192.168.0.156"

}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************

192.168.0.156              : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1

[oracle@oel01db ansible-project]$


Lets break down of the steps.

1. You define a list variable

vars: users: ["tom", "mysql", "oracle", "postgres"]

This creates a list:

IndexValue
0tom
1mysql
2oracle
3postgres

2. First task loops over the users list

ansible.builtin.command: "id -u {{ item }}" loop: "{{ users }}"

Meaning:

Ansible runs the command once for each userSo internally, it does:

id -u tom id -u mysql id -u oracle id -u postgres

Each iteration sets item to the current user.


3. Register collects output from ALL loop items

register: user_check

This is very important.

After the task finishes, this variable contains a structure like:

user_check: results: - item: "tom" rc: 1 stdout: "" stderr: "id: tom: no such user" - item: "mysql" rc: 1 stderr: "id: mysql: no such user" - item: "oracle" rc: 0 stdout: "1001" - item: "postgres" rc: 0 stdout: "26"

So register produces a list of results, one for each iteration.

When a task with a loop is registered, the variable (user_check) isn't a simple list; it's a dictionary containing a key called results. The value of user_check.results is a list of dictionaries, one for each iteration of the loop.


⚠️ 4. ignore_errors: true

ignore_errors: true

When a user does NOT exist, the command fails (rc=1), but because you set this, Ansible continues running the playbook.


5. Second task loops over the results

loop: "{{ user_check.results }}"

Now, instead of looping over the users list, it loops over the results from the previous task.

Each item looks like this inside the loop:

{ "item": "tom", "rc": 1, "stderr": "id: tom: no such user" }
Now item is a dictionary because each element of user_check.results is a dictionary.

So in the second task:

Loop iterationitem value
1{ item: "tom", rc:1, ... }
2{ item: "mysql", rc:1, ... }
3{ item: "oracle", rc:0, ... }
4{ item: "postgres", rc:0, ... }

⚠️ Now item ≠ username
Instead, item = whole dictionary

In the second loop, the default item variable doesn't hold a single string; it holds a dictionary that contains the full details of a single execution of the previous task.

So this line evaluates:

'EXISTS' if item.rc == 0 else 'DOES NOT EXIST'

Meaning:

  • rc=0 → user exists

  • rc!=0 → user does not exist


🎯 6. Final output message for each user

Example:

  • For tom → rc=1 → "DOES NOT EXIST"

  • For mysql → rc=1 → "DOES NOT EXIST"

  • For oracle → rc=0 → "EXISTS"

  • For postgres → rc=0 → "EXISTS"


🎛️ 7. loop_control just makes output pretty

loop_control defines how to display or manage information during the loop.

loop_control: label: "{{ item.item }}"

This controls what appears in the output logs.

Without it, Ansible prints very long JSON lines.

With it, you see clean output:

ok: [192.168.0.156] => (item=tom)

Final Summary – How the loop concept works

First loop

➡️ Loops over usernames
➡️ Runs id
➡️ Stores all outputs in user_check.results

Second loop

➡️ Loops over the results of first loop
➡️ Prints EXISTS / DOES NOT EXIST

Logically the loop command  belongs right after the module callbut in YAML the order does NOT matter as long as indentation is correct.

Why item.item?

Remember:

  • First item = loop variable in second task

  • Second item = username from first task

Because each element in user_check.results looks like:

{ item: "tom", rc: 1, stdout: "", stderr: "id: tom: no such user" }

So inside second task:

  • item → whole dictionary

  • item.item → the original username


item is the default variable name that Ansible uses inside any loop construct.

When you use the loop: keyword in an Ansible task, the current item being processed in that loop is automatically assigned to the variable item.

🔁 Key Points about item

  • Default Name: If you don't specify a custom variable name using loop_control: loop_var:, Ansible always uses item.

  • Loop Data: It holds the actual data value from the list you are iterating over.

    • Example: If you loop over [a, b, c], item will be a, then b, then c.

  • Result Data (Complex Loops): When you loop over the results of a previously registered task (like user_check.results), item holds the entire dictionary structure for that iteration, which is why you access nested data using dots, such as item.rc or item.item.

In your original playbook:

  • In the first task (ansible.builtin.command: "id -u {{ item }}"), item held the usernames (tom, mysql, etc.).

  • In the below task, when you switched the name to user_result using loop_control: loop_var: user_result, you were essentially telling Ansible: "Don't use the default item; use user_result for the current loop data instead."


[oracle@oel01db ansible-project]$ cat ./playbooks/ansible-variable-list.yml
---
- name: Check for existence of specified users
  hosts: db_servers # Target your desired group
  gather_facts: no # Not strictly needed for this check, speeds up execution
  become: false # No elevated permissions needed for the 'id' check itself

  vars:
    # 1. Define the list variable
    users: ["tom", "mysql", "oracle", "postgres"]

  tasks:
    - name: Check if user exists using the 'id' command
      ansible.builtin.command: "id -u {{ item }}"
      # Register the output of the command (success or failure)
      register: user_check
      # Ensure the task doesn't fail if the user is not found (rc=1)
      ignore_errors: true
      loop: "{{ users }}"

    - name: Report on user existence
      ansible.builtin.debug:
        # 2. Use the custom loop variable 'user_result' for all lookups
        msg: "User '{{ user_result.item }}' {{ 'EXISTS' if user_result.rc == 0 else 'DOES NOT EXIST' }} on {{ inventory_hostname }}"

      # Loop over the results registered from the previous task
      loop: "{{ user_check.results }}"

      # 3. Define the custom loop variable and label with correct indentation
      loop_control:
        loop_var: user_result # <-- Sets the loop variable name
        label: "{{ user_result.item }}"
[oracle@oel01db ansible-project]$
[oracle@oel01db ansible-project]$
[oracle@oel01db ansible-project]$ ansible-playbook -i ./inventory/hosts ./playbooks/ansible-variable-list.yml

PLAY [Check for existence of specified users] ***********************************************************************************************************************************************************************************************

TASK [Check if user exists using the 'id' command] ******************************************************************************************************************************************************************************************
failed: [192.168.0.156] (item=tom) => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "ansible_loop_var": "item", "changed": true, "cmd": ["id", "-u", "tom"], "delta": "0:00:00.009488", "end": "2025-12-05 21:28:06.222495", "item": "tom", "msg": "non-zero return code", "rc": 1, "start": "2025-12-05 21:28:06.213007", "stderr": "id: tom: no such user", "stderr_lines": ["id: tom: no such user"], "stdout": "", "stdout_lines": []}
failed: [192.168.0.156] (item=mysql) => {"ansible_loop_var": "item", "changed": true, "cmd": ["id", "-u", "mysql"], "delta": "0:00:00.012842", "end": "2025-12-05 21:28:07.015244", "item": "mysql", "msg": "non-zero return code", "rc": 1, "start": "2025-12-05 21:28:07.002402", "stderr": "id: mysql: no such user", "stderr_lines": ["id: mysql: no such user"], "stdout": "", "stdout_lines": []}
changed: [192.168.0.156] => (item=oracle)
changed: [192.168.0.156] => (item=postgres)
...ignoring

TASK [Report on user existence] *************************************************************************************************************************************************************************************************************
ok: [192.168.0.156] => (item=tom) => {
    "msg": "User 'tom' DOES NOT EXIST on 192.168.0.156"
}
ok: [192.168.0.156] => (item=mysql) => {
    "msg": "User 'mysql' DOES NOT EXIST on 192.168.0.156"
}
ok: [192.168.0.156] => (item=oracle) => {
    "msg": "User 'oracle' EXISTS on 192.168.0.156"
}
ok: [192.168.0.156] => (item=postgres) => {
    "msg": "User 'postgres' EXISTS on 192.168.0.156"
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
192.168.0.156              : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1

[oracle@oel01db ansible-project]$

We can also define variable like below 

  vars:
    users:
      - tom
      - mysql
      - oracle
      - postgres



No comments:

Post a Comment

Building a Safer PostgreSQL CI/CD Pipeline with GitHub Actions: Dev → PR Review → Test Promotion

In my previous post, we explored a simple push-to-main deployment strategy . While functional, that model is not considered an industry best...