loopとwhen — タスクではなくデータを増やす
一言でいうと
同じタスクをコピーして10個作る代わりに、タスクは1つだけ置いて、データを10行にします。
なぜ必要なのか
ユーザー3人を作るプレイブックをコピー・ペーストで書くと、4人目のユーザーができたときにタスクを1つ追加します。ところが、そのうちの1つだけが権限の引数を抜かしていたら、そのユーザーだけ設定が違います。このエラーは、コードをいくら見ても目に留まりにくいです。4つがほとんど同じに見えるからです。
loopは、この構造的なリスクをなくします。タスクが1つだけなので、引数を抜かす場所も1か所だけで、データは表のように見えるので、抜けたマスが目立ちます。
どう動くのか
loopはリストを受け取り、itemに1つずつ入れてタスクを繰り返します。リストの要素がディクショナリなら、item.nameのようにフィールドでアクセスします。
whenは条件です。loopとwhenを一緒に使うと、条件は繰り返しの各項目ごとに評価されます。つまり、条件に合う項目だけが処理されます。この組み合わせが、「条件付きの繰り返し」を作ります。
注意すべき組み合わせがいくつかあります。
| ツール | 用途 | 落とし穴 |
|---|---|---|
loop_control.label |
ログに出力する省略名を指定 | 指定しないと、ディクショナリ全体がログに露出する |
until / retries / delay |
条件が真になるまでリトライ | untilはloopと一緒に使うのが厄介 |
productフィルター |
2つのリストのすべての組み合わせ | 組み合わせの数が掛け算で増える |
fileglobルックアップ |
パターンに合うファイルの一覧 | 順序が保証されない |
loop_control.labelはセキュリティの問題でもあります。ディクショナリにパスワードが入っていると、デフォルトのログにそのまま出力されます。ラベルを指定するかno_logを使うのが安全です。
untilは「そうなるまで待つ」を表現します。サービスが起動するのを待つ状況に使いますが、リトライは失敗を遅らせるだけで、なくしはしません。リトライの回数と間隔を決めるときには、「この時間内にできなければ本当に問題だ」という判断が一緒に入っている必要があります。
現場での姿
1つ目は、繰り返しの中の冪等性です。繰り返すタスクも、各項目に対して冪等でなければなりません。繰り返しが10回動けば、非冪等の被害も10倍です。
2つ目は、組み合わせ爆発です。環境3 × コンポーネント2 = 6なら問題ありませんが、ここにリージョン5を掛けると30になります。組み合わせの繰り返しは便利ですが、実行時間とログの長さが掛け算で増えることを、計算に入れる必要があります。
3つ目は、順序に依存しないことです。ファイルのパターンで調べた結果の順序は、保証されません。順序が重要なら、ソートするか、明示的なリストを使います。
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を先に置きます。Pythonのように短絡評価されるので、後ろの条件が安全になります。
when: extra_port is defined and extra_port | int > 1024
失敗を扱う3つの方法
- 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で待機ロジックを作ります。2つのリストの組み合わせで6つの設定ファイルを作り、パターンでそれらをもう一度集めたあと、全体をまとめたレポートで締めくくります。