Ansibleのrole設計入門|ディレクトリ構造とtasks・handlers・defaultsで再利用可能なコードを作る方法

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Ansible > Ansibleのrole設計入門|ディレクトリ構造とtasks・handlers・defaultsで再利用可能なコードを作る方法
「AnsibleのPlaybookが長くなりすぎて、どこに何を書いたか分からなくなってきた」
「同じ設定を複数のPlaybookにコピーしているが、修正のたびに全箇所を書き直す羽目になっている」

こうした課題の答えが Ansible role(ロール) です。roleはPlaybookの処理を機能単位で分割し、別のPlaybookでもそのまま再利用できるようにする仕組みです。Ansibleを本格的に使い始めたら、最初に覚えるべき設計パターンのひとつです。

この記事では、roleのディレクトリ構造からansible-galaxy initによる雛形作成、handlersdefaultsの使い方、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間の依存関係をコードで宣言できる


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

なぜroleが必要なのか?Playbookの肥大化問題を整理する

Ansibleを使い始めた当初は、1つのPlaybookファイルにすべてのタスクを書き並べるスタイルで十分に動きます。しかしサーバーの台数や管理対象の設定項目が増えてくると、Playbookが200行・300行を超えるようになり、以下の問題が表面化します。

・どのタスクがどのホストに対する処理なのか、ひと目で判断しにくい
・Webサーバー設定とDBサーバー設定が1ファイルに混在して見通しが悪い
・「この処理を別のプロジェクトでも使いたい」という場合にコピーしか手段がない
・複数のroleを組み合わせるとき、実行順序の管理がPlaybook側に漏れ出てメンテナンスが大変になる

roleを使うと、「Webサーバー設定」「NTP設定」「セキュリティ強化」といった機能単位でコードを分割できます。分割したroleはPlaybookからシンプルに呼び出せるため、再利用・テスト・チーム共有のいずれも格段に楽になります。さらにmeta/main.ymldependenciesを使えば、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

実際に Rocky Linux 9.4 上で実行した結果が以下です。

[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を自動実行する

こう設定すると、Playbook側はwebserverだけ書けばOKです。os_baseの実行順序をPlaybookで管理する必要はなくなります。

# site.yml(dependenciesを使うとこれだけでOK) - hosts: webservers roles: - webserver # os_baseはAnsibleが自動で先行実行する

依存するroleが存在しない(依存なし)の場合は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

ポイントはhandler名の一致です。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

defaultsはAnsibleの変数優先順位で最も低いため、Playbook変数・group_varshost_varsのどれからでも上書きできます。ユーザーがroleの動作をカスタマイズしたい値(ポート番号、ドキュメントルートなど)はここに書くのがベストプラクティスです。

varsは優先順位が高く、外部から上書きするにはかなり高優先度の変数指定が必要です。role内部でのみ使う固定的な値(パッケージ名、設定ファイルパスなど)はこちらに書きます。

なお、meta/main.ymldependenciesで依存roleを宣言するとき、その依存roleに変数を渡すこともできます。たとえば「基礎設定roleをWebサーバー用にカスタマイズして呼び出す」場合は以下のように書きます。

# roles/webserver/meta/main.yml dependencies: - role: os_base vars: timezone: "Asia/Tokyo" ntp_server: "ntp.nict.jp"

基礎roleのdefaults/main.ymlにデフォルト値を持たせておき、依存元roleから上書きしたい値だけをvars:に書くパターンです。こうすることで、基礎roleは汎用的なまま保ちつつ、呼び出し側の用途に応じたカスタマイズが可能になります。
roleの設計パターンを含むAnsible実務スキルを体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
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

notifyではhandlerの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/ を呼び出す

Playbookファイルと同じディレクトリにroles/フォルダを置くか、ansible.cfgroles_pathで検索パスを設定することで自動的に認識されます。

2. include_roleタスクで動的に呼び出す

条件分岐やloopと組み合わせてroleを呼び出したい場合はinclude_roleを使います。

# 条件付きでroleを適用する例 - name: 本番環境でのみセキュリティroleを適用する ansible.builtin.include_role: name: security_hardening when: env == 'production'

実際にPlaybookを実行した際の出力例(管理対象:web01.example.internal)が以下です。

[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 found
roleが見つからないエラーです。Playbookファイルからの相対パスにroles/webserver/ディレクトリが存在するか確認してください。またはansible.cfgroles_path設定を確認します。

# ansible.cfg に検索パスを追加する [defaults] roles_path = ./roles:/etc/ansible/roles

エラー2: notifyを書いてもhandlerが実行されない
handler名に誤字がある場合や、タスクの結果がok(変更なし)の場合に起こります。以下の2点を確認してください。

notify:に書いた文字列とhandlers/main.ymlname:が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.ymldependenciesで循環依存が生じた場合(例:RoleAがRoleBに依存し、RoleBがRoleAに依存する)、Ansibleは自動的にこれを検出してエラーを出します。

ERROR! A circular dependency was detected in the role tree: webserver -> os_base -> webserver

解消するには、両方のroleが共通して必要とする処理を第3のrole(共通基礎role)として切り出し、両方がそのroleに依存するように再設計します。循環しそうな共通処理は基礎roleに逃がすのが基本的な対処法です。

エラー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.ymldependencies:
roleをPlaybookから呼ぶ roles:キーまたはinclude_roleタスク
role適用前の前提条件チェック pre_tasksにassertを書く(rolesより先に実行される)
role適用後の動作確認 post_tasksにuri/wait_forを書く(全て完了後に実行される)
roleを導入することで、Playbookの可読性・再利用性・チーム開発効率が大幅に改善されます。listenキーワードを活用すればrole境界を越えてhandlerを共有でき、meta: flush_handlersを組み合わせることで再起動タイミングを柔軟に制御できます。さらにmeta/main.ymldependenciesを活用すれば、role間の実行順序もPlaybookではなくrole自身が管理するようになり、Playbookをよりシンプルに保てます。pre_tasksでOSバージョンや依存サービスの状態を事前確認し、post_tasksでデプロイ後のヘルスチェックを実施する構成にすると、本番環境での安全性が一段と高まります。まずは1つの機能(例:Webサーバー設定)を切り出してrole化してみることをお勧めします。慣れてきたらAnsible Galaxyで公開されているコミュニティroleも積極的に活用しましょう。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
Ansible実践ハンズオンの詳細を見る >>

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

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

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

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

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

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

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

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

この記事を書いた人

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

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

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