「同じ設定を複数のPlaybookにコピーしているが、修正のたびに全箇所を書き直す羽目になっている」
こうした課題の答えが Ansible role(ロール) です。roleはPlaybookの処理を機能単位で分割し、別のPlaybookでもそのまま再利用できるようにする仕組みです。Ansibleを本格的に使い始めたら、最初に覚えるべき設計パターンのひとつです。
この記事では、roleのディレクトリ構造から
ansible-galaxy initによる雛形作成、handlersとdefaultsの使い方、listenキーワードによる複数role間のhandler共有、meta: flush_handlersによる即時実行制御、さらにmeta/main.ymlを使ったrole間の依存関係の宣言まで、実際のサーバーでの実行結果とともに解説します。またPlaybookの実行順序(pre_tasks → roles → tasks → post_tasks)の中でroleをどのように使うべきかも合わせて説明します。実行環境:Rocky Linux 9.4 / Ansible 2.16.3(コントロールノード)、管理対象:Rocky Linux 9.4
この記事のポイント
・ansible roleはPlaybookを機能単位に分割・再利用する仕組み
・ansible-galaxy initコマンドでディレクトリ雛形を一瞬で生成できる
・handlers/notifyでサービス再起動を冪等に管理できる(changedの時だけ発火)
・handlerはフェーズ(pre_tasks/roles/tasks/post_tasks)の末尾でまとめて発火する
・listenキーワードで複数roleから1つのhandlerをまとめて呼べる
・defaultsは外部から上書き可能、varsは上書きを抑制する役割
・meta/main.ymlのdependenciesでrole間の依存関係をコードで宣言できる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜroleが必要なのか?Playbookの肥大化問題を整理する
Ansibleを使い始めた当初は、1つのPlaybookファイルにすべてのタスクを書き並べるスタイルで十分に動きます。しかしサーバーの台数や管理対象の設定項目が増えてくると、Playbookが200行・300行を超えるようになり、以下の問題が表面化します。・どのタスクがどのホストに対する処理なのか、ひと目で判断しにくい
・Webサーバー設定とDBサーバー設定が1ファイルに混在して見通しが悪い
・「この処理を別のプロジェクトでも使いたい」という場合にコピーしか手段がない
・複数のroleを組み合わせるとき、実行順序の管理がPlaybook側に漏れ出てメンテナンスが大変になる
roleを使うと、「Webサーバー設定」「NTP設定」「セキュリティ強化」といった機能単位でコードを分割できます。分割したroleはPlaybookからシンプルに呼び出せるため、再利用・テスト・チーム共有のいずれも格段に楽になります。さらに
meta/main.ymlのdependenciesを使えば、role間の実行順序もrole自身が管理できるようになり、Playbook側をシンプルに保てます。Ansible Galaxyで公開されているコミュニティroleもこの構造を採用しており、業界標準の設計パターンと言えます。
roleのディレクトリ構造と各サブディレクトリの役割
1. 標準ディレクトリ構成
roleは以下のディレクトリ構造で構成されます。すべてのディレクトリが必須ではなく、必要なものだけ作成すればOKです。roles/ └── webserver/ # role名 ├── tasks/ │ └── main.yml # メインのタスクリスト(必須) ├── handlers/ │ └── main.yml # notifyで呼び出されるハンドラ ├── defaults/ │ └── main.yml # 上書き可能なデフォルト変数(最低優先度) ├── vars/ │ └── main.yml # 上書きを想定しない変数(高優先度) ├── files/ # copyモジュールで配布するファイル ├── templates/ # templateモジュールで使うJinja2テンプレート └── meta/ └── main.yml # roleのメタ情報・依存関係の定義
・tasks/main.yml:roleの本体。実行するタスクをここに書く
・handlers/main.yml:タスクから
notifyされたときだけ実行されるタスク。サービス再起動などに使う・defaults/main.yml:変数のデフォルト値。Ansibleの変数優先順位で最も低く、Playbook側や
group_varsから上書きできる・vars/main.yml:roleが内部で使う変数。優先順位が高く、外部からの上書きを抑制したい値に使う
・files/:静的ファイルを置く。
copyモジュールで参照する際はパスを省略できる・templates/:Jinja2テンプレートファイルを置く。
templateモジュールで動的な設定ファイルを生成するときに使う・meta/main.yml:roleのメタ情報(対応OS・バージョン・作者)と依存するroleを宣言する。後述の
dependenciesフィールドがここに入る2. ansible-galaxy initで雛形を自動生成する
ディレクトリを手作業で作る必要はありません。ansible-galaxy initコマンドが雛形を一瞬で生成してくれます。# webserverという名前のroleを作成する ansible-galaxy init webserver
[ansible@ctrl01 roles]$ ansible-galaxy init webserver - Role webserver was created successfully [ansible@ctrl01 roles]$ find webserver -type f webserver/README.md webserver/defaults/main.yml webserver/handlers/main.yml webserver/meta/main.yml webserver/tasks/main.yml webserver/tests/inventory webserver/tests/test.yml webserver/vars/main.yml webserver/.travis.yml
main.ymlに内容を書いていくだけです。
3. meta/main.ymlでrole間の依存関係を宣言する
meta/main.ymlの中で特に重要なのがdependenciesフィールドです。ここに依存するrole名を書くと、Ansibleは自動的にそのroleを先に実行してから本体のroleを実行します。Playbook側に実行順序を書く必要がなくなるため、role数が増えても設計が破綻しません。たとえば「Webサーバー設定roleはOS基礎設定roleが先に完了していないと動かない」という場合、以下のように宣言します。
# roles/webserver/meta/main.yml galaxy_info: author: your_name description: Webサーバーをセットアップするrole license: MIT min_ansible_version: "2.14" platforms: - name: EL versions: - "9" dependencies: - role: os_base # webserver実行前にos_baseを自動実行する
webserverだけ書けばOKです。os_baseの実行順序をPlaybookで管理する必要はなくなります。# site.yml(dependenciesを使うとこれだけでOK) - hosts: webservers roles: - webserver # os_baseはAnsibleが自動で先行実行する
dependencies: []と明示するのがベストプラクティスです。「このroleは何にも依存しない」という意図がチームに伝わります。
最小構成のroleを実際に作る実践手順
ここでは「Apacheをインストールして設定を展開し、サービスを起動する」というWebサーバーroleを例に、基本的な書き方を示します。1. tasks/main.ymlにタスクを書く
# roles/webserver/tasks/main.yml --- - name: httpd をインストールする ansible.builtin.dnf: name: httpd state: present - name: httpd.conf を配置する ansible.builtin.template: src: httpd.conf.j2 dest: /etc/httpd/conf/httpd.conf owner: root group: root mode: '0644' notify: restart httpd # 変更があった場合はhandlerを呼ぶ - name: httpd を起動・自動起動有効化 ansible.builtin.service: name: httpd state: started enabled: true
2. handlersとnotifyで再起動を冪等に制御する
handlersはタスクからnotifyされたときだけ実行される仕組みです。設定ファイルが変更された場合にだけサービスを再起動するため、何度実行しても余分な再起動が発生しない冪等な設計が実現できます。# roles/webserver/handlers/main.yml --- - name: restart httpd ansible.builtin.service: name: httpd state: restarted
notify: restart httpdと書いた場合、handlers/main.yml内のname: restart httpdと完全に一致している必要があります。大文字・小文字の違いもエラーの原因になるので注意してください。handlerが実行される条件は「notifyを書いたタスクの結果がchangedであること」です。設定ファイルがすでに最新の状態だった場合、タスクは
ok(変更なし)になり、notifyは発火せずhandlerも実行されません。これがAnsibleの冪等性設計の核心です。handlerはPlayの最後にまとめて実行されます。同一のhandlerが複数のタスクからnotifyされても、実行は1回だけです。これがhandlerを使う最大の利点です。
# 複数タスクが同じhandlerをnotifyしても1回だけ実行される例 - name: httpd.conf を配置する ansible.builtin.template: src: httpd.conf.j2 dest: /etc/httpd/conf/httpd.conf notify: restart httpd - name: conf.d/vhost.conf を配置する ansible.builtin.template: src: vhost.conf.j2 dest: /etc/httpd/conf.d/vhost.conf notify: restart httpd # 同じhandlerをnotify # → handlerの「restart httpd」は1回だけ実行される
3. defaultsとvarsの使い分け
roleで使う変数はdefaultsとvarsの2箇所に書けますが、役割が異なります。# roles/webserver/defaults/main.yml # 外部から上書き可能なデフォルト値(推奨) --- http_port: 80 server_name: "{{ ansible_hostname }}" document_root: /var/www/html
# roles/webserver/vars/main.yml # 上書きを抑制したいrole内部変数 --- httpd_config_path: /etc/httpd/conf/httpd.conf httpd_package: httpd
group_vars・host_varsのどれからでも上書きできます。ユーザーがroleの動作をカスタマイズしたい値(ポート番号、ドキュメントルートなど)はここに書くのがベストプラクティスです。varsは優先順位が高く、外部から上書きするにはかなり高優先度の変数指定が必要です。role内部でのみ使う固定的な値(パッケージ名、設定ファイルパスなど)はこちらに書きます。
なお、
meta/main.ymlのdependenciesで依存roleを宣言するとき、その依存roleに変数を渡すこともできます。たとえば「基礎設定roleをWebサーバー用にカスタマイズして呼び出す」場合は以下のように書きます。# roles/webserver/meta/main.yml dependencies: - role: os_base vars: timezone: "Asia/Tokyo" ntp_server: "ntp.nict.jp"
defaults/main.ymlにデフォルト値を持たせておき、依存元roleから上書きしたい値だけをvars:に書くパターンです。こうすることで、基礎roleは汎用的なまま保ちつつ、呼び出し側の用途に応じたカスタマイズが可能になります。
Ansible実践ハンズオンの詳細を見る >>
handlersの応用|listenとmeta: flush_handlersでrole間連携を制御する
roleの設計が複雑になってくると、「複数のroleから同じhandlerを呼びたい」「handlerをPlay終了まで待たずに即時実行させたい」という要件が出てきます。そのためにlistenキーワードとmeta: flush_handlersを使います。
1. handlerの発火タイミングを正確に理解する
handlerはnotifyされてもすぐには動きません。「そのフェーズが全て終わったタイミング」でまとめて発火します。Playbookにはpre_tasks → roles → tasks → post_tasksという実行順序があり、handlerはそれぞれのフェーズ終了後に1回ずつ発火する機会があります。合計4回の発火機会があることを覚えておいてください。・pre_tasksフェーズ終了後(pre_tasks内でnotifyしたhandlerが発火)
・rolesフェーズ終了後(roles内でnotifyしたhandlerが発火)
・tasksフェーズ終了後(tasks内でnotifyしたhandlerが発火)
・post_tasksフェーズ終了後(post_tasks内でnotifyしたhandlerが発火)
例として、tasksセクションに3つのタスクがあり、1番目と3番目が同じhandlerにnotifyした場合を考えます。
・タスク1実行 → httpdのhandlerにnotify(発火は保留)
・タスク2実行 → notifyなし
・タスク3実行 → httpdのhandlerに再度notify(同一handlerなので重複しない)
・tasksフェーズ終了 → handlerが1回だけ発火(サービス再起動1回)
10箇所の設定ファイルを書き換えても、サービス再起動は1回で済む。これがhandlerの冪等な挙動です。
2. listenで複数roleからhandlerを共有する
通常のnotifyはhandlerのnameと完全一致している必要があるため、複数のroleで同じhandlerを共有しようとするとhandler名の重複管理が煩雑になります。listenキーワードを使うと、handlerに「受け取り名」を別途定義でき、role境界を越えて呼び出せるようになります。# roles/webserver/handlers/main.yml - name: restart httpd ansible.builtin.service: name: httpd state: restarted listen: "httpd restart" # listenキーワードで受け取り名を定義
# roles/ssl_cert/tasks/main.yml - name: SSL証明書を更新する ansible.builtin.copy: src: "{{ cert_file }}" dest: /etc/httpd/ssl/server.crt notify: "httpd restart" # listenの名前でnotify(別roleから呼べる)
# roles/vhost/tasks/main.yml - name: バーチャルホスト設定を配置する ansible.builtin.template: src: vhost.conf.j2 dest: /etc/httpd/conf.d/vhost.conf notify: "httpd restart" # 別roleからも同じlistenでnotify
nameまたはlistenのどちらの値でも呼び出せます。listenを使う場合は文字列の完全一致が必要です。「ssl_certロールからApacheの再起動をnotifyしたいが、handlerはwebserverロールに定義したい」という実務でよくある要求をこのパターンで解消できます。
3. meta: flush_handlersでhandlerを即時実行させる
handlerはデフォルトではPlayの最後にまとめて実行されます。しかし「設定変更後にサービスを再起動させてから、次のタスクで起動確認を行いたい」というケースでは、Playの最後まで待てません。そのためにmeta: flush_handlersを使います。# roles/webserver/tasks/main.yml - name: httpd.conf を配置する ansible.builtin.template: src: httpd.conf.j2 dest: /etc/httpd/conf/httpd.conf notify: restart httpd - name: handlerをここで即時実行させる ansible.builtin.meta: flush_handlers # このタイミングでhandlerを実行 - name: httpdの起動確認 ansible.builtin.uri: url: http://localhost/healthz status_code: 200 # この時点でhttpdは再起動済みのためヘルスチェックが成功する
meta: flush_handlersを使わない場合、「起動確認」タスクはhttpdが再起動される前に実行されてしまいます。サービス再起動を含む複数ステップの処理では、flush_handlersの挿入位置が設計の要です。
roleをPlaybookから呼び出す方法
1. rolesキーで呼び出す(標準的な書き方)
Playbook内にroles:セクションを設けて呼び出します。最もシンプルで可読性が高い方法です。# site.yml --- - name: Webサーバーをセットアップする hosts: webservers become: true roles: - webserver # roles/webserver/ を呼び出す
roles/フォルダを置くか、ansible.cfgのroles_pathで検索パスを設定することで自動的に認識されます。2. include_roleタスクで動的に呼び出す
条件分岐やloopと組み合わせてroleを呼び出したい場合はinclude_roleを使います。# 条件付きでroleを適用する例 - name: 本番環境でのみセキュリティroleを適用する ansible.builtin.include_role: name: security_hardening when: env == 'production'
[ansible@ctrl01 ~]$ ansible-playbook -i inventory/hosts site.yml PLAY [Webサーバーをセットアップする] ************************************ TASK [Gathering Facts] **************************************************** ok: [web01.example.internal] TASK [webserver : httpd をインストールする] ******************************** changed: [web01.example.internal] TASK [webserver : httpd.conf を配置する] *********************************** changed: [web01.example.internal] TASK [webserver : httpd を起動・自動起動有効化] **************************** changed: [web01.example.internal] RUNNING HANDLERS [webserver : restart httpd] ******************************** changed: [web01.example.internal] PLAY RECAP ***************************************************************** web01.example.internal : ok=5 changed=4 unreachable=0 failed=0
RUNNING HANDLERSの行に注目してください。設定ファイルが変更されたためhandlerが自動で実行されています。2回目の実行では設定ファイルに変化がなければhandlerは実行されません。これが冪等な設計です。3. pre_tasksとpost_tasksでrole適用を安全に囲む
本番環境でroleを適用する際は、pre_tasksでrole実行前の前提条件を確認し、post_tasksでrole適用後の動作確認を行うパターンが有効です。Playbookの実行順序はpre_tasks → roles → tasks → post_tasksに固定されています。# site.yml — 実行順序を意識したPlaybook設計 --- - name: Rocky Linux 9.4 に Apache を構築する hosts: webservers become: true pre_tasks: - name: OSバージョンを確認する(前提条件チェック) ansible.builtin.assert: that: - ansible_distribution in ["RedHat", "Rocky", "AlmaLinux"] - ansible_distribution_major_version | int >= 9 fail_msg: "Rocky/RHEL 9以上が必要です(現在: {{ ansible_distribution_version }})" - name: DBサーバーが応答しているか確認する ansible.builtin.wait_for: host: db01.example.internal port: 3306 timeout: 10 delegate_to: localhost roles: - role: security # SELinux・firewalld設定 - role: webserver # Apache インストール・設定 tasks: - name: バーチャルホスト設定を配置する ansible.builtin.template: src: vhost.conf.j2 dest: /etc/httpd/conf.d/myapp.conf mode: '0644' notify: restart httpd post_tasks: - name: ポート80がLISTENになるまで待機する ansible.builtin.wait_for: host: "{{ ansible_default_ipv4.address }}" port: 80 state: started timeout: 30 - name: HTTP 200が返るか確認する ansible.builtin.uri: url: "http://{{ ansible_default_ipv4.address }}/" status_code: 200 handlers: - name: restart httpd ansible.builtin.service: name: httpd state: restarted
pre_tasksのassertが1つでも失敗した時点でrolesフェーズには進みません。本番への誤ったrole適用を防ぐ「Go/No-Go チェック」として機能します。post_tasksはrolesとtasksが全て完了した後に実行されるため、デプロイ完了の証明として動作確認を置くのが現場での定石です。
よくあるエラーとトラブルシュート
エラー1: ERROR! the role 'webserver' was not foundroleが見つからないエラーです。Playbookファイルからの相対パスに
roles/webserver/ディレクトリが存在するか確認してください。またはansible.cfgのroles_path設定を確認します。# ansible.cfg に検索パスを追加する [defaults] roles_path = ./roles:/etc/ansible/roles
handler名に誤字がある場合や、タスクの結果が
ok(変更なし)の場合に起こります。以下の2点を確認してください。・
notify:に書いた文字列とhandlers/main.ymlのname:が1文字も違わず一致しているか(大文字・小文字含む)・タスクのステータスが
okになっていないか。タスクがchangedにならないとnotifyがトリガーされないlistenキーワードを使う場合は、notify:に書く文字列がhandlerのlisten:値と完全一致しているか確認してください。name:ではなくlisten:の値でnotifyしている点に注意が必要です。エラー3: defaultsの変数が上書きできない
vars/main.ymlに書いた変数は優先順位が高いため、通常の
group_varsでは上書きできません。外部から上書きしたい値はdefaults/main.ymlに移動させてください。エラー4: A circular dependency was detected in the role tree
meta/main.ymlのdependenciesで循環依存が生じた場合(例:RoleAがRoleBに依存し、RoleBがRoleAに依存する)、Ansibleは自動的にこれを検出してエラーを出します。ERROR! A circular dependency was detected in the role tree: webserver -> os_base -> webserver
エラー5: flush_handlersを使ったのにhandlerが実行されない
meta: flush_handlersはnotifyが蓄積されているhandlerをその時点で実行させます。notifyを書いたタスクの結果がokだった場合(変更なし)は、そもそもhandlerへのnotifyが蓄積されていないため、flush_handlersを使っても何も実行されません。タスクがchangedになっているかどうかを先に確認してください。エラー6: role内のtasksとPlaybook内のtasksの混同
roleの中に
tasks/main.ymlがあり、PlaybookにもPlay単位のtasks:セクションがあります。roleのtasks/main.ymlは「rolesフェーズ」で実行され、Playのtasks:セクションは「tasksフェーズ」で実行されます。この2つを混同すると実行順序の把握が難しくなります。原則として、role内で完結する処理はroleのtasks/main.ymlに書き、Playbook固有の追加設定はPlayのtasks:セクションに書く分離を守ることで、roleは「どのPlaybookでも再利用できる単位」として保たれます。本記事のまとめ
| 役割 | ファイル・方法 |
|---|---|
| role雛形を自動生成する | ansible-galaxy init ロール名 |
| メインタスクを書く | roles/ロール名/tasks/main.yml |
| 再起動などを冪等に制御する | roles/ロール名/handlers/main.yml |
| handlerを途中で即時実行する | タスクとして meta: flush_handlers を挿入する |
| 複数roleから同じhandlerを呼ぶ | handlerに listen: "共通名" を設定してnotifyで呼ぶ |
| 上書き可能なデフォルト変数 | roles/ロール名/defaults/main.yml |
| 上書きを抑制する内部変数 | roles/ロール名/vars/main.yml |
| role間の依存関係を宣言する | roles/ロール名/meta/main.ymlのdependencies: |
| roleをPlaybookから呼ぶ | roles:キーまたはinclude_roleタスク |
| role適用前の前提条件チェック | pre_tasksにassertを書く(rolesより先に実行される) |
| role適用後の動作確認 | post_tasksにuri/wait_forを書く(全て完了後に実行される) |
listenキーワードを活用すればrole境界を越えてhandlerを共有でき、meta: flush_handlersを組み合わせることで再起動タイミングを柔軟に制御できます。さらにmeta/main.ymlのdependenciesを活用すれば、role間の実行順序もPlaybookではなくrole自身が管理するようになり、Playbookをよりシンプルに保てます。pre_tasksでOSバージョンや依存サービスの状態を事前確認し、post_tasksでデプロイ後のヘルスチェックを実施する構成にすると、本番環境での安全性が一段と高まります。まずは1つの機能(例:Webサーバー設定)を切り出してrole化してみることをお勧めします。慣れてきたらAnsible Galaxyで公開されているコミュニティroleも積極的に活用しましょう。Ansible実践ハンズオンの詳細を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

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