Ansibleのwhen条件・loop・tagsでPlaybookを制御する方法|環境ごとの分岐と対象タスクを絞り込む設計パターン

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Ansible > Ansibleのwhen条件・loop・tagsでPlaybookを制御する方法|環境ごとの分岐と対象タスクを絞り込む設計パターン
「本番サーバーとテスト環境で別々のパッケージをインストールしたい」
「ユーザーアカウントを10件、同じ設定で一括作成したい。コピペを繰り返すのはつらい」
「Playbookが100行を超えてきた。今日はNginxの設定変更だけ反映して、他のタスクはスキップしたい」
「設定ファイルの書き込みが失敗したとき、自動でバックアップから元に戻したい」

Ansibleを本格的に使い始めると、必ずこうした要求に直面します。単純な「命令を並べるだけ」のPlaybookは、環境差分への対応・複数リソースの一括操作・部分実行・エラー時の自動回復が必要になった瞬間に限界を迎えます。

この記事では、Ansibleの制御フロー3本柱であるwhen条件式(実行分岐)・loop繰り返し処理tags部分実行を体系的に解説します。さらにregister/set_factによる動的変数設計block・rescue・alwaysによるエラーハンドリング設計も含め、RHEL 10 / Rocky Linux 9の実行例とともに説明します。

この記事のポイント

・whenはOS種別・ファクト変数・register結果・グループ判定など多様な条件を記述できる
・loopはリスト・辞書を使った繰り返しで複数ユーザー・パッケージの一括操作を簡潔に書ける
・tagsで「インストールだけ」「設定だけ」を選択実行でき大規模Playbookの運用効率が上がる
・registerで保存した実行結果をset_factで加工することで動的な変数設計が実現できる
・block/rescue/alwaysはPythonのtry/except/finallyに相当するエラーハンドリング構造


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Ansibleの制御フローが必要になる理由|シンプルPlaybookの限界

Ansibleを使い始めた頃は、タスクを上から順に並べるだけで十分でした。しかし運用が進むにつれ、次のような「壁」に必ずぶつかります。

環境差分の壁: 本番はRHEL 10、開発はUbuntu 24.04。OSが違えばパッケージ名もサービス管理コマンドも違う。
繰り返しの壁: Webアプリ用ユーザーを5件作りたい。内容が少し違う5つのタスクをコピペするのは保守性がゼロ。
部分適用の壁: 設定ファイルだけ更新したい。インストール済みのパッケージに触りたくない。
変数管理の壁: コマンドの実行結果を後続タスクで使いたい。「インストール済みか確認して、なければインストール」という流れをPlaybookに組み込めない。
障害回復の壁: 設定変更が失敗した後、サービスが壊れたまま放置される。失敗時の自動ロールバックをPlaybookに組み込めない。

これら5つの壁を突き破るのがwhenlooptagsregister/set_factblock/rescue/alwaysです。5つを使いこなすことで、Playbookは「手順書の写し」から「設計として表現されたインフラの状態」に昇格します。

when条件式でタスクの実行を制御する方法

whenはタスクに実行条件を付けるキーワードです。条件がfalseのときはタスクがスキップされ、「changed」も「failed」も起きません。

1. Ansibleファクト変数を使ったOS別分岐

最も多く使われるパターンが、Ansibleが自動収集するファクト変数によるOS判定です。

--- - name: OSに応じたNginxインストール hosts: webservers tasks: - name: Nginxをインストールする(RHEL系) ansible.builtin.dnf: name: nginx state: present when: ansible_os_family == "RedHat" - name: Nginxをインストールする(Debian系) ansible.builtin.apt: name: nginx state: present when: ansible_os_family == "Debian"

実行すると、対象ホストのOSに合致しないタスクは「skipping」として表示されます。

TASK [Nginxをインストールする(RHEL系)] ***** ok: [web01.example.com] TASK [Nginxをインストールする(Debian系)] ***** skipping: [web01.example.com]

主要なファクト変数と使いどころは次のとおりです。

ansible_os_family: "RedHat" / "Debian" / "Suse" など。パッケージ管理の分岐に使う
ansible_distribution: "Rocky" / "AlmaLinux" / "Ubuntu" など。より細かい分岐に使う
ansible_distribution_major_version: "9" / "10" など。バージョン別対応に使う
ansible_hostname: ホスト名。特定ホストにのみ適用するタスクに使う

2. register変数を使った前タスクの結果による分岐

registerでタスクの結果を変数に格納し、後続タスクのwhenで参照するパターンです。既存の状態を確認してから次の処理を決める「チェックアンドアクト」の実装に使います。

