Ansible には標準で多数のモジュールが用意されているけど、オリジナルのモジュールを自作することもできて、その方法は公式ドキュメントに詳しく書かれている。
http://docs.ansible.com/ansible/developing_modules.html
モジュールは基本的に実行ファイルとして作ることになるが、パラメータの入力と結果の出力の仕方には幾つか異なるやり方がある。 これは、Ansible のバージョン毎にやり方がそれぞれ違っていてややこしい。 特に、これからリリースされる予定の v2.0 系では、これまで一般的だった引数でパラメータの内容が書かれたファイルをやり取りする方法がどうやら使えなくなるようで混迷が深まりそうな雰囲気だ。
ただ、Python が書ける場合には上記をさほど悲観する必要はない。 Python のユーティリティ API を使ってモジュールを書いておけば v2.0 系でも互換性が保たれるようだ。 これは v2.0 の alpha 2 リリースで確認している。
Ansible をインストールする
今回は使用するプラットフォームが Mac OSX なので Homebrew を使って Ansible をインストールした。 これは別に Python のパッケージマネージャ PIP を使うなど、別のやり方を取っても構わない。
$ brew install ansible
自作モジュールを用意する
まずは自作モジュールを入れるディレクトリを作る。 library という名前のディレクトリがあると、その上位ディレクトリで ansible 系のコマンドを実行した際にモジュールの探索対象になるようだ。
$ mkdir library
それ以外にも、環境変数の ANSIBLE_LIBRARY でパスを指定したり、あるいは ansible 系のコマンド実行時に --module-path オプションを指定するやり方もある。
最初に作るモジュールは、引数を何も取らず現在の時刻を返すというもの。 Python のユーティリティを使ってモジュールを作る場合には ansible.module_utils.basic 以下をワイルドカードインポートする。 そして、モジュールは AnsibleModule のインスタンスで表現することになる。 AnsibleModule クラスをインスタンス化する際のコンストラクタにモジュールの引数を指定するものの、今回は引数を取らないので関係ない。 そして、モジュールの返り値は AnsibleModule#exit_json() メソッドを使って JSON 形式で返すことになる。 モジュールの名前はファイル名とイコールになる。
$ cat << EOF > library/date.py #!/usr/bin/python import datetime from ansible.module_utils.basic import * def main(): module = AnsibleModule( argument_spec=dict() ) date = str(datetime.datetime.now()) module.exit_json(date=date, changed=False) if __name__ == '__main__': main() EOF
上記のモジュールを使った Playbook を用意する。
$ cat << EOF > site.yml --- - hosts: - all tasks: - name: test date: EOF
Playbook を実行してみよう。 実行対象は localhost にしている。
$ ansible-playbook -i "localhost," -c local site.yml -vv PLAY [all] ******************************************************************** GATHERING FACTS *************************************************************** <localhost> REMOTE_MODULE setup ok: [localhost] TASK: [test] ****************************************************************** <localhost> REMOTE_MODULE date ok: [localhost] => {"changed": false, "date": "2015-09-25 02:10:47.601483"} PLAY RECAP ******************************************************************** localhost : ok=2 changed=0 unreachable=0 failed=0
ばっちり。
引数を取るモジュールを作る
次は引数を取るモジュールを作ってみよう。 今度は AnsibleModule クラスをインスタンス化する際に name というパラメータを受け取っている。 デフォルト値は 'World' にして、'Hello, ' という文字列を連結して返すことになる。
$ cat << EOF > library/greet.py #!/usr/bin/python from ansible.module_utils.basic import * def main(): module = AnsibleModule( argument_spec=dict( name=dict(required=False), ) ) args = module.params name = args.get('name') or 'World' message = 'Hello, {name}'.format(name=name) module.exit_json(message=message, changed=False) if __name__ == '__main__': main() EOF
上記のモジュールを使った Playbook を用意する。 name パラメータを渡すパターンと渡さないパターンの二通りで greet モジュールを呼び出している。
$ cat << EOF > site.yml --- - hosts: - all tasks: - name: 引数あり greet: name="Foo" - name: 引数なし greet: EOF
先ほどと同様に実行してみる。
$ ansible-playbook -i "localhost," -c local site.yml -vv PLAY [all] ******************************************************************** GATHERING FACTS *************************************************************** <localhost> REMOTE_MODULE setup ok: [localhost] TASK: [引数あり] ********************************************************** <localhost> REMOTE_MODULE greet name="Foo" ok: [localhost] => {"changed": false, "message": "Hello, Foo"} TASK: [引数なし] ********************************************************** <localhost> REMOTE_MODULE greet ok: [localhost] => {"changed": false, "message": "Hello, World"} PLAY RECAP ******************************************************************** localhost : ok=3 changed=0 unreachable=0 failed=0
name 引数があるものについてはそれが使われて、ないものはデフォルト値が使われていることが分かる。
まとめ
今回は Ansible のモジュールを自作してみた。 モジュールへのパラメータの渡し方はバージョン毎に混迷を極めているものの、Python のユーティリティ API を使ってモジュールを書く場合には特に問題はなさそうだ。 また、今のところプラグインを作るにも結局は Python を書く必要がある。 特に理由がない限り、Python を使わずとも書けることがうたわれていたとしても、Python で書いておくのが無難そうに思える。