데이터 다루기

데이터 다루기 (Manipulating data)

플레이북을 작성하다 보면 변수로 복잡한 연산을 해야 할 때가 많아요. Ansible은 데이터 처리 도구로 권장되지는 않지만, 내장된 Jinja2 템플릿팅과 Ansible이 추가한 다양한 필터·룩업·테스트를 함께 쓰면 꽤 복잡한 변환도 할 수 있습니다. 이 페이지에서는 그 방법을 예시와 함께 다룹니다.

출처: 문서

본문

여러 경우에 변수로 복잡한 연산을 수행해야 할 필요가 있어요. Ansible이 데이터 처리/조작 도구로 권장되지는 않지만, 기존 Jinja2 템플릿팅을 Ansible이 추가한 많은 필터, 룩업, 테스트와 함께 사용하면 매우 복잡한 변환을 수행할 수 있습니다.

각 플러그인 유형의 빠른 정의부터 시작하겠습니다:

  • lookups: 주로 '외부 데이터'를 조회하는 데 사용해요. Ansible에서 룩업은 with_<lookup> 구문을 사용한 루프의 핵심 부분이지만, 처리할 데이터를 반환하기 위해 독립적으로도 사용할 수 있습니다. 앞서 언급했듯 루프에서의 주 기능 때문에 보통 목록을 반환합니다. lookup 또는 query Jinja2 연산자와 함께 사용합니다.
  • filters: 데이터를 변경/변환하는 데 사용하며, | Jinja2 연산자와 함께 사용합니다.
  • tests: 데이터를 검증하는 데 사용하며, is Jinja2 연산자와 함께 사용합니다.

루프와 리스트 컴프리헨션 (Loops and list comprehensions)

대부분의 프로그래밍 언어에는 객체 목록을 포함한 리스트에 변환을 적용하는 루프(for, while 등)와 리스트 컴프리헨션이 있어요. Jinja2에는 이 기능을 제공하는 몇 가지 필터가 있습니다: map, select, reject, selectattr, rejectattr.

  • map: 기본 for 루프로, 리스트의 모든 항목을 변경할 수 있게 해줘요. 'attribute' 키워드를 사용하면 리스트 요소의 속성에 기반해 변환할 수 있습니다.
  • select/reject: 조건이 있는 for 루프로, 조건 결과에 따라 일치(또는 불일치)하는 리스트의 부분집합을 만들 수 있게 해줘요.
  • selectattr/rejectattr: 위와 매우 비슷하지만 조건문에 리스트 요소의 특정 속성을 사용합니다.

지수 백오프(exponential backoff)를 만들기 위해 루프를 사용해 보세요.

- name: try wait_for_connection up to 10 times with exponential delay
  ansible.builtin.wait_for_connection:
    delay: '{{ item | int }}'
    timeout: 1
  loop: '{{ range(1, 11) | map("pow", 2) }}'
  loop_control:
    extended: true
  ignore_errors: "{{ not ansible_loop.last }}"
  register: result
  when: result is not defined or result is failed

리스트 요소와 일치하는 키를 딕셔너리에서 추출하기 (Extract keys from a dictionary matching elements from a list)

이에 해당하는 Python 코드는:

chains = [1, 2]
for chain in chains:
    for config in chains_config[chain]['configs']:
        print(config['type'])

Ansible에서는 여러 가지 방법이 있는데, 그중 하나의 예시입니다:

딕셔너리 목록에서 일치하는 키를 추출하는 방법

tasks:
  - name: Show extracted list of keys from a list of dictionaries
    ansible.builtin.debug:
      msg: "{{ chains | map('extract', chains_config) | map(attribute='configs') | flatten | map(attribute='type') | flatten }}"
    vars:
      chains: [1, 2]
      chains_config:
          1:
              foo: bar
              configs:
                  - type: routed
                    version: 0.1
                  - type: bridged
                    version: 0.2
          2:
              foo: baz
              configs:
                  - type: routed
                    version: 1.0
                  - type: bridged
                    version: 1.1

debug 작업의 결과, 추출된 키를 담은 리스트:

ok: [localhost] => {
    "msg": [
        "routed",
        "bridged",
        "routed",
        "bridged"
    ]
}

호스트마다 다른 변수의 고유 값 리스트 얻기:

vars:
    unique_value_list: "{{ groups['all'] | map ('extract', hostvars, 'varname') | list | unique}}"

마운트 지점 찾기 (Find mount point)

이 경우 머신들에서 주어진 경로에 대한 마운트 지점을 찾고 싶어요. 이미 mount 팩트를 수집하고 있으므로 다음을 사용할 수 있습니다:

selectattr로 마운트를 필터링해 목록으로 만든 뒤 정렬하고 마지막 선택하기

- hosts: all
  gather_facts: True
  vars:
     path: /var/lib/cache
  tasks:
  - name: The mount point for {{path}}, found using the Ansible mount facts, [-1] is the same as the 'last' filter
    ansible.builtin.debug:
     msg: "{{(ansible_facts.mounts | selectattr('mount', 'in', path) | list | sort(attribute='mount'))[-1]['mount']}}"

같은 딕셔너리 목록에서 값 결합하기 (Combine values from same list of dicts)

위 예시들의 양수·음수 필터를 결합하면, 값이 있을 때 '값'을, 없을 때 '폴백(fallback)'을 얻을 수 있어요.

필요에 따라 selectattr와 rejectattr로 ansible_host 또는 inventory_hostname 얻기