--- - name: サービス状態を確認してから設定を反映する hosts: webservers tasks: - name: サービス情報を収集する ansible.builtin.service_facts: - name: httpdが起動中の場合のみ設定ファイルを更新する ansible.builtin.template: src: httpd.conf.j2 dest: /etc/httpd/conf/httpd.conf validate: "httpd -t -f %s" when: "'httpd.service' in ansible_facts.services and ansible_facts.services['httpd.service'].state == 'running'" notify: restart httpd - name: MySQLが存在するか確認する ansible.builtin.command: cmd: rpm -q mariadb-server register: mysql_check failed_when: false changed_when: false - name: MySQLが未インストールの場合のみインストールする ansible.builtin.dnf: name: mariadb-server state: present when: mysql_check.rc != 0

failed_when: falseを付けておくことで、コマンドが失敗してもPlaybookが止まらずにregisterに結果が保存されます。rc(終了コード)をwhenで評価するパターンは、「リソースの存在確認 → 条件付き作成」という冪等設計の定石です。Apacheの設定ファイル操作の詳細(タイムアウト値・KeepAlive等)についてはApache タイムアウト設定の詳細も参照してください。

3. グループ判定とカスタム変数による分岐

インベントリのグループや独自変数を使った分岐は、環境ごとの設定差分を管理する定番パターンです。

--- - name: 環境別セキュリティ設定 hosts: all tasks: - name: 本番環境のWebサーバーのみSELinuxをenforcingにする ansible.builtin.command: cmd: setenforce 1 when: - inventory_hostname in groups['webservers'] - server_env == "production" - name: ステージング以外では詳細ログを無効化する ansible.builtin.lineinfile: path: /etc/nginx/nginx.conf regexp: '^access_log' line: "access_log off;" when: server_env != "staging"

複数条件のリスト記法(暗黙のand)
whenをリスト形式で書くと、全条件のAND(論理積)になります。

# リスト形式 = すべての条件を満たす場合のみ実行(AND) when: - ansible_os_family == "RedHat" - ansible_distribution_major_version | int >= 9 - server_role is defined # OR条件は括弧で明示する when: server_role == "web" or server_role == "proxy"

registerとset_factで変数を動的に定義・加工する方法

when条件式の解説で見たように、registerはタスクの実行結果を変数に保存する仕組みです。保存された結果は辞書(ディクショナリ)構造を持ち、stdoutrcchangedなどのキーにアクセスして後続タスクの判断材料にします。さらにset_factを使うと、registerで得た生の結果を加工・変換した新しい変数を定義できます。2つを組み合わせると、タスク間のデータの流れを柔軟に設計できます。

1. register変数のデータ構造と主要キー

registerで保存される変数の中身を知るには、debugモジュールで全体を出力するのが一番の近道です。

- name: MySQLのデータベース一覧を取得する ansible.builtin.command: cmd: mysql -e "SHOW DATABASES;" --batch --skip-column-names register: mysql_db_list changed_when: false # register変数全体をdumpして構造を確認する - name: register変数の構造を確認する ansible.builtin.debug: var: mysql_db_list

実行すると次のような出力が得られます。

ok: [db01.example.com] => { "mysql_db_list": { "changed": false, "cmd": ["mysql", "-e", "SHOW DATABASES;", "--batch", "--skip-column-names"], "delta": "0:00:00.145231", "failed": false, "rc": 0, "stderr": "", "stderr_lines": [], "stdout": "information_schema\nmysql\nperformance_schema\nmyapp", "stdout_lines": [ "information_schema", "mysql", "performance_schema", "myapp" ] } }

よく使うキーと用途を整理します。

キー名 内容 よく使う用途
stdout 標準出力全体(文字列) 特定文字列が含まれるか判定(in演算子)
stdout_lines 標準出力を改行で分割したリスト 行単位でloopやフィルター処理
stderr 標準エラー出力(文字列) エラー内容の確認・通知
rc 終了コード(0が正常、1以上がエラー) when: 変数.rc != 0による条件分岐
changed タスクがchanged扱いになったか 「変更が発生した時だけ後続を実行」
failed タスクが失敗したか rescue内での失敗判定

2. set_factで変数を動的に定義・加工する

set_factはPlaybook実行中にホスト変数を動的に定義するモジュールです。registerで得た複雑な辞書構造から必要な値だけを抽出して、シンプルな変数として再定義するのが主な使い方です。

# Pythonのバージョンを取得する(出力例: "Python 3.11.4") - name: Pythonバージョンを確認する ansible.builtin.command: cmd: python3 --version register: python_version_raw changed_when: false # set_factで必要な数値部分だけを抽出・変換する - name: Pythonバージョン文字列を整形する ansible.builtin.set_fact: python_version: "{{ python_version_raw.stdout.split(' ')[1] }}" python_major: "{{ python_version_raw.stdout.split(' ')[1].split('.')[0] | int }}" # 整形した変数を条件分岐で使う - name: Python 3.10以上の場合のみパッケージをインストールする ansible.builtin.pip: name: tomllib state: present when: python_major | int >= 3

