loop and when — Grow the Data, Not the Tasks
Summary in one line
Instead of copying the same task to make ten of them, keep one task and turn the data into ten lines.
Why this is needed
If you write a playbook that creates three users by copy and paste, then when a fourth user comes along you paste in one more task. But if just one of them omits the permission argument, only that user is configured differently. This kind of error does not stand out no matter how much you look at the code — because all four look almost the same.
loop removes this structural risk. With only one task there is only one place to forget an argument, and the data looks like a table, so a missing cell stands out.
How it works
loop takes a list and repeats the task, putting one element at a time into item. If the list elements are dictionaries, you access their fields like item.name.
when is a condition. When you use loop and when together, the condition is evaluated for each item of the loop. That is, only the items that satisfy the condition are processed. This combination creates a "conditional loop."
There are a few combinations to watch out for.
| Tool | Use | Pitfall |
|---|---|---|
loop_control.label |
Specifies an abbreviated name to print in the log | If not specified, the whole dictionary is exposed in the log |
until / retries / delay |
Retries until a condition becomes true | until is tricky to combine with loop |
product filter |
All combinations of two lists | The number of combinations grows as a product |
fileglob lookup |
List of files matching a pattern | The order is not guaranteed |
loop_control.label is also a security matter. If a dictionary contains a password, it is printed as is in the default log. It is safer to specify a label or use no_log.
until expresses "wait until it works." It is used when waiting for a service to come up, but a retry only delays a failure; it does not eliminate it. When you decide the number of retries and the interval, the judgment "if it doesn't work within this time, there is a real problem" has to be part of it.
What it looks like in the field
First, idempotency inside a loop. A task that loops must also be idempotent for each item. If the loop runs ten times, the damage from non-idempotency is ten times as large.
Second, combinatorial explosion. 3 environments × 2 components = 6 is fine, but multiply that by 5 regions and you get 30. A combination loop is convenient, but you have to factor in that the execution time and the log length grow as a product.
Third, do not depend on order. The order of the results of a file pattern lookup is not guaranteed. If order matters, sort them or use an explicit list.
The shape of loop and its cost
# 목록을 그대로
- name: 패키지 설치
apt: {name: "{{ item }}", state: present}
loop: [git, curl, jq] # ❌ 태스크가 세 번 돈다
# 모듈이 목록을 받으면 한 번에
- name: 패키지 설치
apt: {name: [git, curl, jq], state: present} # ✅ apt 한 번
If a module accepts a list, do not use loop. apt, yum, and pip take a list
and process it all at once, but if you run them through a loop, apt is called again for every package.
With 100 packages, that is 100 calls.
When looping over complex data, use loop_control to make the output easier to read.
- name: 사용자 만들기
user: {name: "{{ item.name }}", groups: "{{ item.groups }}"}
loop: "{{ users }}"
loop_control:
label: "{{ item.name }}" # 로그에 딕셔너리 전체 대신 이름만
pause: 1 # 항목 사이 1초 — API 속도 제한이 있을 때
Without label, the log gets flooded with the full dictionaries. If a secret is in there,
it is printed as is, so this is also a security matter (together with no_log: true).
When a when condition is evaluated
when is evaluated for each item. When used with loop, items are filtered one by one.
- name: 프로덕션 호스트에만
copy: {src: prod.conf, dest: /etc/app.conf}
when: env == 'prod' # 호스트마다 평가
Do not use {{ }} inside when. It is already a Jinja2 expression context,
so wrapping it a second time produces a warning or is evaluated as a string.
when: env == 'prod' # ✅
when: "{{ env == 'prod' }}" # ❌ 되기는 하지만 경고
When dealing with an undefined variable, put is defined first. Short-circuit evaluation works
as in Python, so the condition after it becomes safe.
when: extra_port is defined and extra_port | int > 1024
Three ways to handle failure
- command: /opt/check.sh
register: result
failed_when: result.rc not in [0, 2] # 2도 성공으로 본다
changed_when: false # 상태를 안 바꾸는 조회
ignore_errors: true # 실패해도 계속 (최후의 수단)
ignore_errors is easy to overuse. If you ignore a failure, the tasks after it run on a wrong
premise. In most cases it is better to define "what counts as a real failure" with failed_when.
With block/rescue/always, you can use it like exception handling.
- block:
- command: /opt/migrate.sh
rescue:
- command: /opt/rollback.sh
- fail: {msg: "마이그레이션 실패 — 되돌렸습니다"}
always:
- command: /opt/unlock.sh
What you will do in the next lab
You loop over a list and a list of dictionaries to create directories, and use when to process only the items that match a condition. You tidy up the log with loop_control.label and build waiting logic with until. You create 6 configuration files from combinations of two lists, gather them again with a pattern, and finish with a report that summarizes everything.