はじめに
Field Notes の編集者は、異なる写真セットを持つ 2 台のワークステーションで処理を行います。コンタクトシートと対応するファイル一覧があれば写真を選別できますが、バックグラウンドジョブの開始や同名の古いファイルの存在だけでは納品完了にはなりません。まず小さな例を作り、その後 Ansible で各ワークステーションで今回生成された実際の結果を待って収集します。
基本的なターミナル操作と、小規模な Ansible インベントリ、変数、command/file タスクの経験が必要です。SSH アクセス、利用許諾のある写真、画像ツールは準備済みです。このラボでは、引数リスト、ループ、ホスト別ジョブ ID、終了確認、fetch の保存先を使用前に説明します。所要時間は読解と通常の修正を含む著者の見積もりで、実際の初心者の完了時間を測定した値ではありません。
小さなコンタクトシートを作る
このステップでは、Ansible で小さな写真ジョブを実行し、実際の画像とファイル一覧を収集して、より大きな処理の見本にします。
Photo Desk タブを開きます。A previous walk の Open contact sheet をクリックしてください。コンタクトシートは複数の写真を小さくして 1 ページに並べ、すべての原本を開かずに構図を比較できるようにしたものです。この過去の例には previous-walk と表示されています。参考資料であり、今日の完成品ではありません。画面が狭い場合は画像枠の中をスクロールしてファイル名を読んでください。Back to photo desk をクリックすると、同じタブのページに戻ります。
物語とワークステーションの割り当ては架空です。風景写真は利用許諾のある教材で、Photo credits に実際の撮影者と出典が記載されています。準備済みの画像ツールは本物の JPEG を読み、PNG のコンタクトシートと CSV のファイル一覧を生成します。画像処理コードを書くのではなく、このツールの実行を自動化します。
Controller タブに切り替えます。開始ディレクトリは /home/labex/project です。コントローラで Ansible が動きます。2 つの SSH ターゲットはこの演習用 VM のカーネルを共有するコンテナで、独立した 2 台の VM ではありません。同じパス名でも、各ターゲットには別々のファイルがあります。
pwd は現在のディレクトリを、cat はファイルを表示します。準備済みインベントリと変数を読みます。
pwd
cat inventory.yml
cat group_vars/all.yml
cat host_vars/camera-north.yml
cat host_vars/camera-south.yml
インベントリの cameras グループには、実際の SSH 設定を持つ camera-north と camera-south があります。プロジェクトの ansible.cfg がこの inventory.yml と準備済みのホスト信頼設定を選択します。イメージに元からある inventory.ini はこのコースのインベントリではありません。設定、インベントリ、変数ファイルは変更しないでください。
これらのファイルの JSON 形式の波かっこは、有効な YAML です。group_vars/all.yml は共通変数を提供します。input_root は /srv/photo-sheets/batches、output_root は /srv/photo-sheets/rendered、collection_root はコントローラ上で Playbook の隣にある collected ディレクトリです。host_vars 内のファイルは同名のホストに適用されます。ここでは photo_batch が北側で morning-walk、南側で afternoon-walk を選びます。今日の選択は内容も枚数も異なります。
まず北側で、2 枚の写真が入った準備済みの sample 入力を使います。Ansible の command モジュールが、その SSH ターゲットで準備済みプログラムを実行します。argv リストは要素ごとに 1 つの引数を渡し、シェルにコマンド文字列の解釈を任せません。ツールの --input は元のディレクトリ、--output は結果のディレクトリ、--station は結果のワークステーション表示を指定します。元の写真は編集されません。
{{ variable }} は Ansible の値を挿入します。inventory_hostname は現在のホストのインベントリ名、playbook_dir はコントローラ上で Playbook を置いたディレクトリです。gather_facts: false はこの写真ジョブに不要なマシン情報の収集を省略します。
fetch モジュールは SSH ターゲットからコントローラへファイルを転送します。copy とは逆方向です。loop で指定した 2 つのファイル名について繰り返し、{{ item }} がその回のファイル名になります。通常 fetch はホストと元のパスを含むディレクトリ構造を残します。ここでは flat: true で元のパスを省くため、保存先に inventory_hostname を明示します。これにより別のワークステーションの同名ファイルを分離できます。必要な保存先ディレクトリは fetch が作ります。
sample.yml を作ります。cat > filename はファイルを書き込み、既存の同名ファイルを置き換えます。<<'YAML' はヒアドキュメントの開始です。最後の、単独行の YAML まで貼り付けます。引用符付きの区切り文字により貼り付けたテキストがそのまま保持され、Ansible が Playbook 実行時に式を処理します。YAML の字下げには空白を使います。
cat > sample.yml <<'YAML'
- name: Build a small selection before scheduling both workstations
hosts: camera-north
gather_facts: false
tasks:
- name: Build the two-photo practice sheet
ansible.builtin.command:
argv:
- /usr/local/bin/photo-sheet
- --input
- "{{ input_root }}/sample"
- --output
- "{{ output_root }}/sample"
- --station
- "{{ inventory_hostname }}"
- name: Collect the practice image and file list
ansible.builtin.fetch:
src: "{{ output_root }}/sample/{{ item }}"
dest: "{{ playbook_dir }}/sample/{{ inventory_hostname }}/"
flat: true
loop:
- contact-sheet.png
- files.csv
YAML
実行前に構文を確認します。--syntax-check は Playbook の構造を調べますが、写真は処理しません。
ansible-playbook sample.yml --syntax-check
スペルや字下げに誤りがあれば、Playbook 全体をもう一度コピーし、構文を再確認します。それでは実行します。
ansible-playbook sample.yml
画像タスクは標準では同期実行です。Ansible はプログラムが終わってからファイルを取得します。集計には failed=0 と unreachable=0 が表示されるはずです。写真ジョブは実際に処理を行い、changed と報告することがあります。画像処理を繰り返しても変更が発生しないという保証はありません。
収集したファイル一覧を読みます。
cat sample/camera-north/files.csv
ヘッダーは station,batch,position,filename,sha256,width,height です。camera-north と sample のデータ行が 2 行あり、画像の左から右への順序と一致するはずです。ファイル名は frame-01.jpg と frame-02.jpg です。SHA256 は元ファイルの内容の指紋であり、長い値を暗記する必要はありません。寸法は小さなプレビューではなく元の写真のものです。
Photo Desk に切り替えて再読み込みします。Small practice sheet の Open contact sheet で実際の 2 枚の結果を確認します。Back to photo desk で戻り、この練習カードの Download contact sheet (PNG) と Download file list (CSV) を使います。ダウンロードした画像と CSV を開き、2 枚の写真、ファイル名、行の順序が一致することを確認してください。同じファイルを再度ダウンロードすると、ブラウザが名前に番号を付ける場合があります。
次の練習カードには、実際の 2 枚の画像と両方のダウンロードリンクがあります。ファイルを使う前にワークステーションと sample の表示を確認してください。