stdout.split(' ')[1]のようなJinja2フィルターを組み合わせることで、コマンド出力から必要な情報を切り出せます。| intは文字列を整数に変換するAnsible標準フィルターです。数値比較をする場合は必ずこの変換を行ってください。

ディスク使用率の取得と閾値判定も、同じパターンで実装できます。

# ルートパーティションの使用率を取得する - name: ディスク使用率を取得する ansible.builtin.command: cmd: df / --output=pcent register: disk_usage_raw changed_when: false # 出力例(2行: ヘッダ + 値) # Use% # 72% # 末尾の%を除いて整数に変換する - name: ディスク使用率を整形する ansible.builtin.set_fact: disk_usage_pct: "{{ disk_usage_raw.stdout_lines[1] | trim | replace('%', '') | int }}" # 80%以上なら警告を出す - name: ディスク使用率が高い場合に警告を出す ansible.builtin.debug: msg: "警告: ディスク使用率が {{ disk_usage_pct }}% に達しています。クリーンアップを検討してください。" when: disk_usage_pct | int >= 80

| trimは前後の空白を除去するJinja2フィルター、| replace('%', '')は文字列置換です。この3段の変換(trim → replace → int)で数値として扱えるようになります。

3. loopとregisterを組み合わせる(.resultsリスト)

loopregisterを組み合わせると、全ループ分の結果が変数名.resultsにリスト形式で格納されます。各要素にはitem.item(ループの元データ)とitem.rc(終了コード)などが含まれます。

# 複数サービスの稼働状態を一括確認する - name: 各サービスの稼働状態を確認する ansible.builtin.command: cmd: systemctl is-active {{ item }} loop: - nginx - mysqld - redis register: service_status failed_when: false changed_when: false # 停止しているサービスだけを起動する - name: 停止サービスを起動する ansible.builtin.systemd: name: "{{ item.item }}" state: started loop: "{{ service_status.results }}" when: item.rc != 0 loop_control: label: "{{ item.item }}"

service_status.resultsはリストであり、各要素が1回のループ実行結果です。item.itemでループの元データ(サービス名)、item.rcで終了コードにアクセスします。この構造を使うと「失敗したもの・条件に合致するものだけに後続処理をかける」という設計が実現します。

registerとset_factを使った動的変数設計を実機で体験したい方は、>> Ansibleハンズオン講座の詳細を見る をご覧ください。

loopで繰り返し処理を設計する方法

loopはAnsible 2.5以降の推奨繰り返し記法です。旧来のwith_itemsと同じ役割ですが、より統一的に使えます。

1. シンプルリストのloop(パッケージ一括インストール)

- name: 必要なパッケージをまとめてインストールする ansible.builtin.dnf: name: "{{ item }}" state: present loop: - nginx - mariadb-server - php-fpm - firewalld

ただしdnfモジュールはnameにリストを直接渡す書き方の方が効率的です。loopは1パッケージごとにモジュールを1回起動しますが、リスト渡しは1回のdnf呼び出しで済みます。

# より効率的な書き方(loopより推奨) - name: 必要なパッケージをまとめてインストールする ansible.builtin.dnf: name: - nginx - mariadb-server - php-fpm - firewalld state: present

loopが本領を発揮するのは、「タスク単位で1リソースしか扱えないモジュール」(userモジュールなど)での繰り返しです。

2. 辞書リストのloop(ユーザー一括作成)

辞書(キーと値のペア)のリストをloopに渡すと、item.キー名で各フィールドにアクセスできます。

- name: Webアプリ用ユーザーを一括作成する ansible.builtin.user: name: "{{ item.name }}" groups: "{{ item.groups }}" comment: "{{ item.comment }}" state: present loop: - { name: "appuser", groups: "nginx", comment: "Web Application User" } - { name: "deployer", groups: "wheel,nginx", comment: "Deploy Bot User" } - { name: "monitor", groups: "systemd-journal", comment: "Monitoring Agent" } # 実行結果の例 TASK [Webアプリ用ユーザーを一括作成する] ***** changed: [web01.example.com] => (item={'name': 'appuser', 'groups': 'nginx', ...}) changed: [web01.example.com] => (item={'name': 'deployer', 'groups': 'wheel,nginx', ...}) changed: [web01.example.com] => (item={'name': 'monitor', 'groups': 'systemd-journal', ...})

3. loop_control.labelで出力を見やすくする

辞書リストのloopはデフォルトだと全フィールドが出力されて見づらいです。loop_control.labelで表示内容を絞り込めます。

