Ansibleのモジュール開発(基礎編)

ANSIBLE

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは、最近どっぷりサバゲーにハマってる藤本です。

Ansibleは数多くの標準モジュールを提供しています。標準モジュールを利用、もしくは標準モジュールを組み合わせて利用することで多くの設定を実現することができます。それでもケースによっては標準モジュールではできないことを実施したくなることがあります。そういった場合にAnsibleは独自のモジュールを実装することで任意の処理を行うことができます。今回は公式ドキュメントをなぞってみて、Ansibleで独自のモジュールを作成する基礎をご紹介します。

その他のAnsibleのモジュール開発シリーズは以下をご参照ください。

公式ドキュメントのページは以下となります。
http://docs.ansible.com/ansible/developing_modules.html

モジュール開発というとすごく大変そうなイメージがありますが、Ansibleのモジュール開発はいくつかのルールに則るだけで、作成するハードルは低いです。開発することよりもOSバージョン、ディストリビューションの差異の吸収、ミドルウェアバージョンアップへの追従などメンテナンスのコストが高いです。

なので、モジュール開発はゴリゴリに実装することはあまりオススメしません。先にも記載しましたが、可能な限り標準モジュールを利用、標準モジュールを組み合わせて実装することをオススメします。

開発言語

Ansible自体や標準モジュールはPythonで書かれていますが、ファイルI/O、標準出力ができれば、その他言語でも問題ありません。ただ、Pythonを利用すれば、モジュール開発にユーティリティライブラリを利用できるので、Pythonを利用することをオススメします。

本エントリでは分かりやすさを優先し、Bashでモジュールを記述します。

Python

python_module.py
#!/usr/bin/env python
print '{}'
# ansible -m python_module -M . 10.255.0.100
10.255.0.100 | SUCCESS => {
    "changed": false
}

Bash

bash_module.py
#!/bin/bash
echo '{}'
# ansible -m bash_module -M . 10.255.0.100
10.255.0.100 | SUCCESS => {
    "changed": false
}

Ruby

ruby_module.rb
#!/usr/bin/env ruby
print '{}'
# ansible -m ruby_module -M . 10.255.0.100
10.255.0.100 | SUCCESS => {
    "changed": false
}

JSON形式による標準出力

最低限覚えておくルールは一つです。モジュールのスクリプトでJSON形式で標準出力してください。JSON形式で標準出力しないとAnsibleによってモジュールはfailed判定となります。

plain.sh
#!/bin/sh
echo "success"
# ansible -m plain -M . 10.255.0.100
10.255.0.100 | FAILED! => {
    "changed": false,
    "failed": true,
    "module_stderr": "",
    "module_stdout": "success\r\n",
    "msg": "MODULE FAILURE",
    "parsed": false
}

Ansibleはモジュールから標準出力のJSONを受け取り、モジュールのステータス判定、実行結果の利用を行うことができます。

ステータス判定

ステータス判定はモジュールの実行による変更の有無や実行の成否を表します。それらを以下のキーで表します。

changed: true or false(変更の有無) failed: true or false(モジュール実行の成否)

両キーともに必須のキーではありません。
changedは前節で空のJSONを返した時の結果から分かるように標準出力に指定しない場合、Ansibleによってfalseが設定されます。
failedは標準出力で渡さなくとも特に影響はありません。エラーと判断した時のみfailedにtrueを指定し、標準出力してください。またシェルスクリプト内でエラーや、スクリプト内で例外が発生した場合、Ansibleによってtrueが設定されます。

サンプルコードを見れば分かりやすいと思います。色が付いていた方が分かりやすいのでPlaybookで実施します。

# vi ok.sh
#!/bin/sh
echo '{"changed": false}'

# vi changed.sh
#!/bin/sh
echo '{"changed": true}'

# vi failed.sh
#!/bin/sh
echo '{"failed": true}'

# vi linux.yml
- hosts: 10.255.0.100
  tasks:
  - ok:
  - changed:
  - failed:

ansible-develop-modulee

このように標準出力の結果によって、okchangedfailedといったステータスが判断されています。逆に言えば、コマンドが失敗したからといって、failedが入っていないJSONを標準出力すれば、正常だと判断されてしまいます。そのようなことからもエラーハンドリングできる言語を選択するとよいでしょう。

冪等性の担保