コマンドが「No such file or directory」と表示したら、ls で現在のディレクトリを確認し、綴りを調べます。cd /home/labex/project で作業場所に戻り、該当コマンドを繰り返します。元の写真、過去の参考ファイル、編集メモ、準備済み接続設定をすべて保持してください。より大きな今回の選択はまだ作成されていません。
各ジョブの結果を待って収集する
このステップでは、両ワークステーションの今回の選択を処理し、各ホスト自身の成功結果を待って、対応するファイルを衝突なく収集します。
Controller の /home/labex/project に留まります。cameras グループが両ターゲットを選び、それぞれの photo_batch が各自の準備済み入力を選びます。2 台の名前や入力パスを固定せず、別のメンバー、バッチ名、ディレクトリでも同じ流れを使えるように、これらの変数を利用してください。
同期タスクは、Ansible が次のタスクへ進む前に終了を待ちます。async: 120 はリモート処理に 120 秒の実行時間枠を与えます。poll: 0 はそのタスク内で終了を待たずに開始します。開始の成功と処理の成功は別です。リモートジョブはその後エラーで終わることもあります。
register: photo_job は開始結果をインベントリのホストごとに保存します。ansible_job_id はそのホスト上のジョブを識別します。一方の JID で他方のワークステーションの結果を照会することはできません。debug モジュールで保存した観察値を表示し、異なる割り当てを確認します。
async_status は同じホストに JID の状態を問い合わせ、各応答を photo_result に保存します。until: photo_result is finished はネイティブの終了フラグが真になるまで照会を繰り返します。retries: 60 は回数の上限、delay: 1 は照会間の 1 秒待機です。これは状態照会の再試行であり、新しい写真ジョブを作るものではありません。実際には SSH や処理の時間も加わります。小さな実データでは最初の照会時に終わっていることもあります。この演習は人工的な遅延を追加しません。
終了だけでは成功を証明できません。リモートプログラムの rc は終了ステータスで、ゼロが成功です。async_status は標準でゼロ以外を失敗として伝えます。追加の assert は終了ステータスがゼロであるという要件を明示するもので、エラーを成功に変えません。続くデバッグ出力で、収集前に実際の finished と rc を読みます。同等の実装では追加のアサーションを使わず、モジュールの標準の失敗伝播に任せても構いません。
両ホストはそれぞれのファイルシステムに、同名の contact-sheet.png と files.csv を作ります。成功後だけに取得し、コントローラのインベントリホスト別ディレクトリに各組を保存します。Photo Desk はこの明示的なフラット構成でも、ワークステーションの収集フォルダ内にある標準のホストと元パスの階層でも読み取れます。欠けた収集結果を参考ファイルから補うことはありません。
前のステップと同じ引用符付きヒアドキュメントで process-photos.yml を書きます。
cat > process-photos.yml <<'YAML'
- name: Build and collect each workstation's current photo selection
hosts: cameras
gather_facts: false
tasks:
- name: Start this workstation's contact sheet
ansible.builtin.command:
argv:
- /usr/local/bin/photo-sheet
- --input
- "{{ input_root }}/{{ photo_batch }}"
- --output
- "{{ output_root }}"
- --station
- "{{ inventory_hostname }}"
async: 120
poll: 0
register: photo_job
- name: Show the job assigned to this workstation
ansible.builtin.debug:
msg:
workstation: "{{ inventory_hostname }}"
job: "{{ photo_job.ansible_job_id }}"
- name: Wait for this workstation's own job
ansible.builtin.async_status:
jid: "{{ photo_job.ansible_job_id }}"
register: photo_result
until: photo_result is finished
retries: 60
delay: 1
- name: Require successful processing before collecting
ansible.builtin.assert:
that: photo_result.rc == 0
- name: Read the actual completion result
ansible.builtin.debug:
msg:
workstation: "{{ inventory_hostname }}"
finished: "{{ photo_result.finished }}"
rc: "{{ photo_result.rc }}"
- name: Collect the image and its file list by workstation
ansible.builtin.fetch:
src: "{{ output_root }}/{{ item }}"
dest: "{{ collection_root }}/{{ inventory_hostname }}/"
flat: true
loop:
- contact-sheet.png
- files.csv
YAML
構文を確認します。
ansible-playbook process-photos.yml --syntax-check
実際の処理を開始します。
ansible-playbook process-photos.yml
各ワークステーションに固有のジョブ ID が表示され、終了メッセージに finished: true と rc: 0 が現れるはずです。最後の集計では両ホストとも failed=0、unreachable=0 になります。北側は 3 枚、南側は 6 枚です。実際のジョブは別々のタイミングで終わることがあります。これは本物の非同期実行の演習であり、より大きな処理の性能保証ではありません。
両方の収集済み一覧を読みます。
cat collected/camera-north/files.csv
cat collected/camera-south/files.csv
station と batch は各ホストの割り当てと一致する必要があります。同じファイル名でも元の写真が同じとは限りません。各行の位置、ファイル名、指紋、寸法は実際の入力写真 1 枚を表し、画像も同じ順序を使います。
Photo Desk に切り替えて再読み込みします。Current collections で各ワークステーションの Open contact sheet を開きます。3 枚と 6 枚のシートを原寸で確認し、その間は Back to photo desk で戻ります。各 PNG と CSV をダウンロードして両方を開き、写真と完全な行が対応することを確認してください。参考資料と小さな練習結果も利用できる状態を保ちます。
2 枚のカードのホストとバッチ表示を比較します。北側は 3 枚の 1 行、南側は 3 枚ずつの 2 行で、それぞれ異なる元の選択です。