- name: Nginxバーチャルホストの設定ファイルを配置する ansible.builtin.template: src: "vhost.conf.j2" dest: "/etc/nginx/conf.d/{{ item.domain }}.conf" loop: - { domain: "app.example.com", port: 8080, upstream: "app_pool" } - { domain: "api.example.com", port: 8081, upstream: "api_pool" } - { domain: "admin.example.com",port: 8082, upstream: "admin_pool" } loop_control: label: "{{ item.domain }}" # ← ドメイン名だけ表示 # label指定後の出力(すっきり) changed: [web01] => (item=app.example.com) changed: [web01] => (item=api.example.com) changed: [web01] => (item=admin.example.com)

4. loopとwhenを組み合わせる

loopwhenを組み合わせると、「リストの中から条件に合うものだけ処理する」ことができます。

- name: 本番環境用ユーザーのみ作成する ansible.builtin.user: name: "{{ item.name }}" state: present loop: - { name: "appuser", env: "production" } - { name: "testuser", env: "staging" } - { name: "devuser", env: "development"} when: item.env == server_env loop_control: label: "{{ item.name }} ({{ item.env }})"

loopとwhenのパターンを実機で試したい方は、>> Ansibleハンズオン講座の詳細を見る をご覧ください。

tagsでPlaybookを部分実行する設計パターン

tagsはタスク・プレイ・roleに「ラベル」を付ける機能です。実行時に--tags--skip-tagsを指定することで、Playbook全体から特定の処理だけを選んで実行できます。

1. タグの付け方(タスクレベルとプレイレベル)

--- - name: Webサーバーのフルセットアップ hosts: webservers tags: webserver # ← プレイ全体にタグを付ける tasks: - name: Nginxをインストールする ansible.builtin.dnf: name: nginx state: present tags: [install, nginx] # ← タスクに複数のタグを付ける - name: Nginx設定ファイルを配置する ansible.builtin.template: src: nginx.conf.j2 dest: /etc/nginx/nginx.conf tags: [config, nginx] notify: nginx restart - name: Nginxを起動・自動起動有効にする ansible.builtin.service: name: nginx state: started enabled: true tags: [service, nginx] - name: ログ収集用の監視エージェントをインストールする ansible.builtin.dnf: name: node_exporter state: present tags: [install, monitoring]

2. --tagsと--skip-tagsで実行対象を絞り込む

# インストールタスクだけ実行する ansible-playbook site.yml --tags install # 設定ファイル更新だけ実行する(サービス再起動なし) ansible-playbook site.yml --tags config # Nginx関連タスクだけ実行する ansible-playbook site.yml --tags nginx # 複数のタグを指定する(いずれかに一致するタスクを実行) ansible-playbook site.yml --tags "install,config" # 監視エージェントをスキップしてWebサーバーのみセットアップ ansible-playbook site.yml --skip-tags monitoring # 利用可能なタグを事前確認する(実行はしない) ansible-playbook site.yml --list-tags # 出力例 play #1 (webservers): Webサーバーのフルセットアップ TASK TAGS: [config, install, monitoring, nginx, service, webserver]

3. alwaysとneverの特殊タグ

alwaysタグを付けたタスクは、--skip-tagsでも除外されず必ず実行されます。逆にneverタグを付けたタスクは、--tags neverを明示しない限り実行されません。

- name: 実行前の事前確認(常に実行する) ansible.builtin.debug: msg: "対象ホスト: {{ inventory_hostname }}, 環境: {{ server_env }}" tags: always # ← どんな--skip-tagsでも必ず実行 - name: 本番環境の全データを削除する(明示的に指定した時のみ実行) ansible.builtin.file: path: /var/lib/app/data state: absent tags: never # ← --tags neverを指定した時のみ実行(誤実行防止)

alwaysタグの典型的なユースケースは「前提確認タスク」(Ansibleバージョンの確認・ファクト収集・必須変数のバリデーション)です。neverタグは「通常は実行しない危険な処理」(DBリセット・全ファイル削除)の誤実行防止に使います。

4. タグ設計のベストプラクティス

タグは「多すぎても少なすぎても使いにくい」ツールです。実務では次の2階層設計が管理しやすいです。

機能タグ(何をするか): install / config / service / cleanup
コンポーネントタグ(何に対して): nginx / mysql / firewalld / monitoring

この設計だと「Nginxの設定だけ変更する」は--tags "config,nginx"、「サービス起動系を全部スキップ」は--skip-tags serviceと直感的に指定できます。

when・loop・tagsを組み合わせた実務設計パターン

3つを組み合わせた実例として、「本番・ステージング・開発の3環境に対して、複数のWebアプリユーザーを環境別条件でセットアップする」Playbookを示します。

