loopとwhen — タスクではなくデータを増やす
한국어 원문으로 표시합니다.
한 줄 요약
같은 태스크를 복사해 열 개 만드는 대신, 태스크는 하나 두고 데이터를 열 줄로 만든다.
왜 이게 필요했나
사용자 세 명을 만드는 플레이북을 복사·붙여넣기로 쓰면, 네 번째 사용자가 생겼을 때 태스크를 하나 더 붙인다. 그런데 그중 하나만 권한 인자를 빠뜨리면 그 사용자만 설정이 다르다. 이 오류는 코드를 아무리 봐도 눈에 잘 안 띈다 — 네 개가 거의 같아 보이기 때문이다.
loop 는 이 구조적 위험을 없앤다. 태스크가 하나뿐이니 인자를 빠뜨릴 자리도 하나뿐이고, 데이터는 표처럼 보여 빠진 칸이 눈에 띈다.
어떻게 동작하나
loop 는 리스트를 받아 item 에 하나씩 넣어 태스크를 반복한다. 리스트 원소가 딕셔너리면 item.name 처럼 필드로 접근한다.
when 은 조건이다. loop 와 when 을 함께 쓰면 조건은 반복의 각 항목마다 평가된다. 즉 조건에 맞는 항목만 처리된다. 이 조합이 "조건부 반복"을 만든다.
주의할 조합이 몇 가지 있다.
| 도구 | 쓰임 | 함정 |
|---|---|---|
loop_control.label |
로그에 찍을 축약 이름 지정 | 지정하지 않으면 딕셔너리 전체가 로그에 노출됨 |
until / retries / delay |
조건이 참이 될 때까지 재시도 | until 은 loop 와 함께 쓰기 까다로움 |
product 필터 |
두 리스트의 모든 조합 | 조합 수가 곱으로 늘어남 |
fileglob 조회 |
패턴에 맞는 파일 목록 | 순서가 보장되지 않음 |
loop_control.label 은 보안 문제이기도 하다. 딕셔너리에 비밀번호가 들어 있으면 기본 로그에 그대로 찍힌다. 라벨을 지정하거나 no_log 를 쓰는 것이 안전하다.
until 은 "될 때까지 기다린다"를 표현한다. 서비스가 뜨기를 기다리는 상황에 쓰지만, 재시도는 실패를 늦출 뿐 없애지 않는다. 재시도 횟수와 간격을 정할 때 "이 시간 안에 안 되면 진짜 문제"라는 판단이 함께 들어가야 한다.
현장에서 만나는 모습
첫째, 반복 안의 멱등성. 반복하는 태스크도 각 항목에 대해 멱등해야 한다. 반복이 열 번 돌면 비멱등의 피해도 열 배다.
둘째, 조합 폭발. 환경 3 × 컴포넌트 2 = 6 은 괜찮지만, 여기에 리전 5 를 곱하면 30 이 된다. 조합 반복은 편하지만 실행 시간과 로그 길이가 곱으로 늘어난다는 것을 계산에 넣어야 한다.
셋째, 순서에 의존하지 않기. 파일 패턴 조회의 결과 순서는 보장되지 않는다. 순서가 중요하면 정렬하거나 명시적 리스트를 쓴다.
loop 의 형태와 그 대가
# 목록을 그대로
- name: 패키지 설치
apt: {name: "{{ item }}", state: present}
loop: [git, curl, jq] # ❌ 태스크가 세 번 돈다
# 모듈이 목록을 받으면 한 번에
- name: 패키지 설치
apt: {name: [git, curl, jq], state: present} # ✅ apt 한 번
모듈이 목록을 받는다면 loop 를 쓰지 않습니다. apt·yum·pip 는 목록을
받아 한 번에 처리하는데, loop 로 돌리면 패키지마다 apt 를 다시 부릅니다.
100개면 100번입니다.
복잡한 자료를 돌 때는 loop_control 로 출력을 읽기 쉽게 만듭니다.
- name: 사용자 만들기
user: {name: "{{ item.name }}", groups: "{{ item.groups }}"}
loop: "{{ users }}"
loop_control:
label: "{{ item.name }}" # 로그에 딕셔너리 전체 대신 이름만
pause: 1 # 항목 사이 1초 — API 속도 제한이 있을 때
label 이 없으면 로그가 딕셔너리 전문으로 도배됩니다. 시크릿이 들어 있으면
그것이 그대로 찍히므로 보안 문제이기도 합니다(no_log: true 와 함께).
when 이 평가되는 시점
when 은 각 항목마다 평가됩니다. loop 와 함께 쓰면 항목별로 걸러집니다.
- name: 프로덕션 호스트에만
copy: {src: prod.conf, dest: /etc/app.conf}
when: env == 'prod' # 호스트마다 평가
when 안에서는 {{ }} 를 쓰지 않습니다. 이미 Jinja2 표현식 문맥이라
이중으로 감싸면 경고가 나거나 문자열로 평가됩니다.
when: env == 'prod' # ✅
when: "{{ env == 'prod' }}" # ❌ 되기는 하지만 경고
정의되지 않은 변수를 다룰 때는 is defined 를 앞에 둡니다. 파이썬처럼 단축 평가가
되므로 뒤 조건이 안전해집니다.
when: extra_port is defined and extra_port | int > 1024
실패를 다루는 세 가지
- command: /opt/check.sh
register: result
failed_when: result.rc not in [0, 2] # 2도 성공으로 본다
changed_when: false # 상태를 안 바꾸는 조회
ignore_errors: true # 실패해도 계속 (최후의 수단)
ignore_errors 는 남용하기 쉽습니다. 실패를 무시하면 그 뒤 태스크가 잘못된
전제 위에서 돕니다. 대개는 failed_when 으로 "무엇이 진짜 실패인지" 를 정의하는
편이 맞습니다.
block/rescue/always 를 쓰면 예외 처리처럼 쓸 수 있습니다.
- block:
- command: /opt/migrate.sh
rescue:
- command: /opt/rollback.sh
- fail: {msg: "마이그레이션 실패 — 되돌렸습니다"}
always:
- command: /opt/unlock.sh
다음 실습에서 할 것
리스트와 딕셔너리 리스트를 돌며 디렉터리를 만들고, when 으로 조건에 맞는 항목만 처리한다. loop_control.label 로 로그를 정리하고, until 로 대기 로직을 만든다. 두 리스트의 조합으로 6개 설정 파일을 만들고, 패턴으로 그것들을 다시 모은 뒤, 전체를 요약한 보고서로 마무리한다.