playbook内のtask定義にtagを設定しておくことで、指定tagのtaskのみ実行したり、逆に指定tagのtaskを除外してansible-playbookを実行することができます。
開発中のtaskのみピンポイントで実行したい場合や、逆に、共有のDBのデータを更新したりするtaskはほかのユーザーやチームと調整してからでないと実行が難しかったり、Blue-Greenデプロイメントの実装で環境Aの機能をオフにしてもう片方の環境Bをオンにするような処理だけど開発中は環境Bだけ確認したかったり、大量データのダウンロードや冪等の確認を伴い処理に時間がかかるため開発中は実行したくないなど特定のtaskは実行したくない場合に利用できます。
また、特殊tagとして、常に実行するalwaysと実行しないneverというtagが予約語として用意されています。
neverは特に「通常は実行したくない場合」にtaskに設定しておくと、簡単に除外設定できます。
ただし、tag設定はtaskの呼び出し元のtag設定も継承されるという特徴があります。
tag設定をちゃんと設計・把握していないと、例えばrole単体で作成してて自前のplaybookから呼んでいるときは問題なかったけど、チームの他の人が作成したplaybookと結合したら想定してなかったtagが追加されて、期待しない動作をすることがあるので注意が必要です。
なお、本文中の「tagの設定」はplaybookに対するtagsディレクティブを使ったtagの使用を表し、「tagの指定」はansible-playbook実行時のオプション--tagsや--skip-tagsによるtagの使用を表します。(この記事内でのローカルルール)
また、Ansibleバージョンは2.10.5で確認しています。
TL;DR
本記事の内容は、importでtaskを読み込んだ場合の話です。
- playbookのtag設定は、「tagの継承」によってそのtaskの呼び出し元定義のtag設定も有効
- tag設定によってtaskが実行されるか評価されるのはtask単位
- 呼び出し元のplayやroleのtag設定と除外指定だけで配下の全taskが除外されたりはしない
- 動かしたくないtaskがある場合は
never設定だけに頼らず--skip-tagsを指定--tags neverのみ指定するとnevertag設定されたtaskは動作するnevertagと他のtagを複数設定されたtaskは、他に設定されているtagを--tagsで指定されると動作する--skip-tags neverでnevertagが設定されたtaskは他の有効なtagがあっても動かない
ansible-playbook実行時は、まず--tagsで処理対象taskかチェックされてから--skip-tagsで除外対象かをチェックされる--skip-tagsの指定が優先される--skip-tags alwaysでalwaystag設定されたtaskは動かない
- 実行前に
ansible-playbook --list-tasksで処理対象taskとそのtag設定を確認しよう
tag設定の継承例
playbook定義
tag設定の継承のされかたは、ちょっとわざとらしい上に一部無意味だけど以下のplaybookを例で説明します。
# playbook --- - hosts: localhost gather_facts: false tags: __play tasks: - import_role: name: other tags: __other - name: sample task debug: msg: "sample tasks" roles: - role: sample tags: __role1 - role: other tags: __role2
- playに
__playを設定 - playはtasksにtaskを2つ定義
- task1は
__othertagを設定してotherroleを設定 - task2はtag設定無し
- task1は
- playはrolesも使ってroleを2つ定義
- role1は
__role1tagを設定してsampleroleを設定 - role2は
__role2tagを設定して(taskと同じ)otherroleを設定
- role1は
※ tasksとrolesを同時指定した場合の処理順はroles->tasks
role定義
上記playbookが参照している一つ目のsampleroleは以下の通り。
# roles/sample/tasks/main.yml --- - name: task1 debug: msg: use __tag1 tags: __tag1 - name: task2 debug: msg: use __tag2 and never tags: - __tag2 - never - name: task3 debug: msg: use never tags: - never
もう一つのotherroleは以下の通り。
# roles/other/tasks/main.yml --- - name: other debug: msg: "other" tags: __other_task - name: import import_tasks: other_sub.yml
そしてさらにotherroleでは、import_tasksを使って別のtaskファイルを読んでいる。内容は以下の通り。
# roles/other/tasks/other_sub.yml --- - name: other_sub debug: msg: other_sub
構造を整理
上記のplaybookのplay/task/roleの呼び出し構成とtagの設定場所の概要は以下の通り。
playやtaskのように要素に対して設定されてるものはブロックの中へ、rolesやimport_role・import_tasksのように別定義の呼び出し時に設定されるものは矢印にtagを記載しています。

