「Ansibleで設定ファイルを配布した後、サービスのreloadを毎回手動でやっている」
こんな悩みは、Ansibleを使い始めたエンジニアが必ずぶつかる壁だ。copyモジュールで静的ファイルを配布するだけでは、環境ごとの差異(ホスト名・ポート番号・SSL設定の有無など)を吸収できない。設定変更のたびに手動でサービスを再起動するのは自動化の恩恵を半分しか受けていない状態だ。
この記事では、Ansibleの templateモジュール(Jinja2テンプレートで設定ファイルを動的生成)と handler/notify(設定変更を検知してサービスを自動再起動)の仕組みを、httpd.confを題材に解説する。さらに flush_handlers(handler即時実行)・listen(疎結合な通知設計)・handler連鎖(Ansible 2.9以降)まで踏み込み、実務で使えるPlaybook設計の全体像を網羅する。動作確認環境: RHEL 10 / Rocky Linux 9・Ansible 2.17。概念から実際のPlaybook設計まで順を追って説明する。
この記事のポイント
・templateモジュールはJinja2で変数・条件分岐を埋め込める設定ファイル配布モジュール
・handlerはnotify時のみPlay終了後に1回だけ実行される通知型タスク
・flush_handlersでhandlerを即時実行でき、後続taskが再起動後サービスに依存する場合に使う
・listenで複数taskから1つのhandlerを疎結合に呼び出せる
・handler連鎖(Ansible 2.9以降)でhandlerが別のhandlerをnotifyできる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
copyモジュールではなくtemplateモジュールを使うべき理由
Ansibleで設定ファイルを配布するとき、最初はcopyモジュールを使いがちだ。copyモジュールはローカルのファイルをそのままリモートサーバーへ転送する。シンプルで理解しやすいが、「環境ごとに内容を変えたい」という要件が出た途端に限界が来る。たとえば、次のような状況を考えてほしい。
・開発環境のhttpd.conf:
ServerName dev01.example.com・本番環境のhttpd.conf:
ServerName www.example.comこの差異をcopyモジュールで管理しようとすると、ファイルを環境の数だけ用意してwhen条件で分岐させるか、変数ファイルで制御するか、いずれにせよ構成が複雑になっていく。そこで登場するのがtemplateモジュールだ。
copyとtemplateの違いは1点だけ
両モジュールの機能はほぼ同じ。決定的な違いは Jinja2レンダリングの有無だ。| モジュール | 転送方法 | Jinja2変数埋め込み | 用途 |
|---|---|---|---|
| copy | バイナリそのまま転送 | 不可 | 画像・バイナリ・固定テキスト |
| template | レンダリング後に転送 | 可能(.j2ファイル) | 環境依存の設定ファイル |
.j2 のJinja2テンプレートファイルを用意する。Ansibleは実行時にそのファイルをレンダリング(変数展開・条件分岐の解決)してからリモートサーバーへ転送する。テンプレートファイルはAnsibleコントロールノード(自分のPC)側に置き、生成済みのファイルをリモートへ送る、という流れだ。この仕組みにより「1つのテンプレートファイルから複数環境の設定を生成する」ことが可能になる。
Jinja2テンプレートの基本構文|変数・条件分岐・フィルターを使いこなす
Jinja2はPython製のテンプレートエンジンで、AnsibleはこれをPlaybook変数と統合している。設定ファイル(.j2)に以下の構文を埋め込むことで動的な内容を生成できる。1. 変数の埋め込み({{ }})
最も基本の構文だ。二重波括弧の中にPlaybook変数名を書く。# httpd.conf.j2(変数展開の例) ServerName {{ ansible_hostname }} ServerAdmin {{ server_admin }} Listen {{ http_port }}
vars や group_vars に定義した変数が展開される。ansible_hostname はAnsibleが自動収集するマジック変数(インベントリのホスト名)だ。実行時にはサーバーごとのホスト名に自動で置き換わる。2. 条件分岐({% if %})
環境によって設定内容を切り替えたいときに使う。# SSL設定を本番環境のみ有効化する例 {% if enable_ssl | default(false) %} SSLEngine on SSLCertificateFile {{ ssl_cert_path }} SSLCertificateKeyFile {{ ssl_key_path }} {% endif %}
enable_ssl が true の場合のみSSL設定ブロックが生成される。| default(false) はフィルター(次節参照)でデフォルト値を設定している。開発環境ではSSLを無効のまま、本番環境ではgroup_varsで enable_ssl: true を設定する、という使い方が典型的だ。3. フィルター(|)
変数をパイプ(|)でつないで加工できる。よく使うフィルターを示す。・
{{ http_port | default(80) }}: 変数が未定義なら80を使う・
{{ server_name | upper }}: 大文字に変換・
{{ items | join(',') }}: リストをカンマ区切りの文字列に変換・
{{ path | basename }}: パスからファイル名を取り出す特に
| default() は必須のフィルターだ。変数が未定義のままテンプレートに記述すると実行時にエラーになる(AnsibleUndefinedVariable)。デフォルト値を設定しておけばグループ全体で安全に使いまわせる。templateモジュールでhttpd.confを動的生成する実践例
実際のディレクトリ構成から、Playbook・テンプレートの記述、実行結果まで順を追って説明する。1. ディレクトリ構成
site.yml inventory/ prod/ ← 本番環境インベントリ dev/ ← 開発環境インベントリ group_vars/ webservers.yml ← 共通変数 prod.yml ← 本番環境変数(enable_ssl: true等) roles/ apache/ tasks/ main.yml templates/ httpd.conf.j2 ← Jinja2テンプレート handlers/ main.yml
2. Jinja2テンプレート(roles/apache/templates/httpd.conf.j2)
# httpd.conf.j2 — Jinja2変数・条件分岐は実行時に展開される ServerName {{ ansible_hostname }} ServerAdmin {{ server_admin | default('webmaster@localhost') }} Listen {{ http_port | default(80) }} DocumentRoot {{ document_root | default('/var/www/html') }}
Options Indexes FollowSymLinks AllowOverride {{ allow_override | default('None') }} Require all granted {% if enable_ssl | default(false) %} Listen 443ServerName {{ ansible_fqdn }} SSLEngine on SSLCertificateFile {{ ssl_cert_path }} SSLCertificateKeyFile {{ ssl_key_path }} {% endif %}
3. タスクファイル(roles/apache/tasks/main.yml)
--- - name: httpd.confをテンプレートから配布する ansible.builtin.template: src: httpd.conf.j2 dest: /etc/httpd/conf/httpd.conf owner: root group: root mode: '0644' validate: '/usr/sbin/httpd -t -f %s' notify: Reload Apache
validate パラメーターに注目してほしい。httpd -t -f は設定ファイルの構文チェックを行うコマンドで、これが失敗するとAnsibleはファイルを転送しない。誤った設定を本番サーバーに配布するリスクを防ぐ重要なオプションだ。notify: Reload Apache は次章のhandlerを呼び出す。Apacheの設定項目の詳細(タイムアウト値やKeepAliveの調整など)については、Apache タイムアウト設定の詳細も参考にしてほしい。
4. 実行結果(RHEL 10 / Rocky Linux 9 での出力例)
本番サーバー(web01.example.com)で初回適用した際の出力を示す。# ansible-playbook site.yml -i inventory/prod 実行時の出力 TASK [apache : httpd.confをテンプレートから配布する] ** changed: [web01.example.com] RUNNING HANDLERS ************************************ TASK [apache : Reload Apache] *********************** changed: [web01.example.com] PLAY RECAP ****************************************** web01.example.com : ok=4 changed=2 unreachable=0 failed=0
changed: [web01.example.com] が ok: [web01.example.com] に変わり、handlerは呼ばれない。これがAnsibleの冪等性(べき等性)だ。同じPlaybookを何度実行しても、変更がなければサービスへの影響はゼロになる。templateとhandlerを組み合わせたPlaybook設計を実機で体験したい方は、>> Ansibleハンズオン講座の詳細を見る をご覧ください。
handlerとnotifyの仕組み|設定変更時だけサービスを再起動する
handlerはAnsibleにおける「通知受信型のタスク」だ。通常のタスクとの違いを理解することが、Playbook設計の品質を大きく左右する。1. handlerが解決する問題
設定ファイルを変更した後は必ずサービスをreloadまたはrestartしなければならない。しかし通常のタスクとしてサービス再起動を毎回書くと、2つの問題が発生する。・不必要なサービス再起動: 設定ファイルに変更がなくてもサービスが再起動される。本番環境では瞬断リスクがある
・重複再起動: httpd.confとssl.confを別タスクで配布した場合、サービスが2回再起動される
handlerはこの両方を解決する。「changedになった時だけ」「Play終了後に1回だけ」実行される点が通常のタスクとの決定的な違いだ。
2. handlerの書き方(roles/apache/handlers/main.yml)
--- - name: Reload Apache ansible.builtin.service: name: httpd state: reloaded - name: Restart Apache ansible.builtin.service: name: httpd state: restarted
handlers/main.yml(Role構成の場合)またはPlaybook内の handlers: セクションに定義する。name がhandlerの識別子で、タスク側の notify: と完全一致させる必要がある(大文字・小文字・スペースを含む)。3. 複数タスクから同じhandlerをnotifyする
--- # httpd.confとssl.confを両方配布するタスク - name: httpd.confを配布する ansible.builtin.template: src: httpd.conf.j2 dest: /etc/httpd/conf/httpd.conf validate: '/usr/sbin/httpd -t -f %s' notify: Reload Apache - name: ssl.confを配布する ansible.builtin.template: src: ssl.conf.j2 dest: /etc/httpd/conf.d/ssl.conf notify: Reload Apache
changed になっても、Reload Apache handlerはPlay終了時に1回だけ実行される。Ansibleが重複をキューで自動排除するためだ。4. handlerが実行されるタイミング(実行フロー)
Ansibleのhandlerは通常、Playの全タスク終了後(Play recapの直前)に実行される。実行フローを整理すると次のようになる。・タスク1: httpd.conf配布 → changed → 「Reload Apache」をキューに追加
・タスク2: ssl.conf配布 → changed → 「Reload Apache」をキューに追加(重複は自動排除)
・タスク3: ファイアウォール設定 → ok(変更なし、notifyは発火しない)
・(全タスク終了)
・RUNNING HANDLERS: 「Reload Apache」を1回だけ実行
5. flush_handlersでhandlerをその場で実行する
Play末尾まで待たずに途中でhandlerを実行させたい場面がある。・後続タスクが再起動後のサービスに依存する: Apacheを再起動してからHTTPヘルスチェックを実行したい
・pre_tasksとtasksの間でhandlerを実行したい: OSの設定を反映してからアプリをインストールしたい
・複数のPlayに分割したPlaybookで前Playのhandlerを確実に終わらせたい: handlerはPlay内でしか持ち越されないため
meta: flush_handlers をタスクとして記述すると、その時点で通知済みの全handlerが即時実行される。- hosts: webservers tasks: - name: httpd.confを配布する ansible.builtin.template: src: httpd.conf.j2 dest: /etc/httpd/conf/httpd.conf validate: '/usr/sbin/httpd -t -f %s' notify: Reload Apache - name: ssl.confを配布する ansible.builtin.template: src: ssl.conf.j2 dest: /etc/httpd/conf.d/ssl.conf notify: Reload Apache # ここでhandlerを即時実行する - name: Flush handlers before health check ansible.builtin.meta: flush_handlers # この時点ではApacheが再起動済み - name: Check Apache health endpoint ansible.builtin.uri: url: http://localhost/health status_code: 200 timeout: 10 handlers: - name: Reload Apache ansible.builtin.service: name: httpd state: reloaded
meta: flush_handlers の実行後、Reload Apache は通知済みリストから消える。Play末尾での再実行はない。6. listenで複数タスクから同一handlerを呼び出す
通常のnotifyはhandler名を直接指定する。Role間にまたがる設計ではhandler名がRole間の暗黙の依存になりやすい。listen を使うと、handler側にトピック名を持たせることができ、タスク側はトピック名だけを通知するだけで済む。tasks: - name: httpd.confを配布する ansible.builtin.template: src: httpd.conf.j2 dest: /etc/httpd/conf/httpd.conf notify: apache config changed # トピック名を通知 - name: ssl.confを配布する ansible.builtin.template: src: ssl.conf.j2 dest: /etc/httpd/conf.d/ssl.conf notify: apache config changed # 同じトピック名 handlers: - name: Validate Apache config ansible.builtin.command: /usr/sbin/httpd -t listen: apache config changed # トピックを購読 - name: Reload Apache ansible.builtin.service: name: httpd state: reloaded listen: apache config changed # 同じトピックを購読
apache config changed トピックに通知が来ると、Validate Apache config と Reload Apache の両方が handlers:セクションに記述した順序 で実行される。構文チェックが先に走り、問題があればreloadを止めることができる。listenを使った設計の利点をまとめる。
・handler名の変更がタスクに波及しない: handler名を変えてもタスク側の修正が不要
・1つの通知で複数のhandlerを順序付きで起動できる: 構文チェック→reloadを1つのnotifyで実現できる
・Role間の結合度を下げられる: Roleの境界を越えてトピックを共有でき、疎結合な設計になる
7. handler連鎖(Ansible 2.9以降)|handlerが別のhandlerを呼び出す
Ansible 2.9以降、handlerが別のhandlerをnotifyする「handler連鎖」が公式サポートされている。再起動後にヘルスチェックを走らせるといった、2段階の後処理を1つのnotifyでまとめて記述できる。handlers: - name: Restart Apache ansible.builtin.service: name: httpd state: restarted notify: Verify Apache Health # handler連鎖:下流handlerを呼び出す - name: Verify Apache Health ansible.builtin.uri: url: http://127.0.0.1/health status_code: 200 retries: 3 delay: 5
Postfix(Postfix の基本設定の解説)のmain.cfなど、設定ファイルを持つ他のサービスでも全く同じtemplateとhandlerのパターンが使える。
よくある失敗パターンとトラブルシュート
1. handlerが実行されない
症状: タスクがchanged になっているのに RUNNING HANDLERS が出ない。【重要】原因①: notifyとhandler名が一致していない
タスク側の
notify とhandler側の name は文字列の完全一致が必要だ。大文字・小文字・スペースの違いも区別される。この不一致がhandlerが動かない最多原因なので、名前のコピーペーストミスに注意してほしい。# NG: notify名とhandler名が不一致(大文字・小文字が異なる) notify: reload apache handlers: - name: Reload Apache # OK: 完全一致 notify: Reload Apache handlers: - name: Reload Apache
--check(ドライラン)モードでは実際の変更が発生しないため、handlerも実行されない。動作確認は通常実行で行うこと。原因③: Playbookがエラーで中断した
タスクがエラーで中断すると、その後のhandlerは実行されない。
--force-handlers オプションを使うと、エラー発生時でもhandlerを実行できる(クリーンアップ処理などに活用する)。原因④: タスクがchangedでなくokだった
notifyはタスクが
changed を返したときだけ有効になる。設定ファイルに変更がない場合、template は ok を返し、notifyは発動しない。意図的にhandlerを起動したい場合は changed_when: true を設定することで、強制的に changed 扱いにできる。# 常にchangedとして扱い、notifyを発動させる例 - name: 設定反映スクリプトを実行する ansible.builtin.command: /opt/app/bin/reload-config changed_when: true notify: Restart Apache
2. テンプレートで「undefined variable」エラーが出る
症状:fatal: [web01.example.com]: FAILED! => { "msg": "AnsibleUndefinedVariable: 'ssl_cert_path' is undefined" }
# テンプレート内でデフォルト値を指定する SSLCertificateFile {{ ssl_cert_path | default('/etc/pki/tls/certs/localhost.crt') }}
# 変数が定義されている場合のみSSL設定を出力する {% if ssl_cert_path is defined %} SSLCertificateFile {{ ssl_cert_path }} {% endif %}
そもそも変数を定義し忘れているケースが多い。
group_vars/webservers.yml や host_vars/web01.yml に必要な変数を明示的に定義することが根本的な解決策だ。変数の優先順位はhost_vars > group_vars > defaults(Role)の順になる。3. validateエラーで設定ファイルが配布されない
症状: templateモジュールにvalidate を設定した際に構文エラーが検出され、ファイル転送がブロックされる。これは 正常な動作だ。
validate は誤った設定を本番サーバーに送らないための安全装置として機能している。エラーメッセージに従って .j2 テンプレートを修正し、正しい設定ファイルが生成されることを確認してから再実行すること。4. タスク失敗後もhandlerを確実に実行したい(force_handlers)
デフォルトでは、Playbookのタスクが失敗するとその時点でPlay実行が止まり、通知済みのhandlerも実行されない。クリーンアップやアラート通知のhandlerを確実に動かしたい場合はforce_handlers: true をPlay宣言に追加する。- hosts: appservers force_handlers: true # タスク失敗時もhandlerを実行する tasks: - name: 設定ファイルを配布する ansible.builtin.template: src: app.conf.j2 dest: /etc/app/app.conf notify: Notify deploy result - name: データベースマイグレーションを実行する ansible.builtin.command: /opt/app/bin/migrate.sh # このタスクが失敗してもhandlerが実行される handlers: - name: Notify deploy result ansible.builtin.uri: url: https://hooks.example.internal/notify method: POST body_format: json body: text: "Deploy finished on {{ inventory_hostname }}"
force_handlers: true はPlay単位での指定を推奨する。ansible.cfgに force_handlers = True と書いて全Playbookのデフォルトにもできるが、予期しないhandler実行が増えるリスクがあるため、必要なPlayに限定するのが安全だ。5. handler内でregister変数を使う
症状: handlerにregisterを書いたが、後続のタスクから変数を参照できない。handlerのタスク結果をregisterで受け取っても、後続のtaskから参照できない。handlerはPlay末尾で実行されるため、タスクのループ内で結果を使い回すような処理には向いていない。こうした処理はtask側にインラインで書くか、別のPlayに分離するのが適切だ。
# NG: handlerでregisterしても後続taskから参照できない handlers: - name: Check service status ansible.builtin.command: systemctl is-active httpd register: service_result # この変数は後続taskから見えない # OK: task側でregisterを使い、handlerは再起動のみに限定する tasks: - name: httpd.confを配布する ansible.builtin.template: src: httpd.conf.j2 dest: /etc/httpd/conf/httpd.conf notify: Reload Apache register: template_result - name: 設定変更の結果を確認する ansible.builtin.debug: var: template_result.changed
6. 複数ホストでhandlerの挙動を誤解する
症状: web01でchangedが出たのに、web02のApacheが再起動されない。handlerはホストごとに独立して管理される。web01でchangedが出てもweb02のhandlerは呼ばれない。「1台でも設定変更があったら全台一斉に再起動したい」という要求がある場合は、handlerではなく
delegate_to や run_once を組み合わせた別の設計が必要だ。# 全ホストへの適用完了後に1回だけ実行したい場合の例 handlers: - name: Notify all done ansible.builtin.uri: url: https://hooks.example.internal/notify method: POST body_format: json body: text: "Deploy complete" run_once: true # 1ホストでchangedが出た時点で1回だけ実行 delegate_to: localhost
本記事のまとめ
| やりたいこと | 使う機能 | ポイント |
|---|---|---|
| 環境別設定ファイルを動的生成 | templateモジュール + .j2ファイル | Jinja2で変数・条件を埋め込む |
| 変数が未定義の場合にデフォルト値を使う | {{ 変数 | default(値) }} フィルター |
AnsibleUndefinedVariableを防ぐ |
| 設定変更時だけサービスを再起動 | notify + handler | Play終了後に1回だけ実行 |
| 設定ファイルの構文を転送前にチェック | templateモジュールのvalidate |
httpd -t -f でApache構文検証 |
| 複数タスクから同じhandlerを1回だけ呼ぶ | 同名のnotifyを複数設定 |
Ansibleがキューで重複を自動排除 |
| 後続タスクの前にhandlerを即時実行する | meta: flush_handlers |
ヘルスチェック前のサービス再起動に使う |
| 複数タスクから1つのhandlerを疎結合に呼ぶ | handlerのlistenトピック |
handler名変更がタスクに波及しない |
| 再起動後にヘルスチェックを自動実行する | handler連鎖(handler内のnotify) | Ansible 2.9以降。多段は最小限に |
| タスク失敗時もhandlerを必ず実行する | Play宣言にforce_handlers: true |
クリーンアップ・通知処理に活用 |
さらに
flush_handlers と listen を組み合わせ、Ansible 2.9以降のhandler連鎖も活用することで、再起動タイミングの制御と疎結合な設計が可能になる。どのサービス(Apache・Postfix・Nginxなど)でも同じ構造で応用できる汎用パターンとして覚えておいてほしい。
Ansible実践ハンズオンの詳細を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 次のページへ:Ansible動的インベントリ設計|aws_ec2プラグインでEC2ホストを自動取得する仕組みと設定パターン
- 前のページへ:Ansible Vaultでシークレットを安全に管理する方法|パスワードファイルや暗号化変数の実践例
- この記事の属するカテゴリ:Ansibleへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます