Ansible map Filter — Transform Lists and Extract Attributes

Introduction

The map filter applies a transformation to every element in a list — extract an attribute, apply another filter, or call a test. It's how you go from a list of dictionaries to a list of specific values, or transform all strings in a list at once.

Extract Attribute from List of Dicts

vars:
  users:
    - name: alice
      uid: 1001
      role: admin
    - name: bob
      uid: 1002
      role: developer
    - name: carol
      uid: 1003
      role: developer

tasks:
  - name: Get all usernames
    ansible.builtin.debug:
      msg: "{{ users | map(attribute='name') | list }}"
    # Output: ["alice", "bob", "carol"]

  - name: Get all UIDs
    ansible.builtin.debug:
      msg: "{{ users | map(attribute='uid') | list }}"
    # Output: [1001, 1002, 1003]

Apply Filter to Each Element

vars:
  hostnames:
    - Web-Server-01
    - DB-Server-01
    - Cache-Server-01

tasks:
  - name: Convert all to lowercase
    ansible.builtin.debug:
      msg: "{{ hostnames | map('lower') | list }}"
    # Output: ["web-server-01", "db-server-01", "cache-server-01"]

  - name: Add prefix to each
    ansible.builtin.debug:
      msg: "{{ hostnames | map('regex_replace', '^', 'prod-') | list }}"
    # Output: ["prod-Web-Server-01", "prod-DB-Server-01", "prod-Cache-Server-01"]

Common map Patterns

Extract Nested Attributes

vars:
  servers:
    - name: web01
      network:
        ip: 10.0.1.10
        subnet: 10.0.1.0/24
    - name: web02
      network:
        ip: 10.0.1.11
        subnet: 10.0.1.0/24

tasks:
  - name: Get all IPs (nested attribute)
    ansible.builtin.debug:
      msg: "{{ servers | map(attribute='network.ip') | list }}"
    # Output: ["10.0.1.10", "10.0.1.11"]

Map with int/string Conversion

vars:
  port_strings: ["8080", "8443", "9090"]

tasks:
  - name: Convert to integers
    ansible.builtin.debug:
      msg: "{{ port_strings | map('int') | list }}"
    # Output: [8080, 8443, 9090]

Map + Join (Build Strings)

vars:
  packages:
    - nginx
    - postgresql
    - redis

tasks:
  - name: Build comma-separated list
    ansible.builtin.debug:
      msg: "{{ packages | join(', ') }}"
    # Output: "nginx, postgresql, redis"

  - name: Build install command
    ansible.builtin.debug:
      msg: "apt install {{ packages | join(' ') }}"

Map + Select (Filter + Transform)

vars:
  users:
    - name: alice
      active: true
    - name: bob
      active: false
    - name: carol
      active: true

tasks:
  - name: Get names of active users only
    ansible.builtin.debug:
      msg: "{{ users | selectattr('active') | map(attribute='name') | list }}"
    # Output: ["alice", "carol"]

Map + Flatten

vars:
  teams:
    - name: platform
      members: [alice, bob]
    - name: frontend
      members: [carol, dave]

tasks:
  - name: Get all members across teams
    ansible.builtin.debug:
      msg: "{{ teams | map(attribute='members') | flatten | list }}"
    # Output: ["alice", "bob", "carol", "dave"]

map vs selectattr vs reject

FilterPurposeExample
mapTransform each elementExtract attribute, apply filter
selectattrFilter by attribute valueKeep items where active == true
rejectattrExclude by attributeRemove items where disabled == true
selectFilter by testKeep items matching a test
# Chaining: filter then transform
- debug:
    msg: >-
      {{ users
         | selectattr('role', 'equalto', 'admin')
         | map(attribute='name')
         | list }}
  # Get names of admins only

Real-World Examples

Generate /etc/hosts Entries

- name: Build hosts entries
  ansible.builtin.copy:
    content: |
      {% for entry in groups['all'] | map('extract', hostvars, ['ansible_host']) %}
      {{ entry }} {{ groups['all'][loop.index0] }}
      {% endfor %}
    dest: /etc/hosts.d/ansible-managed

Install Packages from Structured Data

vars:
  applications:
    - name: webapp
      packages: [nginx, nodejs, npm]
    - name: database
      packages: [postgresql, postgresql-contrib]

tasks:
  - name: Install all packages
    ansible.builtin.apt:
      name: "{{ applications | map(attribute='packages') | flatten }}"
      state: present

Build Docker Compose Ports

vars:
  services:
    - name: web
      port: 8080
    - name: api
      port: 3000
    - name: admin
      port: 9090

tasks:
  - name: Show port mappings
    ansible.builtin.debug:
      msg: "{{ services | map(attribute='port') | map('string') | map('regex_replace', '^(.*)$', '\\1:\\1') | list }}"
    # Output: ["8080:8080", "3000:3000", "9090:9090"]

Extract from Registered Output

- name: Get running containers
  ansible.builtin.command: docker ps --format json
  register: docker_ps
  changed_when: false

- name: Parse container names
  ansible.builtin.set_fact:
    container_names: "{{ docker_ps.stdout_lines | map('from_json') | map(attribute='Names') | list }}"

Troubleshooting

IssueFix
map returns generator, not listAdd | list at the end
undefined attribute errorUse default filter: map(attribute='key', default='')
Nested attribute failsUse dot notation: map(attribute='parent.child')
Can't chain map with selectattrPut selectattr BEFORE map
Filter not found in mapUse quotes: map('upper') not map(upper)

Best Practices

  1. Always add | list — map returns a generator, not a list
  2. Chain selectattr → map — filter first, transform second
  3. Use default for optional attributes — prevents undefined errors
  4. Prefer map(attribute=...) over loops — cleaner and faster
  5. Use flatten after mapping lists — collapses nested lists
  6. Test complex chains incrementally — debug each step

Conclusion

The map filter transforms lists: extract attributes with map(attribute='key'), apply filters with map('filtername'), or chain with selectattr for filtered transformations. Always end with | list to materialize the result. Combine with join, flatten, and select for powerful one-line data transformations.