--- - name: 環境別Webアプリセットアップ hosts: all vars: app_users: - { name: "appuser", env: "production", uid: 1001 } - { name: "stagingapp",env: "staging", uid: 1002 } - { name: "devapp", env: "development", uid: 1003 } tasks: - name: OS別にNginxをインストールする ansible.builtin.package: name: nginx state: present when: ansible_os_family in ["RedHat", "Debian"] tags: [install, nginx] - name: 対象環境のWebアプリユーザーを作成する ansible.builtin.user: name: "{{ item.name }}" uid: "{{ item.uid }}" state: present loop: "{{ app_users }}" when: item.env == server_env loop_control: label: "{{ item.name }} ({{ item.env }})" tags: [setup, users] - name: 本番環境のみSSHパスワード認証を無効化する ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^PasswordAuthentication' line: "PasswordAuthentication no" when: - ansible_os_family == "RedHat" - server_env == "production" notify: restart sshd tags: [security, ssh, config] - name: ファイアウォールでHTTP・HTTPSを開放する ansible.builtin.command: cmd: "firewall-cmd --add-service={{ item }} --permanent" loop: - http - https when: inventory_hostname in groups['webservers'] tags: [config, firewalld]

実行時は次のように環境変数とタグを組み合わせてコントロールします。

# 本番環境でインストールとユーザー作成のみ実行 ansible-playbook site.yml -e "server_env=production" --tags "install,setup" # ステージングでセキュリティ設定だけ更新 ansible-playbook site.yml -e "server_env=staging" --tags security

ファイアウォールのfirewall-cmdコマンドの動作確認にはLinux ポート確認の全コマンド(ss・lsofによる開放確認)も活用してください。Ansibleタスクが成功しても実際にポートが開いているかはssコマンドで確認するのが確実です。

block・rescue・alwaysでエラーハンドリングを追加する方法

when・loop・tagsでPlaybookを構造化しても、タスクが失敗したときの処理まで書かなければ本番運用には耐えられません。「設定ファイルの書き込みが失敗したのにサービスが再起動されてしまった」「一部のホストで失敗したのに展開が続いてしまった」——こうした障害を防ぐのがblockrescuealwaysです。Pythonのtry / except / finallyに相当するAnsibleのエラーハンドリング構造です。

1. block・rescue・alwaysの基本構造

# block/rescue/always の基本構造 - name: Webサーバー設定の適用(エラーハンドリング付き) block: - name: 設定ファイルをデプロイする ansible.builtin.template: src: httpd.conf.j2 dest: /etc/httpd/conf/httpd.conf owner: root group: root mode: '0644' - name: 設定の構文チェックを実行する ansible.builtin.command: cmd: httpd -t changed_when: false - name: httpdを再起動する ansible.builtin.service: name: httpd state: restarted rescue: - name: 設定ファイルをバックアップから復元する ansible.builtin.copy: src: /etc/httpd/conf/httpd.conf.bak dest: /etc/httpd/conf/httpd.conf remote_src: yes - name: httpdを元の設定で再起動する ansible.builtin.service: name: httpd state: restarted - name: デプロイ失敗を明示的に通知する ansible.builtin.fail: msg: "rollback completed, but original deploy failed on {{ inventory_hostname }}" always: - name: 一時ファイルを削除する ansible.builtin.file: path: /tmp/httpd_deploy_lock state: absent ignore_errors: yes

実行の流れを整理すると次のようになります。

block: 複数タスクをグループ化する。block内のいずれかのタスクが失敗するとrescueに制御が移る
rescue: 失敗時のみ実行される回復処理。rescueが正常終了するとPlaybookは「回復した」とみなして次へ続行する。「ロールバックは成功したがデプロイ自体は失敗した」として後続を止めたい場合は末尾にansible.builtin.failを必ず入れる
always: block成功・rescue完了のどちらでも必ず実行されるクリーンアップ処理。always内タスクも失敗しうるためignore_errors: yesを添えて波及を防ぐ

一点注意が必要なのは、rescue自体がエラーになるとPlaybookはその時点でエラー終了するという点です。rescue内の処理はできるだけシンプルに保ち、複雑なリカバリが必要な場合はrescue内でさらにblockを使う設計にしてください。

2. ansible_failed_task・ansible_failed_resultで失敗情報を参照する

rescueブロック内では、失敗情報を参照するための特殊変数が2つ利用できます。

変数名 内容
ansible_failed_task 失敗したタスクの情報(name・action・args等)
ansible_failed_result 失敗したタスクの実行結果(msg・rc・stderr等)

これらを使うと、失敗の種類に応じた分岐処理を書けます。

rescue: - name: 失敗の詳細を表示する ansible.builtin.debug: msg: > 失敗したタスク: {{ ansible_failed_task.name }} 終了コード: {{ ansible_failed_result.rc | default('N/A') }} エラー内容: {{ ansible_failed_result.msg | default('詳細なし') }} - name: 終了コードが2の場合はディスク不足として処理する ansible.builtin.command: cmd: /usr/local/bin/cleanup-disk.sh when: ansible_failed_result.rc is defined and ansible_failed_result.rc == 2

