Playbookは、ただの「手順書」から「プログラム」へ進化する。
こんにちは!「LINUX工房」管理人の「リナックス先生」です。
前回は、Dynamic Inventoryを使って、クラウド上のサーバー情報を自動的に取得する方法を学びました。
しかし、自動取得した情報は、往々にして「複雑な構造(ネストしたJSON)」をしています。
「取得した全サーバーの中から、”Status=Running” のサーバーだけを抜き出して、そのプライベートIPリストを作り、設定ファイルに書き込みたい」
…そんな時、あなたは手作業でコピペしますか? それとも、スマートなフィルタ処理を書きますか?
先生、前回AWSから取得したサーバーリスト、便利なんですけど情報量が多すぎて…。
欲しいのはIPアドレスだけなのに、タグとかIDとか全部ついてきて、どうやって取り出せばいいか分かりません。
あと、ループの中でさらにループしたい時とか、item が被っちゃってわけわからなくなります!
それはAnsibleの「ロジック」と「データ加工」の知識が足りていない証拠ね。
Ansibleは単にモジュールを呼ぶだけじゃないわ。Jinja2フィルタを使いこなせば、Pythonコードを書くのと同じくらい自由自在にデータを加工できるの。
今回は、Ansibleを「プログラミング言語」として使いこなすための、深遠なるテクニックを伝授するわ!
本記事では、AlmaLinux 9 をベースに、ループ処理の高度な制御、エラーハンドリングの応用、そしてエンジニアを最も悩ませる(そして最も強力な)Jinja2フィルタの活用法について徹底解説します。
🚀 Ansible応用講座(全8回)
現在地:【第2回】ロジックを極める。Loop制御、Block例外処理、Jinja2フィルタの魔術
- 【第1回】静的ファイルは捨てろ。Dynamic InventoryによるAWS/クラウド環境の自動追従
- 【第2回】ロジックを極める。Loop制御、Block例外処理、Jinja2フィルタの魔術
- 【第3回】爆速化チューニング。Forks、Pipelining、Mitogenによるパフォーマンス改善
- 【第4回】標準機能で足りないなら。PythonによるCustom Module開発入門
- 【第5回】品質を保証する。MoleculeによるRoleの自動テストとCI連携
- 【第6回】Windowsも支配する。WinRM接続とPowerShell操作の完全ガイド
- 【第7回】ネットワーク機器の自動化。Cisco/Juniperスイッチの設定管理
- 【第8回】新時代の実行環境。Ansible Builder/RunnerによるExecution Environment (EE)
※入門編の復習はこちら:【入門 第3回】Playbookの基礎。YAMLの作法と「モジュール」
第1章:ループの進化論 (loop vs with_*)
Ansibleのループといえば with_items が有名ですが、現在は loop キーワードの使用が推奨されています。
単に書き方が変わっただけでなく、制御の幅が広がっています。
1. loop_control で変数を支配する
標準のループでは、取り出した要素は自動的に item という変数に入ります。
しかし、二重ループ(ネスト)する場合、内側のループでも item を使うと、外側の item が上書きされてしまいます。
loop_control を使えば、変数名を自由に変更できます。
- name: Create users and assign groups
ansible.builtin.user:
name: "{{ user_item.name }}"
groups: "{{ group_item }}"
append: yes
loop: "{{ users }}"
loop_control:
loop_var: user_item # 変数名を 'item' から 'user_item' に変更
# ここで内側のループなどを書く場合、名前が被らない!
2. インデックス(連番)の取得
「1つ目のサーバーはmaster、2つ目以降はslave」のように、順番に応じた処理をしたい場合、index_var が便利です。
- name: List servers with index
ansible.builtin.debug:
msg: "Server #{{ my_idx }} is {{ item }}"
loop:
- web01
- web02
- web03
loop_control:
index_var: my_idx # 0から始まる連番が入る
3. 辞書(ハッシュ)のループ
辞書型データをループさせたい場合、dict2items フィルタを使ってリスト型に変換するのが定石です。
vars:
users:
alice:
uid: 1001
shell: /bin/bash
bob:
uid: 1002
shell: /bin/zsh
tasks:
- name: Create users from dict
ansible.builtin.user:
name: "{{ item.key }}" # キー (alice, bob)
uid: "{{ item.value.uid }}" # 値の中身
shell: "{{ item.value.shell }}"
loop: "{{ users | dict2items }}"
第2章:条件分岐とステータス制御 (When / Changed)
when は「実行するかどうか」を決めるだけではありません。
「実行結果をどう扱うか」を制御する failed_when と changed_when こそが、玄人への入り口です。
1. 複雑な条件分岐
複数の条件を組み合わせる場合、リスト形式で書くと「AND(かつ)」になります。
when: - ansible_distribution == "AlmaLinux" - ansible_distribution_major_version == "9" - inventory_hostname in groups['web']
バージョン比較には version テストが便利です。
when: ansible_distribution_version is version('8.5', '>=')
2. changed_when で「変更なし」を装う
command や shell モジュールは、実行すると必ず「Changed(黄色)」になります。
しかし、「確認コマンドを叩いただけ」なら、本来は「Ok(緑色)」であるべきです。
これを制御することで、Playbookの冪等性(べきとうせい)を保ちます。
- name: Check if nginx is installed ansible.builtin.command: nginx -v register: nginx_check changed_when: false # 常に「変更なし(緑)」にする ignore_errors: true
さらに高度な制御として、「出力に特定の文字が含まれていたらChangedにする」ことも可能です。
changed_when: "'upgraded' in apt_result.stdout"
3. failed_when でエラー判定を覆す
「エラーが出るのが正常(想定内)」というケースがあります。
例えば、「grepコマンドで検索して、見つからなかった(戻り値1)」場合などです。
通常はAnsibleが止まってしまいますが、これを制御します。
- name: Check for error logs ansible.builtin.shell: grep "ERROR" /var/log/app.log register: grep_result # 戻り値が0(見つかった) または 1(見つからない) なら成功とする # 戻り値が2(ファイルがない等) なら失敗とする failed_when: grep_result.rc not in [0, 1]
第3章:失敗を許容する心 (Block / Rescue / Always)
入門編の付録でも少し触れましたが、実務ではより複雑なエラーハンドリング(例外処理)が求められます。
特に「DB更新に失敗したらバックアップから戻す」といったロールバック処理には必須です。
トランザクション的な処理の実装
- name: Database Upgrade Transaction
block:
- name: Backup Database
community.mysql.mysql_db_dump:
name: myapp
target: /tmp/myapp.sql
- name: Run Upgrade Script
ansible.builtin.shell: /opt/scripts/upgrade_db.sh
rescue:
- name: Restore Database (Rollback)
community.mysql.mysql_db_import:
name: myapp
target: /tmp/myapp.sql
- name: Notify Slack
community.general.slack:
token: "..."
msg: "DB Upgrade Failed! Rollback executed."
always:
- name: Remove Backup File
ansible.builtin.file:
path: /tmp/myapp.sql
state: absent
always セクションは、成功・失敗に関わらず必ず実行されるため、一時ファイルの削除などに最適です。
第4章:データの魔術師 (Jinja2 Filters)
ここからが本記事の核心です。
Ansible(Jinja2)のフィルタ機能を使いこなせば、複雑なJSONデータから必要な情報だけを抽出・加工できます。
これを知っているかどうかで、Playbookの行数が10分の1になります。
1. リスト操作の基本 (map, list)
「オブジェクトのリストから、特定の属性だけを抜き出してリストにする」場合に使います。
元データ (users):
[
{"name": "alice", "uid": 1001},
{"name": "bob", "uid": 1002}
]
やりたいこと: ユーザー名のリスト ['alice', 'bob'] が欲しい。
user_names: "{{ users | map(attribute='name') | list }}"
map で属性を抽出し、最後に list でリスト形式に戻します(これがないとイテレータオブジェクトになってしまいます)。
2. 条件による抽出 (selectattr)
「UIDが1002以上のユーザーだけ抜き出したい」場合。
target_users: "{{ users | selectattr('uid', '>=', 1002) | list }}"
逆に除外したい場合は rejectattr を使います。
3. パス操作 (basename, dirname)
ファイルパスからファイル名だけを取り出したい場合。
filename: "{{ '/var/log/httpd/access.log' | basename }}"
# 結果: access.log
4. JSONクエリの最終兵器 (json_query)
map や selectattr でも対応できない複雑なネスト構造には、json_query を使います。
これは JMESPath というクエリ言語を使用します。
前提: コントロールノードに jmespath ライブラリが必要です。
pip install jmespath
元データ (aws_result):
{
"instances": [
{
"tags": [{"Key": "Name", "Value": "Web01"}, {"Key": "Env", "Value": "Prod"}],
"private_ip": "10.0.1.10"
},
{
"tags": [{"Key": "Name", "Value": "Web02"}, {"Key": "Env", "Value": "Dev"}],
"private_ip": "10.0.1.11"
}
]
}
やりたいこと: “Env” タグが “Prod” であるインスタンスのプライベートIPリストが欲しい。
- name: Get Prod IPs
ansible.builtin.debug:
msg: "{{ aws_result | json_query(query) }}"
vars:
query: "instances[?tags[?Key=='Env' && Value=='Prod']].private_ip"
このクエリ一発で、複雑なJSONからピンポイントでデータを引き抜けます。
AWSやAzureのAPI戻り値を扱う際には必須のスキルです。
第5章:【実践演習】動的リストを使った設定ファイル生成
学んだ知識を組み合わせて、実践的なタスクを行いましょう。
「Dynamic Inventoryで取得した全WebサーバーのIPアドレスを抽出し、ロードバランサーの設定ファイル(HAProxy)に書き込む」というシナリオです。
1. 状況設定
- グループ
role_Webに属するホストが複数ある(前回AWSから取得したもの)。 - それぞれのホスト変数は
hostvarsマジック変数に入っている。 - HAProxyの設定ファイルに
server web01 10.0.1.10:80のように列挙したい。
2. テンプレート作成 (haproxy.cfg.j2)
global
log /dev/log local0
defaults
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
frontend http_front
bind *:80
default_backend http_back
backend http_back
balance roundrobin
# ここでループ処理!
{% for host in groups['role_Web'] %}
server {{ host }} {{ hostvars[host]['ansible_host'] }}:80 check
{% endfor %}
3. Playbook作成 (deploy_lb.yml)
---
- name: Configure Load Balancer
hosts: loadbalancer
become: true
tasks:
- name: Deploy HAProxy Config
ansible.builtin.template:
src: templates/haproxy.cfg.j2
dest: /etc/haproxy/haproxy.cfg
validate: haproxy -c -f %s
notify: Restart HAProxy
handlers:
- name: Restart HAProxy
ansible.builtin.service:
name: haproxy
state: restarted
解説
groups['role_Web']: このグループに属するホスト名のリストが取れます。hostvars[host]['ansible_host']: 特定のホストの変数(ここではIPアドレス)を取得する定型句です。
これで、Webサーバーが10台に増えても、このPlaybookを実行するだけでロードバランサーの設定が自動更新されます。
わざわざIPアドレスを調べる必要はありません。
まとめ:ロジックを制する者は自動化を制す
お疲れ様でした!
今回は、Ansibleを「自動化ツール」から「インフラ開発言語」へと昇華させるためのロジックとデータ操作を学びました。
今回の重要ポイント:
loop_controlを使って、変数を整理し、ネストに対応せよ。changed_when/failed_whenでステータスを自在に操れ。- 複雑な処理は
block/rescueでトランザクション化せよ。 mapやjson_queryを使えば、どんな複雑なデータも抽出できる。
これらのテクニックを使えば、「Ansibleでは無理だからシェルスクリプトで書こう…」という妥協を減らすことができます。
Playbookだけで完結させることで、可読性と保守性が向上します。
しかし、ロジックが複雑になると気になるのが「実行速度」です。
数千台のサーバーに対して複雑なループを回すと、Ansibleは驚くほど遅くなることがあります。
次回、応用講座 第3回は「爆速化チューニング。Forks、Pipelining、Mitogenによるパフォーマンス改善」です。
SSH接続のオーバーヘッドを極限まで減らし、並列実行数を最適化し、さらには「Mitogen」という魔法のプラグインを使って実行速度を数倍〜数十倍にするチューニング術を伝授します。
大規模環境のエンジニア必見です。お楽しみに!
▼ ロジック構築力を試す ▼
複雑なPlaybookを実験
「VPS」で自分専用環境
高度な自動化スキルを年収に
「ITエンジニア転職」

コメント