How to NOT print items in an Ansible loop error without no_log

ansible

When I execute an ansible module with a loop that has dictionaries with values to be used by the module, including confidential information, I can hide it using loop_control.label, but not when an error happens.

- ansible.module:
    arg1: "{{ item.name }}"
    arg2: "{{ item.some_value }}"
    arg3: "{{ item.secret }}"
    arg4: "{{ item.throw_error }}"
  loop: 
    - name: "item1"
      some_value: "my value"
      secret: "p4$$w0rd"
  loop_control:
    label: "{{ item.name }}"

I can hide it using no_log: true, but it then hides everything, including when a sucess happens, as well as the error message, that could be perfectly fine, and making me clueless about the actual error when I don't see it.

This is similar to stop all logs in a reverse proxy, or log everything, including authorization headers, both of which are far from desired in a production environment.

So, is there a way to make it not log the loop items when an error occurs, but log everything else? It could be either a task flag, just like no_log, or some global configuration, to include in the ansible.cfg file.

Update (2021-04-19)

The answer from Vladimir actually works as a workaround, but there are several downsides in using it:

  1. You have to install the library jmespath prior to running json_query filter.
  2. It has more code and make the code as a whole less readable.
  3. If you have lots of loops (my specific project has over 80 of them), the code can become really ugly (also because of point 2).
  1. If you have a loop in a parent file, you should use loop_control with loop_var in the debug loop.
  2. The error and where it shows the error message will be different places. The same applies for success messages.
  3. The loop label in the debug task will be different than the one in the main loop task (which may make it more difficult to associate success and error cases to the actual items).
  4. The 2 tasks will be printed unnecessarily when everything is fine (with success messages printed separately from the actual tasks, making the output worse when everything runs successfully, which should be most cases).
  5. Changing and testing the existing loop tasks, as well as making any new loops follow this format, will be a maintenance burden (especially because of point 2, that makes me think if I'm actually improving the code, or making it worse).

(and there are probably more downsides)

So the best I could say, is that this is a workaround that solves a problem and creates several others.

That said, I asked for a solution, and based on how Ansible works at the moment, the provided workaround (or some similar workaround) seems to be the only way to achieve what I asked.

I will weigh the pros and cons to see if I will implement this workaround, or if I continue to use it the way it is now.

Best Answer

There is a way to achieve desired behavior in multuple tasks with ansible's error handling. You can register output of no_log task and print only non-secret part in subsequent debug:

- hosts: localhost
  connection: local
  tasks:
  - block:
    - shell: "/bin/false"
      loop:
      - name: "item1"
        some_value: "my value"
        secret: "p4$$w0rd"
      - name: "item2"
        some_value: "my value"
        secret: "p4$$w0rd"
      loop_control:
        label: "{{ item.name }}"
      # Capture tasks output
      register: my_task
      no_log: true

    # Run this if task above fails
    rescue:

    # Print msg, stderr or whatever needed
    - debug:
        msg: "{{ item }}"
      loop: "{{ my_task | json_query('results[*].msg') }}"

    # finally fail a play
    - fail:

In case if you need always print output (not only on task failure) use always instead of rescue and fail task on condition:

- hosts: localhost
  connection: local
  tasks:
  - block:
    - shell: "/bin/true"
      loop:
      - name: "item1"
        some_value: "my value"
        secret: "p4$$w0rd"
      - name: "item2"
        some_value: "my value"
        secret: "p4$$w0rd"
      loop_control:
        label: "{{ item.name }}"
      register: my_task
      no_log: true

    always:
    - debug:
        msg: "{{ item }}"
      loop: "{{ my_task | json_query('results[*].msg') }}"

    - fail:
      when: my_task['failed'] | default(false)

Update (2021-04-20)

As pointed by Lucas code above have number of downsides. Main idea was that output can be registered and filtered afterwards. Other parts of code are opinionated examples. There is certainly room for improvement. For example here is code that addresses issues 1, 4, 6, (and partially 2):

- hosts: localhost
  connection: local
  tasks:
  - block:
    - shell: "/bin/true"
      register: my_task
      no_log: true
      # Register all loop-related keys so same loop settings can be reused in debug
      <<: &loop
        loop:
        - name: "item1"
          some_value: "my value"
          secret: "p4$$w0rd"
        - name: "item2"
          some_value: "my value"
          secret: "p4$$w0rd"
        loop_control:
          label: "{{ item.name }}"
          index_var: index

    always:
    - debug:
        # match result with item by index
        msg: "{{ my_task['results'][index]['msg'] | default('ok') }}"
      # reuse loop from main task
      <<: *loop

    - fail:
      when: my_task['failed'] | default(false)

As for now it seems there is no way to implement desired behavior w/o workarounds. Ansible recommends writing logs to secure location in case it contains sensitive data.