てくなべ (tekunabe)

ansible / network automation / 学習メモ

[Ansible] meta/argument_specs.yml による Role argument validation の注意点

はじめに

Ansible にはロールの実行に必要な変数のバリデーションができる「Role argument validation」という機能があります。ansible-core 2.11 から利用できます。

どのような変数が必須か、どういう値を取りうるか(選択肢)を、各ロールの meta/argument_specs.yml に定義しておくことで、ロールの呼び出す際、実際の処理をする前に仕様に応じたバリデーションができます。

tekunabe.hatenablog.jp

いくつか注意が必要かなと思う点があるので、この記事でまとめます。

環境は以下の通りです。今後のバージョンアップで変更されるかもしれません。

  • 前提環境
    • ansible-core 2.16.6
    • ansible.builtin.import_role モジュールによるロール呼び出し

注意が必要なポイント

型の判定がゆるめ

typestrintboollistdict、などの型を指定できます。デフォルトは str です。

---
argument_specs:
  main:
    options:
      myvar:
        type: int       # 例

この型の指定で、バシッと型のバリデーションまでできそうですが、実際は割とゆるめです。

傾向としてはキャストができればバリデーションもOKとみなされる雰囲気です。例えば type: int に対しては 100 でも "100" でも OK です。さすがに hoge は NG です。

ほか、定義済みだけど値がない場合はバリデーションが OK になる傾向です。required: true とセットの場合は NG になります。

以下検証結果です。

1. type: str の場合(デフォルト)

定義

---
argument_specs:
  main:
    options:
      mystr:
        type: str

結果

変数の値 バリデーション結果 エラーメッセージ抜粋
100 OK
"100" OK
100.0 OK
hoge OK
true OK
0 OK
値なし OK
[100] OK
{num: 100} OK

「値なし」が OK となっていますが、required: true とセットにした場合は、'None' is not a string and conversion is not allowed というエラーで NG となりました。

2. type: int の場合

定義

---
argument_specs:
  main:
    options:
      myint:
        type: int

結果

変数の値 バリデーション結果 エラーメッセージ抜粋
100 OK
"100" OK
100.0 NG <class 'float'> cannot be converted to an int
hoge NG <class 'ansible.parsing.yaml.objects.AnsibleUnicode'> cannot be converted to an int
true OK
0 OK
値なし OK
[100] NG <class 'list'> cannot be converted to an int
{num: 100} NG <class 'dict'> cannot be converted to an int

「値なし」が OK となっていますが、required: true とセットにした場合は、<class 'NoneType'> cannot be converted to an int というエラーで NG となりました。

3. type: bool の場合

定義

---
argument_specs:
  main:
    options:
      mybool:
        type: bool

結果

変数の値 バリデーション結果 エラーメッセージ抜粋
100 NG 'mybool' is of type <class 'int'> and we were unable to convert to bool: The value '100' is not a valid boolean. Valid booleans include: 0, 1, '0', 'yes', 'on', 'n', 'no', 'f', 't', 'off', 'false', 'y', 'true', '1'
"100" NG 'mybool' is of type <class 'ansible.parsing.yaml.objects.AnsibleUnicode'> and we were unable to convert to bool: The value '100' is not a valid boolean. Valid booleans include: 0, 1, '0', 'yes', 'on', 'n', 'no', 'f', 't', 'off', 'false', 'y', 'true', '1'
100.0 NG 'mybool' is of type <class 'float'> and we were unable to convert to bool: The value '100.0' is not a valid boolean. Valid booleans include: 0, 1, '0', 'yes', 'on', 'n', 'no', 'f', 't', 'off', 'false', 'y', 'true', '1'
hoge NG 'mybool' is of type <class 'ansible.parsing.yaml.objects.AnsibleUnicode'> and we were unable to convert to bool: The value 'hoge' is not a valid boolean. Valid booleans include: 0, 1, '0', 'yes', 'on', 'n', 'no', 'f', 't', 'off', 'false', 'y', 'true', '1'
true OK
0 OK
値なし OK
[100] NG <class 'list'> cannot be converted to a bool
{num: 100} NG <class 'dict'> cannot be converted to a bool

「値なし」が OK となっていますが、required: true とセットにした場合は、<class 'NoneType'> cannot be converted to a bool というエラーで NG となりました。

4. type: listelements: int の場合

リストの変数の場合で、ここでは int の値を持つ仕様を想定します。

定義

---
argument_specs:
  main:
    options:
      mylist:
        type: list
        elements: int

結果

変数の値 バリデーション結果 エラーメッセージ抜粋
100 OK
"100" OK
100.0 NG <class 'str'> cannot be converted to an int
hoge NG <class 'str'> cannot be converted to an int
true NG <class 'str'> cannot be converted to an int
0 OK
値なし OK
[100] OK
{num: 100} NG <class 'dict'> cannot be converted to a list

「値なし」が OK となっていますが、required: true とセットにした場合は、<class 'NoneType'> cannot be converted to a list というエラーで NG となりました。

5. type: dcit の場合