- hosts: localhost
  tasks:
    - name: Check hosts in inventory that respond to ssh port
      wait_for:
        host: "{{ item }}"
        port: 22
      loop: '{{ has_ah + no_ah }}'
      vars:
        has_ah: '{{ hostvars|dictsort|selectattr("1.ansible_host", "defined")|map(attribute="1.ansible_host")|list }}'
        no_ah: '{{ hostvars|dictsort|rejectattr("1.ansible_host", "defined")|map(attribute="0")|list }}'

변수 기반의 커스텀 fileglob (Custom Fileglob Based on a Variable)

이 예시는 Python 인자 목록 언패킹(argument list unpacking)을 사용해 변수에 기반한 커스텀 fileglob 목록을 만듭니다.

변수 기반 리스트로 fileglob 사용하기

- hosts: all
  vars:
    mygroups:
      - prod
      - web
  tasks:
    - name: Copy a glob of files based on a list of groups
      copy:
        src: "{{ item }}"
        dest: "/tmp/{{ item }}"
      loop: '{{ q("fileglob", *globlist) }}'
      vars:
        globlist: '{{ mygroups | map("regex_replace", "^(.*)$", "files/\\1/*.conf") | list }}'

복합 타입 변환 (Complex Type transformations)

Jinja는 단순 데이터 타입 변환(int, bool 등)을 위한 필터를 제공하지만, 데이터 구조를 변환하려 할 때는 그렇게 쉽지 않아요. 위에서 보여준 것처럼 루프와 리스트 컴프리헨션을 사용하고, 다른 필터와 룩업을 연결해 더 복잡한 변환을 이룰 수 있습니다.

리스트에서 딕셔너리 만들기 (Create dictionary from list)

대부분의 언어에서는 쌍(pair)의 리스트에서 딕셔너리(맵/연관 배열/해시 등으로도 불림)를 만드는 것이 쉬워요. Ansible에는 여러 가지 방법이 있으며, 데이터의 출처에 따라 가장 좋은 방법이 달라질 수 있습니다.

다음 예시들은 {"a": "b", "c": "d"}를 만들어냅니다.

리스트를 [key, value, key, value, …]로 가정해 단순 리스트를 dict로:

vars:
    single_list: [ 'a', 'b', 'c', 'd' ]
    mydict: "{{ dict(single_list[::2] | zip_longest(single_list[1::2])) }}"

쌍의 리스트가 있으면 더 단순합니다:

vars:
    list_of_pairs: [ ['a', 'b'], ['c', 'd'] ]
    mydict: "{{ dict(list_of_pairs) }}"

둘 다 같은 결과가 나와요. zip_longestsingle_listlist_of_pairs 생성자로 변환하기 때문입니다.

조금 더 복잡하게, set_factloop를 사용해 2개의 리스트에서 키-값 쌍으로 딕셔너리를 만들거나 업데이트할 수 있습니다:

리스트 집합에서 set_fact로 딕셔너리 만들기

- name: Uses 'combine' to update the dictionary and 'zip' to make pairs of both lists
  ansible.builtin.set_fact:
    mydict: "{{ mydict | default({}) | combine({item[0]: item[1]}) }}"
  loop: "{{ (keys | zip(values)) | list }}"
  vars:
    keys:
      - foo
      - var
      - bar
    values:
      - a
      - b
      - c

이 결과는 {"foo": "a", "var": "b", "bar": "c"}입니다.

이 단순한 예시들을 다른 필터와 룩업과 결합해, 변수 이름에 패턴을 매칭해 딕셔너리를 동적으로 만들 수도 있어요:

작업 없이 리스트 집합에서 'vars'로 딕셔너리 정의하기

vars:
    xyz_stuff: 1234
    xyz_morestuff: 567
    myvarnames: "{{ q('varnames', '^xyz_') }}"
    mydict: "{{ dict(myvarnames|map('regex_replace', '^xyz_', '')|list | zip(q('vars', *myvarnames))) }}"

이 두 줄은 풀어야 할 것이 많으므로 간단히 설명할게요:

  • varnames 룩업은 "xyz_로 시작하는" 변수 목록을 반환해요.
  • 그다음 이전 단계의 목록을 vars 룩업에 넣어 값 목록을 얻습니다. *는 '리스트를 역참조(dereference)하는'(Jinja에서 동작하는 파이썬 방식) 데 사용하며, 그렇지 않으면 리스트를 단일 인자로 취급합니다.
  • 두 목록은 zip 필터에 전달되어 (key, value, key2, value2, …) 통합 목록으로 짝을 지어요.
  • 그런 다음 dict 함수가 이 '쌍의 리스트'를 받아 딕셔너리를 만듭니다.

팩트를 사용해 조건 X를 만족하는 호스트의 데이터를 찾는 예시:

vars:
  uptime_of_host_most_recently_rebooted: "{{ansible_play_hosts_all | map('extract', hostvars, 'ansible_uptime_seconds') | sort | first}}"

호스트 업타임을 일/시/분/초로 보여주는 예시(팩트가 수집되었다고 가정):

- name: Show the uptime in days/hours/minutes/seconds
  ansible.builtin.debug:
   msg: Uptime {{ now().replace(microsecond=0) - now().fromtimestamp(now(fmt='%s') | int - ansible_uptime_seconds) }}

더 보기 (See also)

  • 필터로 데이터 다루기 — Ansible에 포함된 Jinja2 필터.
  • 테스트 (Tests) — Ansible에 포함된 Jinja2 테스트.
  • Jinja2 문서 — 핵심 필터와 테스트 목록을 포함한 Jinja2 문서.

더 알아보기 (Learn more)

  • 이 페이지에서 쓴 필터와 룩업의 전체 목록은 "필터로 데이터 다루기(playbooks_filters)"와 "테스트(playbooks_tests)" 페이지에서 확인할 수 있어요.