現場では、エラーの種類によってリカバリ手順が変わるケースが多いため、ansible_failed_result.rcansible_failed_result.stderr を条件判断に使うパターンは特に有効です。

3. blockをネストして処理スコープを分ける

blockは入れ子にできます。内側のrescueが処理できなかったエラーは外側のblockへ伝播します。

- name: 外側のblock(全体の安全ネット) block: - name: 内側のblock(バックアップフェーズ) block: - name: データベースバックアップ ansible.builtin.command: cmd: "mysqldump -u root mydb > /tmp/mydb.sql" rescue: - name: バックアップ失敗を通知して処理を中止する ansible.builtin.fail: msg: "バックアップなしに本番作業は続行できません" rescue: - name: 全体処理の失敗を記録する ansible.builtin.debug: msg: "バックアップを含む全体処理が失敗しました。手動での確認が必要です"

注意点として、内側のblockが自身のrescueを持っている場合、内部でエラーが完結し外側のrescueには伝播しません。意図しない「エラーの握り潰し」につながるため、ネスト構造を設計する際は各blockのrescueがどこでエラーを止めるかを意識してください。また、ネストを深くすると可読性が下がるため、ロールやinclude_tasksへの分離と組み合わせてフラットな構造を保つことを推奨します。

4. ignore_errors・failed_when・any_errors_fatalとblock/rescue/alwaysの使い分け

Ansibleにはエラー制御のオプションが複数あります。用途を混同するとPlaybookが「止まるだけ」か「何でも無視する」の二択になり、本番運用の設計として脆くなります。

オプション 動作 向いているシーン
ignore_errors: yes エラーを無視して次のタスクへ進む 「存在しなければスキップ」など、失敗が正常ケースに含まれる場合
failed_when タスクの成功・失敗条件を独自に定義する コマンドの終了コードが独自仕様の場合(例: 正常でも1を返すツール)
any_errors_fatal: yes 1台のホストでエラーが出たら全ホストを即停止 ローリング更新中に1台の失敗を検知したら全体を止めたい場合
block/rescue/always 失敗時のリカバリ処理と後処理を構造的に定義する ロールバックや後処理が必要なすべてのシーン

判断フローとしては次のように考えると整理しやすいです。

・後始末が不要でエラーを単純に無視したいだけ → ignore_errors: yes
・タスクの成功条件をカスタム定義したい → failed_when / changed_when
・複数台を一斉停止させたい → any_errors_fatal: yes
・失敗時にリカバリや通知を実行したい → block/rescue/always

# ignore_errors: 特定タスクのみエラーを無視して続行する - name: 既存のcronジョブを削除する(存在しなくても続行) ansible.builtin.cron: name: "old-backup-job" state: absent ignore_errors: yes # failed_when: 失敗条件を自分で定義する - name: プロセスの起動確認(grepはプロセスなし時にrc=1を返す) ansible.builtin.command: cmd: pgrep -x httpd register: httpd_check failed_when: httpd_check.rc != 0 and httpd_check.rc != 1 # rc=0: プロセスあり、rc=1: プロセスなし(想定内)、それ以外: 異常 # changed_when: commandモジュールの変更判定を自分で制御する - name: カスタムスクリプトで設定を変更する ansible.builtin.command: cmd: /opt/scripts/update-config.sh register: update_result changed_when: "'Updated' in update_result.stdout" # 出力にUpdatedが含まれる場合のみchangedとして扱う # changed_when: false で副作用なしチェックコマンドをok扱いにする - name: 設定の整合性チェックを実行する ansible.builtin.command: cmd: /opt/scripts/check-config.sh changed_when: false

5. パッチ適用失敗時のロールバック実装パターン

現場でよく出る要件が「パッケージを更新して動作確認し、問題があれば元のバージョンに戻す」というパターンです。block/rescue/alwaysを使うと安全に実装できます。