ディクショナリの変数の場合で、ここではさらに中に num というキーで int の値を持つ仕様を想定します。

定義

---
argument_specs:
  main:
    options:
      mydict:
        type: dict
        options:
          num:
            type: int

結果

変数の値 バリデーション結果 エラーメッセージ抜粋
100 NG argument of type 'int' is not iterable
"100" NG dictionary requested, could not parse JSON or key=value
100.0 NG argument of type 'float' is not iterable
hoge NG dictionary requested, could not parse JSON or key=value
true NG argument of type 'bool' is not iterable
0 NG argument of type 'int' is not iterable
値なし OK
[100] NG argument of type 'int' is not iterable
{num: 100} OK

「値なし」が OK となっていますが、required: true とセットにした場合は、<class 'NoneType'> cannot be converted to a dict というエラーで NG となりました。

複雑なチェックはできない

たとえば正規表現によるパターンマッチングや、数値の範囲、他のタスクの実行結果を利用したバリデーションなど、複雑なバリデーションはできません。

モジュールの argument_specs ほどの機能はない

モジュールのコードを読んだり書いたりしたこがある方であれば、argument_spec という言葉を見かけたことがあるかもしれません。(ロールではなく)モジュールのパラメーターの仕様を定義するディクショナリで、内部的にバリデーション的な処理がされます。

argument_spec という名前や、含まれるキーの名前が「Role argument validation」のものと似ていますが、一部異なります。比較すると「Role argument validation」によるバリデーションのほうができることが少ないです。

たとえば「Role argument validation」では、以下のような排他や、条件付きの必須の指定はできないようです。

  • mutually_exclusive
  • required_if
  • required_together
  • required_one_of
  • required_by

VS Code の Ansible の拡張を使って meta/argument_specs.yml を書いていると補完されてしまいますが。

あくまで、対応しているのは meta/argument_specs.yml 内の argument_specs のフォーマットにあるもの(requiredchoices)のみと捉えておくのが分かりやすいと思います。

関連 Issue:

default の指定は動作には関係ない

ドキュメントにも記載がありますが、default で指定した値は机上の値であり、実際に適用されるデフォルト値は defaults/main.yml での指定によります。

バリデーションタスクには always タグが暗黙的につく

バリデーションは1つのタスクのような扱われ方をします。

ドキュメントに記載ありますが、バリデーションのタスクには always タグ 扱いです。

たとえば、meta/argument_specs.yml を定義しているロールを呼びだす以下のPlaybookがあった場合。

---
- name: Test Play int
  hosts: localhost
  connection: local
  gather_facts: false

  tasks:
    - name: Import role1
      ansible.builtin.import_role:    # static な import
        name: role1
      vars:
        myint: 100

ansible-playbook コマンドを --list-tasks オプション付きで実行すると、バリデーション用のタスクに always タグが付いていることが分かります。

$ ansible-playbook -i localhost, playbook.yml --list-tasks

playbook: playbook.yml

  play #1 (localhost): Test Play int    TAGS: []
    tasks:
      role1 : Validating arguments against arg spec 'main'      TAGS: [always]

--list-tags オプション付きの実行でも always タグの存在を確認できます。

$ ansible-playbook -i localhost, playbook.yml --list-tags

playbook: playbook.yml

  play #1 (localhost): Test Play int    TAGS: []
      TASK TAGS: [always]

そのため、タグを指定した Playbook の実行時には少し注意が必要です。

たとえば、以下の Playbook を -t role2 で実行すると、role1 のバリデーション実行、role2 のバリデーション実行、role2 の処理実行、となります。role1 のバリデーション実行がされるところがポイントです。

---
- name: Test Play
  hosts: localhost
  connection: local
  gather_facts: false

  tasks:
    - name: Import role1
      ansible.builtin.import_role:
        name: role1
      tags:
        - role1

    - name: Import role2
      ansible.builtin.import_role:
        name: role2
      tags:
        - role2
$ ansible-playbook -i localhost, playbook2.yml -t role2

PLAY [Test Play] ******************************************************************************************************

TASK [role1 : Validating arguments against arg spec 'main'] ***********************************************************
ok: [localhost]

TASK [role2 : Validating arguments against arg spec 'main'] ***********************************************************
ok: [localhost]

TASK [role2 : Debug] **************************************************************************************************
ok: [localhost] => {
    "msg": "In role2"
}

--skip-tags=always を追加すれば、(role1role2も)バリデーションが行われなくなります。ただ、この辺りは凝りだすと複雑になり、思わぬ副作用を誘発しかねないので、ほどほどにした方が良いかなといます。

バリデーションを無効にしたい場合は、ansible.builtin.import_role モジュールであれば rolespec_validatefalse に指定するというのも手です。

Role argument validation 以外の選択肢

複雑なチェックには、ansible.utils.validate モジュールJSON Schema の組み合わせや、ansible.builtin.assert モジュール によるチェックが向いていると思います。

おわりに

meta/argument_specs.yml による Role argument validation の注意点をまとめました。

特性を理解したうえで、複雑でないバリデーションであれば有用かなとおもいました。