終了したジョブでも失敗する
Controller に戻ります。今度は北側だけで、存在しない入力を意図的に選択します。--limit camera-north は呼び出しをそのインベントリホストに限定します。--extra-vars photo_batch=missing はこの呼び出しだけでバッチ変数を上書きし、準備済み変数ファイルを編集しません。ジョブは開始後、処理が終了して失敗したと報告します。既存の正常なファイルを、新しい成功結果と扱ってはいけません。
次のコマンドは失敗する想定です。
ansible-playbook process-photos.yml --limit camera-north --extra-vars photo_batch=missing
直前のコマンドのステータスをすぐ表示します。$? は最後のコマンドの終了ステータスを保持し、echo が表示します。先に別のコマンドを実行すると値が置き換わります。
echo $?
Ansible CLI の終了ステータスはゼロ以外、通常 2 になります。失敗メッセージではリモート結果は終了済みで、プログラムの rc が 2、エラーには /srv/photo-sheets/batches/missing が含まれます。CLI とリモートプログラムのステータスは別の値で、ここでは偶然どちらも 2 です。終了した失敗ジョブは成功した収集ではありません。この失敗した試行では fetch タスクを実行してはいけません。南側はこの呼び出しの対象外です。
準備済みの既定値に戻して選択を修正し、北側だけを再実行します。
ansible-playbook process-photos.yml --limit camera-north
この実行は成功し、実際の morning-walk の結果を収集する必要があります。南側の結果は成功したジョブのものが引き続き利用できます。Photo Desk に戻って再読み込みし、北側の PNG と CSV を再びダウンロードして、正しい 3 枚の選択を確認します。process-photos.yml は再利用できる状態にし、すべての元写真と過去の出力を保ち、編集メモを非公開のままにしてください。
通常の YAML やファイル名の誤りでは、前のステップの ls、ディレクトリへの復帰、ブロック全体の修正を使い、構文を再確認します。入力エラーを隠すために VM をリセットしたり元写真を変更したりしないでください。成功した写真処理を繰り返せば処理は再実行されます。fetch のチェックサムが同じでも、処理が行われなかったとは限りません。
poll: 0 の後、Ansible はジョブの状態キャッシュを残します。終了したジョブを確認したら、async_status の mode: cleanup とそのホストの jid でキャッシュ項目を削除できます。この保守作業は任意です。キャッシュ削除は実行中のジョブを中止せず、コンタクトシートも削除しません。
まとめ
実際の小さなコンタクトシートを作り、Ansible で各ホストに属する 2 つの非同期ジョブを開始して待ちました。画像と CSV の完全な組をワークステーション別に収集し、終了した失敗と成功を区別し、入力がない問題を該当ホストだけの再実行で修正しました。