ここで、playbookのファイル上でtaskに設定されてるだけのtagを整理すると以下の通り。
| No | task | tags |
|---|---|---|
| (1) | sample task | N/A |
| (2) | other | __other_task |
| (3) | other_sub | N/A |
| (4) | task1 | __tag1 |
| (5) | task2 | __tag2, never |
| (6) | task3 | never |
しかし、実際は「tagの継承機能」が働くため、ansible-playbook実行時のtaskごとのtag設定としては以下の通りになります。(順不同)
| No | task | tags |
|---|---|---|
| (1) | sample task | __play_tag |
| (2)-a | other | __other_task, __other, __play_tag |
| (2)-b | other | __other_task, __parent_role_tag2, __play_tag |
| (3)-a | other_sub | __other, __play_tag |
| (3)-b | other_sub | __parent_role_tag2, __play_tag |
| (4) | task1 | __tag1, __parent_role_tag1, __play_tag |
| (5) | task2 | __tag2, never, __parent_role_tag1, __play_tag |
| (6) | task3 | never, __parent_role_tag1, __play_tag |
ここまで来るとマトリクス表の方がわかりやすいかもなのではみ出るけど併記。(はてなブログのスタイルなんか良いのないかなー)
| No | task | __play |
__other |
__role1 |
__role2 |
__other_task |
__tag1 |
__tag2 |
never |
|---|---|---|---|---|---|---|---|---|---|
| (1) | sample task | ✔ | |||||||
| (2)-a | other | ✔ | ✔ | ✔ | |||||
| (2)-b | other | ✔ | ✔ | ✔ | |||||
| (3)-a | other_sub | ✔ | ✔ | ||||||
| (3)-b | other_sub | ✔ | ✔ | ||||||
| (4) | task1 | ✔ | ✔ | ✔ | |||||
| (5) | task2 | ✔ | ✔ | ✔ | ✔ | ||||
| (6) | task3 | ✔ | ✔ | ✔ |
(2)のotherと(3)のother_subは、playで定義されたtasksから呼ばれるパターンと、rolesから呼ばれるパターンで、上位の定義が異なるため、同じファイルでも継承されるtagの情報が異なります。
(tasksからimport_roleで呼ばれるときは__otherが設定され、rolesから呼ばれるときは_role2が設定される)
呼び出し元の判定だけでは終了しない
大抵のプログラミング言語には制御構文ifを使った条件分岐を実装できます。
以下のようなコードを記述すると、条件Aに合致しなかった場合はcode-a以下入れ子になっている条件Bのifは評価されずに処理は進みます。
if (条件A) { // code-a if (条件B) { // code-b if (条件C) { // code-c } } }
それに対してAnsibleのplaybookにおけるroleやimport_tasksなどのimport系の外部taskの呼び出しは、呼び出し元のtag条件に合致しなくても、一旦すべてのtaskの内容をロードし、継承によるtag設定も含めて全tagを評価した上で、そのtaskが処理対象か否かを判定します。
前述の例で、例えばsamplerole内に定義されたtask1(No 4)を見てみると、呼び出し階層は以下のようになります。

コマンドラインオプションの--tags指定の特徴として、「指定されたtagがplaybookで設定されているtaskを実行する。tagが設定されなければ除外」という動きになりますが、--tags __tag1を指定してansible-playbookを実行する場合、「プログラミングの制御構文ifのように呼び出し元のplayのtags: __playの時点でtagの条件に合致しないため処理を終了する」という動きにはなりません。
オプションは後述しますが、--tagsで__tag1を指定して--list-tasksを使ってtaskとtagの一覧を出力すると、以下の通りtask1が処理対象となることが確認できます。
$ ansible-playbook -i localhost, playbook.yml --list-tasks --tags __tag1
playbook: playbook.yml
play #1 (localhost): localhost TAGS: [__play]
tasks:
sample : task1 TAGS: [__play, __role1, __tag1]
(追記) importとinclude
なおこの動作は、import系を使った静的なtaskの読み込みの場合になります。実行時に動的にtask読み込みを行うincludeは、事前の読み込み処理の時点ではtaskを読み込まないため、include_* を行うtask自体がtag条件に合致しなかった場合、対象の(もしimportだったら処理対象だったとしても)taskは読み込まれないため処理対象になりません。逆に、include_* 自体がtag条件に合致して外部のtask情報を読み込んでも、読み込んだtaskへtag情報は継承されません。
本記事も基本的にすべてimport系のtask読み込みを前提とした説明になります。(task読み込みの前処理段階でincludeの場合はtaskを読み込まないため)
Comparing includes and imports: dynamic and static re-use
taskとtagを確認するオプション
1つのplaybookで完結しているなど小規模で目視できるならともかく、この例のように呼び出し階層も複数あると、tagsで文字列検索しても構造を把握するのは正直難しいです。
ですが、ansible-playbookコマンドには対象playbookのtaskとtagの構造をリストアップする便利なオプションがあります。
--list-tags: 実行予定taskで使用される全tagを出力
--list-tagsを指定します。
これを使うと「実行対象のtaskに設定されている全てのtag」をまとめて出力できます。
neverなどによって対象外となっているtaskについては表示されません。
※ 正確には「実行されるかどうか評価の対象となるtask」であり、例えばwhenの評価結果で実行されない場合のtaskでも出力の対象となります。
事前処理としてplaybookの構文解析を行い処理対象となるtask一覧を静的にリストアップできた内容になります。
$ ansible-playbook -i localhost, playbook.yml --list-tags
playbook: playbook.yml
play #1 (localhost): localhost TAGS: [__play]
TASK TAGS: [__other, __other_task, __play, __role1, __role2, __tag1]
もしnever設定のtaskを処理するつもりがないのにneverが含まれている場合は注意してください。
--list-tasks: 実行予定の全taskとそれぞれのtagを出力
実行されるtaskとそのtag設定は、ansible-playbookのオプション--list-tasksで確認できます。
$ ansible-playbook -i localhost, playbook.yml --list-tasks
playbook: playbook.yml
play #1 (localhost): localhost TAGS: [__play]
tasks:
sample : task1 TAGS: [__play, __role1, __tag1]
other : other TAGS: [__other_task, __play, __role2]
other : other_sub TAGS: [__play, __role2]
other : other TAGS: [__other, __other_task, __play]
other : other_sub TAGS: [__other, __play]
sample task TAGS: [__play]
これらのオプションは、--tagや--skip-tagでtag指定した場合は、tag設定によって実行対象となるtaskのみが表示されます。
--checkのさらに1段階前の実行対象のtaskが何になるかを確認するのに利用すると良いです。
例えば--tagを指定すると以下の通り、指定されたtagが設定されている特定taskのみ実行対象になることを確認できます。
$ ansible-playbook -i localhost, playbook.yml --list-tasks --tags __role2
playbook: playbook.yml
play #1 (localhost): localhost TAGS: [__play]
tasks:
other : other TAGS: [__other_task, __play, __role2]
other : other_sub TAGS: [__play, __role2]
neverが設定されたtaskが実行される条件
上記の実行例ではneverが設定されたtaskはリストアップの対象になりません。
--tagsが未指定(あるいはデフォルトのall指定)の場合は、never設定のtaskは処理対象となりません。
neverが設定されたtaskを実行対象にするには以下の通り。
--tags neverを指定する- taskに
never以外にもtagを設定しておき、そのtagを--tagsで指定する neverとalwaysをtaskに同時設定する (--tags指定は不要)
よってnevertagを設定して通常実行されないように設定されているtaskでも、--tagsで明示的に指定されたり、併記・あるいは継承されたtagを--tagsで指定しても実行されます。
言い換えると、taskに設定されたneverを含むtagのうち、どれか一つでも--tagsによって対象と判定されれば(never設定されていても)処理対象となります。
以下のように--tags __role1を指定すると、__role1tagの設定を上位で行ったsampleroleはneverが設定されていても全て実行対象となります。
$ ansible-playbook -i localhost, playbook.yml --list-tasks --tags __role1
playbook: playbook.yml
play #1 (localhost): localhost TAGS: [__play]
tasks:
sample : task1 TAGS: [__play, __role1, __tag1]
sample : task2 TAGS: [__play, __role1, __tag2, never]
sample : task3 TAGS: [__play, __role1, never]
ただしこれは--tagsのみが指定された場合。
--skip-tagsの指定がある場合は、--tagsの判定後に除外判定が行われます。
never設定されたtaskを確実に動かしたくない場合は、--skip-tags neverを指定することで、「taskに設定されたneverを含むtagのうちどれか一つでも--tagsで処理対象と判定」された結果を、除外対象に上書きできます。
$ ansible-playbook -i localhost, playbook.yml --list-tasks --tags __role1 --skip-tags never
playbook: playbook.yml
play #1 (localhost): localhost TAGS: [__play]
tasks:
sample : task1 TAGS: [__play, __role1, __tag1]
実装の調査
というわけで長い前置きでしたが、ここからさらに長くなるおまけコンテンツ(本題)です。
playbookからどうやって設定されているtag情報を読み取って、継承処理を行っているか、その実装を確認してみます。
この辺りはとくにドキュメントで規定されているわけでは(たぶん)ないので、少なくとも現在のAnsible 2.10.5の場合のアーキテクチャの話になります。
今後このあたりの(仕様は変わらなくても)実装が変更されるメジャーバージョンアップなどがあれば話は変わってくるかもしれません。
リスト出力部分
まずは--list-tasksや--list-tagsが指定された場合の「taskとtag情報を保持しつつ実際には処理せずにその情報のみprint」がどうなってるか確認してみます。
リスト表示のみの実行
--list-tasksや--list-tagsが指定された場合の出力箇所はPlaybookCLIのrun()以下。
results変数にplayの情報がセットされており、ループで逐次内容をprintしています。
msg = "\n play #%d (%s): %s" % (idx + 1, ','.join(play.hosts), play.name) mytags = set(play.tags) msg += '\tTAGS: [%s]' % (','.join(mytags))
if context.CLIARGS['listtasks']: cur_tags = list(mytags.union(set(task.tags))) cur_tags.sort() if task.name: taskmsg += " %s" % task.get_name() else: taskmsg += " %s" % task.action taskmsg += "\tTAGS: [%s]\n" % ', '.join(cur_tags)
ここでtask情報のprintを行いreturnするとこのままreturn 0でプログラムは終了するので、リスト表示指定を行わず通常実行の場合では、results変数を取得する前段の以下の時点でターゲットノードに対する各処理は完了していることになります。
results = pbex.run()
通常実行
前述のリスト表示の場合results変数にはplayの情報がセットされていました。
それに対して--list-tasksなどのリスト表示指定をせずに普通に実行すると、run()の戻り値はplayの実行結果(正常なら0、異常なら非0のint)となります。
そのため、リスト表示するかどうか判定するif isinstance(results, list)で処理対象外となり、そのelse節でその戻り値をreturnして終了します。
つまり、--list-tasksなど指定してtask一覧を表示する場合、実際に処理を実行するrun()メソッドはコールされてから(処理が諸々スキップされたのちに)、Ansible実行の最後に出力する実装になっています。
処理分岐の実装箇所
pbex.run()のコールは以下で、前述の通り--list-tagsなどの指定による情報表示の前に行われます。
# create the playbook executor, which manages running the plays via a task queue manager pbex = PlaybookExecutor(playbooks=context.CLIARGS['args'], inventory=inventory, variable_manager=variable_manager, loader=loader, passwords=passwords) results = pbex.run()
コールされているPlaybookExecutor()の実装は以下で、--list-tasksなどの指定が行われている場合、コンストラクタの時点でTaskQueueManagerの初期化を行わずに、このオブジェクトをNoneにセットしています。
if context.CLIARGS.get('listhosts') or context.CLIARGS.get('listtasks') or \ context.CLIARGS.get('listtags') or context.CLIARGS.get('syntax'): self._tqm = None
この後の実処理(run())では、このself._tqmの値がNoneであればリストアップのみとして処理されていきます。
playbookの実行時の処理
playbookファイルのリストを1個ずつ処理していく処理の開始場所は以下の部分。
for playbook_path in self._playbooks: pb = Playbook.load(playbook_path, variable_manager=self._variable_manager, loader=self._loader)
ただし、リスト表示のみの動作の場合は、以下の部分になります。
if self._tqm is None: # we are doing a listing entry = {'playbook': playbook_path} entry['plays'] = []
entryという辞書に、playbookとplaysというキーを持つ変数をセットしています。
この辞書に変数が追加されるのは上記と、以下のもう1か所。
これはplayごとの処理のループ内で、対象playの情報を追加しています。
(このelse節で実処理が走るため、リスト表示時はtaskの実処理が行われない)
if self._tqm is None: # we are just doing a listing entry['plays'].append(play)
play毎の処理のループを抜けたら以下の部分で配列entrylistへentryを追加しています。
if entry: entrylist.append(entry) # per playbook
そしてplaybook毎のループの処理が完了したら、リスト表示時は処理完了。play情報の一覧になっているentrylistをreturnします。
(リスト表示ではなく実処理の場合はこのentrylistは空のままになっているため、ここではreturnされずに処理続行し、最終的にはtask実行が成功した0かエラー時の非0がreturnされるため型が異なる)
if entrylist: return entrylist
よって呼び出し元であるPlaybookCLI#run()は、このreturn値がlist形式か否かで「処理済み」か「処理予定の情報を収集しただけ」かを、isinstance()で判断できます。
tag情報は、playのtagはここで取り出しているplay.tagsで参照しています。
mytags = set(play.tags) msg += '\tTAGS: [%s]' % (','.join(mytags))
同じようにtaskのtagは以下。
(list tasksと list tagsで表示形式が異なるので取得方法も若干異なる)
all_tags.update(task.tags)
if context.CLIARGS['listtasks']:
cur_tags = list(mytags.union(set(task.tags)))
tag情報取得箇所
tag情報のprint箇所はわかったので、playbookで設定されているtagをどこから読み取ってるのか調べてみます。
そうすると、そもそもtagの前にどうやってplayやtaskの情報をロードしているのか、という話になるわけで、そこから追ってみます。
playbookをソースからロード
エントリポイントは、playbook_executor.pyの前述の以下の部分でplaybookの情報をロード。
pb = Playbook.load(playbook_path, variable_manager=self._variable_manager, loader=self._loader)
ここからコールされるのは、以下のplaybook/__init__.pyで実装してあるload()スタティックメソッド。
@staticmethod def load(file_name, variable_manager=None, loader=None): pb = Playbook(loader=loader) pb._load_playbook_data(file_name=file_name, variable_manager=variable_manager) return pb
ここでPlaybookインスタンスを作成し_load_playbook_data()メソッドをコール、そこからplaybookの内容をロードしているのは以下の箇所。
try: ds = self._loader.load_from_file(os.path.basename(file_name)) except UnicodeDecodeError as e: raise AnsibleParserError("Could not read playbook (%s) due to encoding issues: %s" % (file_name, to_native(e)))
ここでコールしているのは、呼び出し元で指定しているloaderインスタンスのload_from_file()メソッド。
これが何かと言うと、playbook_executor.pyのPlaybook.load()コール時の引数のloader=self._loaderの部分。
さらにこれはPlaybookExecutorインスタンス作成時の引数で指定されています。
def __init__(self, playbooks, inventory, variable_manager, loader, passwords): self._playbooks = playbooks self._inventory = inventory self._variable_manager = variable_manager self._loader = loader
じゃあさらに呼び出し元のcli/playbook.pyを見ると、laoderインスタンスは_play_prereqs()で取得しています。
loader, inventory, variable_manager = self._play_prereqs()
_play_prereqs()の実装はここ。
この中でloaderはDataLoader()をコールして取得しています。
def _play_prereqs(): options = context.CLIARGS # all needs loader loader = DataLoader()
そしてこのDataLoader()はこちら。
斜め読みする限り、Ansibleとしての処理ではなくYAMLを読んでいます。
上位からコールされるメソッドのload_from_file()を追っていくと、self.load() -> from_yaml() -> json.loads() と呼ばれて、Pythonの標準パッケージのjsonで処理されています。
(※ YAML形式のplaybookもjsonで処理している)
なので、この時点ではYAMLとして文法が合っていれば、例えば存在しないモジュールや未定義のディレクティブを書いてたりしても読み込み処理までは動作します。
逆に、YAMLとして文法誤りがあれば、例えばインデント誤りなどあればこの時点でエラーになります。
ただし、Ansibleとしては処理されていないため、rolesやimport_playbookのようにAnsibleとしての外部ファイル読み込みもまだ処理されないため、外部ファイルに文法誤りがあってもここではエラー判定は発生しません。
そのままplaybookの処理に戻って内容を追っていくと、呼び出し元で使えるようにplay情報としてリストへ追加しているのはPlaybookクラスの_load_playbook_data()メソッド内下記の2か所で行われます。
pb = PlaybookInclude.load(entry, basedir=self._basedir, variable_manager=variable_manager, loader=self._loader)
if pb is not None:
self._entries.extend(pb._entries)
entry_obj = Play.load(entry, variable_manager=variable_manager, loader=self._loader, vars=vars) self._entries.append(entry_obj)
前者はimport_playbookでplaybookをさらに読み込んでいる場合で、最終的にplaybook内に記述されているplayの読み込みは後者のPlay.load()で、ここで読み取った一つのplay分のYAML情報(entry)をentry_objとして取得し、self._entriesリストに追加されていきます。
playのロード
Play#load()は以下のスタティックメソッドで、Playインスタンスを生成し、load_data()をコールしています。
p = Play()
if vars:
p.vars = vars.copy()
return p.load_data(data, variable_manager=variable_manager, loader=loader)
load_data()の実装はplaybook/base.pyのFieldAttributeBaseクラス。
play以下全taskのロード
load_data()メソッドの下記コードで、全てのplayの情報が再帰的に処理され読み込まれます。
for name, attr in sorted(iteritems(self._valid_attrs), key=operator.itemgetter(1)): # copy the value over unless a _load_field method is defined target_name = name if name in self._alias_attrs: target_name = self._alias_attrs[name] if name in ds: method = getattr(self, '_load_%s' % name, None) if method: self._attributes[target_name] = method(name, ds[name]) else: self._attributes[target_name] = ds[name]
nameにはtasksやroles・そしてtagsなどの属性値が順番にセットされ、_load_tasks()や_load_roles()・_load_tasks()というメソッドが定義されていればそれがコールされ、その戻り値をself._attributesにセットしていきます。
この処理で、taskやroleあるいやimport系の定義による子要素も読み込まれていきます。
よって、この時点で保持されるtag情報は、まだ該当のtaskに設定されているもののみがセットされます。
(呼び出し元のtag情報は継承されていない)
そして実は、全playのデータロードが完了しても、保持されているデータ構造はこのままで、taskに紐づくtag情報はそのtaskに設定されたtagのみとなっています。
(呼び出し元のtag情報をマージした情報が保持される構成にはならない)
継承の仕組み
前述の通り、読み込まれた全taskの情報はtask単位でオブジェクトとして保持されます。
そしてこの個々のtaskオブジェクトは、そのtaskが持つ使用モジュールやwhenやuntilなどの条件、registerなど、各ディレクティブ設定の情報によって構成されています。
(前述の通りself._attributes[key]で参照できる)
設定が継承される動作は、これらの値を参照するタイミングで親情報と結合された内容となる処理によって実装されています。
taskのtag情報などは前述の通りself._attributes['tags']に保持されていますが、実際のコードは.tagsというプロパティ値でアクセスしています。
参照箇所は例えば、--list-tasksで実行した際には以下の通り。
# https://github.com/ansible/ansible/blob/stable-2.10/lib/ansible/cli/playbook.py#L143 mytags = set(play.tags)
# https://github.com/ansible/ansible/blob/stable-2.10/lib/ansible/cli/playbook.py#L171 cur_tags = list(mytags.union(set(task.tags)))
この.tagsというプロパティアクセスは、Pythonの組み込み関数property()を使ったメンバアクセス関数設定とfunctools#partial()を使った引数固定の関数指定を使用し、getterのメソッドとして_generic_g_parent()をコールするように、tagsに限らず各属性に対する定義をメタクラスで行っています。
method = "_get_attr_%s" % attr_name if method in src_dict or method in dst_dict: getter = partial(_generic_g_method, attr_name) elif ('_get_parent_attribute' in dst_dict or '_get_parent_attribute' in src_dict) and value.inherit: getter = partial(_generic_g_parent, attr_name) else: getter = partial(_generic_g, attr_name) setter = partial(_generic_s, attr_name) deleter = partial(_generic_d, attr_name) dst_dict[attr_name] = property(getter, setter, deleter)
このアクセサメソッド設定を、Taggableクラスで定義されている以下の_tagsという名前のプロパティ名を使用し、
class Taggable: untagged = frozenset(['untagged']) _tags = FieldAttribute(isa='list', default=list, listof=(string_types, int), extend=True)
メタクラスの以下のコード部分でtagsという先頭の_を除いた名前でプロパティを動的に設定しています。
keys = list(src_dict.keys()) for attr_name in keys: value = src_dict[attr_name] if isinstance(value, Attribute): if attr_name.startswith('_'): attr_name = attr_name[1:]
これでattr_nameには_tagsの先頭_が取り除かれtagsがセットされ、前述の_generic_g_parent+partial+propertyの設定をdst_dict[attr_name]にセットすることで、.tagsというプロパティアクセスを実装しています。
value.inherit:についてはAtrributeクラスのデフォルト値でTrueとなるように実装されます。
※ なので、ここを改造してFalseにセットして動作させると、tagの継承は発生しません。
def __init__( self, isa=None, private=False, default=None, required=False, listof=None, priority=0, class_type=None, always_post_validate=False, inherit=True, alias=None, extend=False, prepend=False, static=False, ):
以上の実装によって、.tagsプロパティに参照すると、_generic_g_parent(prop_name=tags, self)がコールされるようになります。
_generic_g_parent()の内容についてはメタクラスと同じbase.pyにて実装されています。
def _generic_g_parent(prop_name, self): try: if self._squashed or self._finalized: value = self._attributes[prop_name] else: try: value = self._get_parent_attribute(prop_name) except AttributeError: value = self._attributes[prop_name] # _snip_
(条件によって異なりますが基本的には)_get_parent_attribute()をコールして戻り値を取り出しています。
ではこのメソッドがどこにあるかと言うと、これは呼び出し元(self)が実装しているもので、以下の2クラスで現状は実装されています。
Blockクラス側はRoleの場合の実装の違いなどはありますが、「親情報(_parent)がある場合は、_get_parent_attribute()を再帰的に呼び出す」という処理によって、そのtaskが持つ呼び出し元の設定も含めた情報が得られるようになっています。
playbookにおけるtag設定が呼び出し元の設定も継承するのはこの仕組みによって実装されています。
実行対象のtagかどうかを評価する箇所
ansible-playbookの実行時、playbookにおけるtagsの設定に加え、コマンドラインオプションの--tagsおよび--skip-tagsの指定によって、対象taskが実行されるかどうかが決定します。
指定したオプションは以下のPlay#__init__の実装の通り、only_tagsが--tags指定のもの、skip_tagsが--skip_tags指定のものとして処理されます。
そして、--tags指定を行わなかった場合は、自動的にallが指定されます。
self.only_tags = set(context.CLIARGS.get('tags', [])) or frozenset(('all',)) self.skip_tags = set(context.CLIARGS.get('skip_tags', []))
判定のロジックは以下のTaggable#evaluate_tags()の通りで、まず--tagsによって処理対象かどうかが決定し、その後に--skip-tagsで除外設定かを判定します。(--skip-tagsの指定が優先される)
should_run = True # default, tasks to run if only_tags: if 'always' in tags: should_run = True elif ('all' in only_tags and 'never' not in tags): should_run = True elif not tags.isdisjoint(only_tags): should_run = True elif 'tagged' in only_tags and tags != self.untagged and 'never' not in tags: should_run = True else: should_run = False if should_run and skip_tags: # Check for tags that we need to skip if 'all' in skip_tags: if 'always' not in tags or 'always' in skip_tags: should_run = False elif not tags.isdisjoint(skip_tags): should_run = False elif 'tagged' in skip_tags and tags != self.untagged: should_run = False return should_run
大原則としては以下の通り。
--tagsで指定されなくても動かしたければalwaysを設定 (ただし--skip-tags指定で上書きされる)- tagが設定されたtaskを動かしたければ
--tagsで指定 (ただし--skip-tags指定で上書きされる) - tagが設定されたtaskを動かしたくなければ
--skip-tagsで指定
気を付けるべきは、指定tag以外のtagが設定されたtaskや、--tagsや--skip-tagsで指定しなかったtagのtaskがどうなるか。
単純に「nevertagを設定しているから動かないだろう」「--tagsで指定してないから動かないだろう」は継承によって認識外のtagが付与される可能性もあるので危険。
tag以外 (クイズ)
例えばwhenによる条件も継承されます。
playbook.yml
--- - hosts: localhost gather_facts: false vars: cond1: true cond2: true roles: - role: sample when: - cond1 is defined and cond1|bool - cond2 is defined and cond2|bool - other
roles/sample/tasks/main.yml
--- - name: roled task 1 debug: msg: "cond1 {{cond1}} , cond2 {{cond2}}" - name: set cond2 to false set_fact: cond2: 0 - name: roled task 2 debug: msg: "cond1 {{cond1}} , cond2 {{cond2}}" - name: set cond2 to true set_fact: cond2: 1 - name: roled task 3 debug: msg: "cond1 {{cond1}} , cond2 {{cond2}}"
どうでしょう、5つのうちどのtaskが動作するかわかりますか?
正解は以下の通り。
$ ansible-playbook -i localhost, playbook.yml PLAY [localhost] ***************************************************************************************************************** TASK [sample : roled task 1] ***************************************************************************************************** ok: [localhost] => msg: cond1 True , cond2 True TASK [sample : set cond2 to false] *********************************************************************************************** ok: [localhost] TASK [sample : roled task 2] ***************************************************************************************************** skipping: [localhost] TASK [sample : set cond2 to true] ************************************************************************************************ skipping: [localhost] TASK [sample : roled task 3] ***************************************************************************************************** skipping: [localhost] PLAY RECAP *********************************************************************************************************************** localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
まとまった情報が見当たらないのですが、多くのディレクティブ/playbookキーワードは継承されます。
例えばこのあたりの指定は呼び出し先のtaskに継承されます。
- base.pyで定義
- conditional.pyで定義 (blockなど)
以下のtask.pyのloop_controlのアクセサメソッドの実装のように、inherit=Falseというパラメタを指定しているものは機能は継承されませんが、前述のリストのようにそうでないものは機能が継承される実装になっているようです。
_loop_control = FieldAttribute(isa='class', class_type=LoopControl, inherit=False)
まとめ
- playbookにおける
tagの設定は、そのtaskに設定されてるものが全てではないので気を付けましょう。 --list-tasksや--list-tagsを活用しましょう。