ここで大事なことは冪等性を担保するのはモジュールの責務となります。
モジュールによってシステムへ変更を加えたことをAnsibleが判断してchangedをtrueで返すわけではありません。changedをtrueで返すのか、falseで返すのかはモジュールの実装によります。モジュール実行時に設定内容が既に設定済みであれば、設定変更は行わず、changedをfalseに指定し(もしくはchangedキー自体を指定せず)、標準出力してください。設定内容が設定されていなければ、設定処理を行い、changeをtrueに指定し、標準出力してください。設定処理が想定外の結果となった場合、failedをtrueに指定し、標準出力してください。それによりモジュールの利用者が把握できるように冪等性が担保されるように実装してください。

実行結果の利用

実行結果の利用はPlaybookでモジュールの実行結果をregisterで変数に受け取ることができ、返ってきた値を他のモジュールのオプションや、条件判断などに利用することができます。これらはモジュールからの標準出力を受け取っています。

こちらもサンプルを書いて理解しましょう。Playbookはchangedモジュールが返すJSONをresultという変数にセットし、debugモジュールでresultの内容を表示します。

# vi linux.yml
- hosts: all
  tasks:
  - changed:
    register: result
  - debug: var=result

ansible-playbook -M . linux.yml -v
Using ansible.cfg as config file

PLAY ***************************************************************************

TASK [setup] *******************************************************************
ok: [10.255.0.100]

TASK [changed] *****************************************************************
changed: [10.255.0.100] => {"changed": true}

TASK [debug] *******************************************************************
ok: [10.255.0.100] => {
    "result": {
        "changed": true
    }
}

PLAY RECAP *********************************************************************
10.255.0.100               : ok=3    changed=1    unreachable=0    failed=0

debugの出力にあるように標準出力のJSONを別のtaskに利用することができています。

モジュール配置パス

標準モジュール(Coreモジュール、Extrasモジュール)は<PYTHON_LIBRARY_PATH>/ansible/modules配下に配置されています。

# ls <PYTHON_LIBRARY_PATH>/ansible/modules
__init__.py  __init__.pyc core         extras

例えば、RedHat系のパッケージ管理のyumモジュールは下記パスに配置されています。

/ansible/modules/core/packaging/os/yum.py
#!/usr/bin/python -tt
# -*- coding: utf-8 -*-

# (c) 2012, Red Hat, Inc
# Written by Seth Vidal <skvidal at fedoraproject.org>
# (c) 2014, Epic Games, Inc.
:

独自モジュールを開発する時に上記の標準モジュールと同じパスに配置するわけにはいきません。モジュールは下記のパターンのパスから探しますので、いずれかに配置するよいでしょう。ちなみにこの順番の優先度でモジュールを探しているように見受けられました。

  1. カレントディレクトリのlibraryディレクトリ
    プロジェクト固有のモジュールであれば、プロジェクトディレクトリ配下にlibraryディレクトリを作成して管理すべきでしょう。

  2. 環境変数のANSIBLE_LIBRARYにより指定されたパス

  3. ansible.cfgのlibraryに指定されたパス
    プロジェクトに関わらず共通で利用するモジュールであれば、2.か3.のどちらかで管理するのがよいでしょう。

  4. ansible / ansible-playbookコマンドオプションの--module-pathにより指定されたパス

モジュールパス配下のディレクトリは考慮されず、モジュールのファイルだけの指定となります。例えば、a/b/c.pyというパスのモジュールでもcで指定可能です。逆に同じモジュール名(ファイル名)の場合、どちらか一方のみが実施されるので、モジュール名が重複しないように気をつけましょう。

引数を受け取る

Ansibleモジュールは引数をKey/Value形式で値を受け取ることができます。例えば、yumモジュールであれば、nameをKeyに、パッケージ名をValueをモジュール引数に指定することで、インストールするパッケージを指定します。

