はじめに
Ansible にはロールの実行に必要な変数のバリデーションができる「Role argument validation」という機能があります。ansible-core 2.11 から利用できます。
どのような変数が必須か、どういう値を取りうるか(選択肢)を、各ロールの meta/argument_specs.yml
に定義しておくことで、ロールの呼び出す際、実際の処理をする前に仕様に応じたバリデーションができます。
tekunabe.hatenablog.jp
いくつか注意が必要かなと思う点があるので、この記事でまとめます。
環境は以下の通りです。今後のバージョンアップで変更されるかもしれません。
- 前提環境
- ansible-core 2.16.6
ansible.builtin.import_role
モジュールによるロール呼び出し
注意が必要なポイント
型の判定がゆるめ
type
で str
、int
、bool
、list
、dict
、などの型を指定できます。デフォルトは 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: list
と elements: 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
のフォーマットにあるもの(required
、choices
)のみと捉えておくのが分かりやすいと思います。
関連 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:
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
を追加すれば、(role1
も role2
も)バリデーションが行われなくなります。ただ、この辺りは凝りだすと複雑になり、思わぬ副作用を誘発しかねないので、ほどほどにした方が良いかなといます。
バリデーションを無効にしたい場合は、ansible.builtin.import_role
モジュールであれば rolespec_validate
を false
に指定するというのも手です。
Role argument validation 以外の選択肢
複雑なチェックには、ansible.utils.validate
モジュールと JSON Schema の組み合わせや、ansible.builtin.assert
モジュール によるチェックが向いていると思います。
おわりに
meta/argument_specs.yml による Role argument validation の注意点をまとめました。
特性を理解したうえで、複雑でないバリデーションであれば有用かなとおもいました。