--- - name: httpdのパッチ適用とロールバック設計 hosts: webservers gather_facts: yes tasks: - name: httpd パッチ適用ブロック block: # 更新前バージョンを記録(ロールバック用) - name: 現在のhttpdバージョンを取得する ansible.builtin.command: cmd: rpm -q httpd register: httpd_version_before changed_when: false # パッケージ更新 - name: httpdを最新バージョンに更新する ansible.builtin.dnf: name: httpd state: latest # サービス再起動 - name: httpdを再起動する ansible.builtin.service: name: httpd state: restarted # 動作確認(200が返れば正常) - name: Webサーバーの応答を確認する ansible.builtin.uri: url: "http://{{ inventory_hostname }}/server-status" status_code: 200 retries: 3 delay: 5 rescue: - name: 失敗タスクと原因を記録する ansible.builtin.debug: msg: > パッチ適用失敗: {{ ansible_failed_task.name }} エラー内容: {{ ansible_failed_result.msg | default('詳細なし') }} # httpd_version_beforeが未定義の場合(バージョン取得自体が失敗)はスキップ - name: ロールバック(旧バージョンに戻す) ansible.builtin.dnf: name: "{{ httpd_version_before.stdout }}" state: present when: httpd_version_before is defined and httpd_version_before.rc == 0 - name: httpdを再起動する(ロールバック後) ansible.builtin.service: name: httpd state: restarted always: - name: 最終的なhttpdの稼働状態を確認する ansible.builtin.service_facts: - name: 適用結果を表示する ansible.builtin.debug: msg: "httpd の最終状態: {{ ansible_facts.services['httpd.service'].state }}"

このロールバック設計で重要な点がwhen: httpd_version_before is definedの条件です。もし最初の「バージョン取得タスク」自体が失敗した場合、httpd_version_beforeは未定義になります。この条件がないと変数が未定義のままロールバックタスクが実行されてエラーになるため、is definedチェックが欠かせません。

6. rescue・always間の変数スコープ

blockで定義した変数はrescueとalwaysの両方で参照できます。さらにrescue内でregisterした変数もalwaysから参照可能です。この変数スコープを活用すると、alwaysの通知内容にrescueの実行結果を含める設計ができます。

rescue: - name: ロールバックを実行する ansible.builtin.command: cmd: /usr/local/bin/rollback.sh register: rollback_result always: - name: ロールバック結果をalwaysから参照して表示する ansible.builtin.debug: msg: > ロールバック実行: {{ rollback_result is defined }} ロールバック結果: {{ rollback_result.rc | default('未実行') }}

block/rescue/alwaysを使った本番対応のPlaybook設計を実機で体験したい方は、>> Ansibleハンズオン講座の詳細を見る をご覧ください。

トラブルシュート|when・loop・tagsでよくある失敗と対処法

1. when条件が期待通りに評価されない

症状: ansible_distribution_major_version == 9 と書いたのに条件が一致しない。

原因と対処: ファクト変数が文字列として返ってくるケースがあります。整数比較が必要な場合はJinja2フィルタで変換します。

# NG: 文字列と整数を比較しているため期待通りに動かないことがある when: ansible_distribution_major_version == 9 # OK: | int フィルタで整数に変換してから比較する when: ansible_distribution_major_version | int == 9 # OK: 文字列として比較する when: ansible_distribution_major_version == "9"

2. loopとregisterの結果がうまく取得できない

症状: loopの中でregisterした変数を次のタスクで参照すると、最後のループ結果しか入っていない。

原因と対処: loopと組み合わせたregisterは、変数名.resultsにリスト形式で全ループ結果が格納されます。後続タスクでは.resultsをループ対象にしてitem.item(元データ)・item.rc(終了コード)・item.stdout(標準出力)にアクセスします。

# 複数サービスの確認結果をregisterで保存する - name: 各サービスの稼働状態を確認する ansible.builtin.command: cmd: systemctl is-active {{ item }} loop: - nginx - mysqld - redis register: service_check failed_when: false changed_when: false # .results リストをループして停止サービスだけ起動する - name: 停止サービスを起動する ansible.builtin.systemd: name: "{{ item.item }}" # item.item = 元のループデータ(サービス名) state: started loop: "{{ service_check.results }}" when: item.rc != 0 # item.rc = そのループ回の終了コード loop_control: label: "{{ item.item }}"

3. tagsがinclude_tasksで効かない

症状: include_tasksでインクルードしたファイル内のタスクにtags: configを付けても、--tags configで実行されない。

原因: include_tasksは動的インクルードのため、Playbookの解析時点ではタグが認識されません。静的インクルードのimport_tasksを使うか、include_tasks自体にタグを付けます。

# import_tasks(静的): インクルード先のタグが --tags で認識される - name: Nginx設定タスクをインクルードする ansible.builtin.import_tasks: tasks/nginx_config.yml # include_tasks(動的): include自体にタグを付けることで対応する - name: Nginx設定タスクをインクルードする ansible.builtin.include_tasks: tasks/nginx_config.yml tags: [config, nginx]

4. rescue後にPlaybookが続行してしまう

症状: block内のタスクが失敗してrescueが動いたが、rescueが正常終了したのでPlaybookが次のタスクへ進んでしまった。

原因と対処: rescueが正常終了するとAnsibleは「回復した」と判断し、後続タスクへ進みます。「ロールバックは成功したがデプロイ自体は失敗した」として後続を停止させたい場合は、rescueの末尾にansible.builtin.failを明示的に入れてください。