# ansible --become -m yum -a name=httpd 10.255.0.100
10.255.0.100 | SUCCESS => {
    "changed": true,
    "msg": "",
    "rc": 0,
    "results": [
        "読み込んだプラグイン:fastestmirror\nLoading mirror speeds from cached hostfile\n * base: ftp.riken.jp\n * extras: ftp.riken.jp\n * updates: ftp.riken.jp\n依存性の解決をしています\n--> トランザクションの確認を実行しています。\n---> パッケージ httpd.x86_64 0:2.4.6-40.el7.centos.1 を インストール\n--> 依存性の処理をしています: httpd-tools = 2.4.6-40.el7.centos.1 のパッケージ: httpd-2.4.6-40.el7.centos.1.x86_64\n--> 依存性の処理をしています: /etc/mime.types のパッケージ: httpd-2.4.6-40.el7.centos.1.x86_64\n--> 依存性の処理をしています: libaprutil-1.so.0()(64bit) のパッケージ: httpd-2.4.6-40.el7.centos.1.x86_64\n--> 依存性の処理をしています: libapr-1.so.0()(64bit) のパッケージ: httpd-2.4.6-40.el7.centos.1.x86_64\n--> トランザクションの確認を実行しています。\n---> パッケージ apr.x86_64 0:1.4.8-3.el7 を インストール\n---> パッケージ apr-util.x86_64 0:1.5.2-6.el7 を インストール\n---> パッケージ httpd-tools.x86_64 0:2.4.6-40.el7.centos.1 を インストール\n---> パッケージ mailcap.noarch 0:2.1.41-2.el7 を インストール\n--> 依存性解決を終了しました。\n\n依存性を解決しました\n\n================================================================================\n Package           アーキテクチャー\n                                バージョン                  リポジトリー   容量\n================================================================================\nインストール中:\n httpd             x86_64       2.4.6-40.el7.centos.1       updates       2.7 M\n依存性関連でのインストールをします:\n apr               x86_64       1.4.8-3.el7                 base          103 k\n apr-util          x86_64       1.5.2-6.el7                 base           92 k\n httpd-tools       x86_64       2.4.6-40.el7.centos.1       updates        82 k\n mailcap           noarch       2.1.41-2.el7                base           31 k\n\nトランザクションの要約\n================================================================================\nインストール  1 パッケージ (+4 個の依存関係のパッケージ)\n\n総ダウンロード容量: 3.0 M\nインストール容量: 10 M\nDownloading packages:\n--------------------------------------------------------------------------------\n合計                                               4.8 MB/s | 3.0 MB  00:00     \nRunning transaction check\nRunning transaction test\nTransaction test succeeded\nRunning transaction\n  インストール中          : apr-1.4.8-3.el7.x86_64                          1/5 \n  インストール中          : apr-util-1.5.2-6.el7.x86_64                     2/5 \n  インストール中          : httpd-tools-2.4.6-40.el7.centos.1.x86_64        3/5 \n  インストール中          : mailcap-2.1.41-2.el7.noarch                     4/5 \n  インストール中          : httpd-2.4.6-40.el7.centos.1.x86_64              5/5 \n  検証中                  : mailcap-2.1.41-2.el7.noarch                     1/5 \n  検証中                  : httpd-2.4.6-40.el7.centos.1.x86_64              2/5 \n  検証中                  : apr-util-1.5.2-6.el7.x86_64                     3/5 \n  検証中                  : apr-1.4.8-3.el7.x86_64                          4/5 \n  検証中                  : httpd-tools-2.4.6-40.el7.centos.1.x86_64        5/5 \n\nインストール:\n  httpd.x86_64 0:2.4.6-40.el7.centos.1                                          \n\n依存性関連をインストールしました:\n  apr.x86_64 0:1.4.8-3.el7                      apr-util.x86_64 0:1.5.2-6.el7   \n  httpd-tools.x86_64 0:2.4.6-40.el7.centos.1    mailcap.noarch 0:2.1.41-2.el7   \n\n完了しました!\n"
    ]
}

引数はモジュールのスクリプトとともにargsというファイルで渡されます。

こちらもサンプルコードを書いて理解しましょう。

library/option.sh
#!/bin/bash
source `dirname $0`/args
echo "{\"message\":\"$message\"}"
# ansible -m option -a message=hello 10.255.0.100
10.255.0.100 | SUCCESS => {
    "changed": false,
    "message": "hello"
}

ansibleコマンドで引数として渡したmessage変数がスクリプトに渡っていることを確認できます。

Playbookでは以下のように記述します。

- hosts: all  
  tasks: 
  - option: message=hello

ちなみにPythonのモジュールであれば、このようなことは意識せずとも引数を利用することが可能です。

まとめ

いかがでしたでしょうか?
今回はモジュールの基礎から理解するということで分かりやすくBashで記載し、モジュールがどのように使われているのかを理解することができました。次回は実践編ということでPythonのUtilityライブラリを使ったり、OS/ディストリビューションの差異を吸収する方法をお伝えしたいと思います。