zaki work log

作業ログやら生活ログやらなんやら

[Ansible] そのtag設定、想定通りに動いてますか? (継承機能とその実装を確認する)

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設定も継承されるという特徴があります。

docs.ansible.com

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 nevernevertagが設定されたtaskは他の有効なtagがあっても動かない
  • ansible-playbook実行時は、まず--tagsで処理対象taskかチェックされてから--skip-tagsで除外対象かをチェックされる
    • --skip-tagsの指定が優先される
    • --skip-tags alwaysalwaystag設定された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設定無し
  • playはrolesも使ってroleを2つ定義
    • role1は__role1tagを設定してsampleroleを設定
    • role2は__role2tagを設定して(taskと同じ)otherroleを設定

tasksrolesを同時指定した場合の処理順はroles->tasks

docs.ansible.com

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のように要素に対して設定されてるものはブロックの中へ、rolesimport_roleimport_tasksのように別定義の呼び出し時に設定されるものは矢印にtagを記載しています。

f:id:zaki-hmkc:20210324093715p:plain

ここで、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以下入れ子になっている条件Bifは評価されずに処理は進みます。

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)を見てみると、呼び出し階層は以下のようになります。

f:id:zaki-hmkc:20210324094012p:plain

コマンドラインオプションの--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

docs.ansible.com

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で指定する
  • neveralwaysを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という辞書に、playbookplaysというキーを持つ変数をセットしています。
この辞書に変数が追加されるのは上記と、以下のもう1か所
これはplayごとの処理のループ内で、対象playの情報を追加しています。
(このelse節で実処理が走るため、リスト表示時はtaskの実処理が行われない)

                    if self._tqm is None:
                        # we are just doing a listing
                        entry['plays'].append(play)

play毎の処理のループを抜けたら以下の部分で配列entrylistentryを追加しています。

                if entry:
                    entrylist.append(entry)  # per playbook

そしてplaybook毎のループの処理が完了したら、リスト表示時は処理完了。play情報の一覧になっているentrylistreturnします
(リスト表示ではなく実処理の場合はこの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()の実装はここ
この中でloaderDataLoader()をコールして取得しています。

    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としては処理されていないため、rolesimport_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.pyFieldAttributeBaseクラス

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にはtasksroles・そして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が持つ使用モジュールやwhenuntilなどの条件、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   

docs.ansible.com

まとまった情報が見当たらないのですが、多くのディレクティブ/playbookキーワードは継承されます。
例えばこのあたりの指定は呼び出し先のtaskに継承されます。

以下のtask.pyloop_controlのアクセサメソッドの実装のように、inherit=Falseというパラメタを指定しているものは機能は継承されませんが、前述のリストのようにそうでないものは機能が継承される実装になっているようです。

    _loop_control = FieldAttribute(isa='class', class_type=LoopControl, inherit=False)

まとめ

  • playbookにおけるtagの設定は、そのtaskに設定されてるものが全てではないので気を付けましょう。
  • --list-tasks--list-tagsを活用しましょう。