rescue: - name: ロールバック処理を実行する ansible.builtin.copy: src: /etc/app/config.bak dest: /etc/app/config remote_src: yes # これがないとrescue後にPlaybookが続行される(要注意) - name: デプロイ失敗を明示的に通知する ansible.builtin.fail: msg: "Deployment failed and rolled back on {{ inventory_hostname }}"

5. alwaysのタスクが失敗するとPlaybookがエラーになる

症状: alwaysに書いた後処理が失敗し、blockもrescueも正常だったのにPlaybook全体がエラーになった。

原因と対処: alwaysは必ず実行されますが、always内のタスクが失敗するとPlaybookはエラーになります。「後処理が失敗してもPlaybook全体は正常終了にしたい」という要件には、alwaysの各タスクにignore_errors: yesを付けるか、always内でもさらにblock/rescueを使う設計が必要です。

always: # 通知タスクが失敗してもPlaybook全体の結果に影響させない - name: 結果を管理者へ通知する ansible.builtin.uri: url: "https://hooks.example.com/notify" method: POST body: "{{ inventory_hostname }}: deploy completed" ignore_errors: yes - name: 一時ファイルを削除する ansible.builtin.file: path: /tmp/deploy_lock state: absent ignore_errors: yes

6. register変数が「undefined」になるケースと対処

症状: registerした変数を後続タスクで参照しようとしたが、「変数 'xxx' is undefined」というエラーが出る。

原因1: registerするタスク自体がwhen条件でスキップされた
when条件でスキップされたタスクはregisterも実行されません。変数を参照する側のwhen条件にis definedチェックを追加してください。

# スキップ対策: is defined チェックを追加する - name: バックアップのステータスを報告する ansible.builtin.debug: msg: "バックアップ完了: {{ backup_result.stdout }}" when: - backup_result is defined - backup_result.rc == 0

原因2: register変数は定義したplay内でのみ有効
registerで保存した変数はPlaybook実行中のメモリ上に存在し、そのplay内でのみ参照できます。別のplayや別ファイルのPlaybookからアクセスするにはhostvars['ホスト名']['変数名']を経由するか、set_factでplay間共有可能な形に整えてください。
NTPサーバー設定に関連する時刻同期の詳細については、ntpd 時刻同期設定も参照してください。Ansibleのchronyやntpdインストール処理でwhen条件(OS判定)と組み合わせるケースはよくあるパターンです。

本記事のまとめ

やりたいこと 使う機能と書き方
OS種別でタスクを分岐させる when: ansible_os_family == "RedHat"
コマンド結果で次のタスクを制御する register: 変数名 + when: 変数名.rc == 0
複数の条件をすべて満たす場合に実行する when: にリスト形式で条件を並べる(暗黙のand)
タスクの実行結果の内部構造を確認する debug: var: 変数名 でdump出力
コマンド出力から値を抽出して変数化する set_fact + Jinja2フィルター(split・replace・int)
同じ処理を複数のリソースに適用する loop: [item1, item2, ...] + {{ item }}
辞書リストで複数フィールドを繰り返す loop: [{name: x, value: y}, ...] + {{ item.name }}
loop出力を見やすくする loop_control: {label: "{{ item.name }}"}
loopの全結果を後続タスクで処理する register + loop変数名.results をloopに渡す
インストールだけ・設定だけを実行する tags: [install, config] + ansible-playbook --tags install
特定のコンポーネントだけスキップする ansible-playbook --skip-tags monitoring
常に実行するタスクを指定する tags: always
明示的に指定した時のみ実行する tags: never
失敗時に自動でロールバックする blockで処理をまとめ、rescueに回復処理を書く
成否に関わらず後処理を必ず実行する alwaysにクリーンアップ処理を書く(ignore_errors: yesも添える)
特定タスクのエラーを無視して続行する ignore_errors: yes
コマンドの失敗・成功条件を自分で定義する failed_when: 変数.rc != 0またはchanged_when: false
rescue内で失敗タスクの情報を参照する ansible_failed_task.name / ansible_failed_result.rc
rescue内で取得した結果をalwaysから参照する rescue内でregisterした変数はalwaysスコープでそのまま参照できる
register変数が未定義エラーになるのを防ぐ when: 変数名 is definedチェックを必ず追加する
when・loop・tagsの3つをマスターすると、Playbookの品質が一段階上がります。「OS別に分岐する」「複数リソースを一括操作する」「特定の処理だけ実行する」——これらは実務で毎日のように必要になる操作です。さらにregister/set_factによる動的変数設計とblock・rescue・alwaysによるエラーハンドリングを加えることで、本番サーバーへの展開でも安全に自動化できる設計になります。最初は1つずつ試して、徐々に組み合わせていくのが習得の近道です。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
Ansible実践ハンズオンの詳細を見る >>

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。