Ansible モジュール作成のイロハ

いわゆるサーバ構成管理ツールとしては Chef が不動の人気を博していますが、最近では Python 製の Ansible の情報も多く見かけるようになってきています。現在 Backlog をはじめとするヌーラボの各サービスでは、この Ansible を利用しています。

私たちにとって採用の決め手となったのはサーバ側にインストールの必要がなく ssh 出来さえすればいい点や、YAML を書くだけで大抵の事は出来るといったシンプルさです。ただし、

  • 標準の shell モジュールや command モジュールの組み合わせがツラくなった
  • よくある処理を複数のプロジェクトで共有したくなった

といった場合には独自のモジュールを書くのがおススメです。例えば Backlog では cpanm や plenv また pyenv、AWS の特定の設定を行う用のモジュールを作成して利用しています。

さて、その Ansible でのモジュール作成について二回に渡りご紹介します。本エントリでは、Ansible モジュールの概要と shell でのモジュール作成方法、次回は Python でのモジュール作成方法を取り上げます。エントリ中で紹介するモジュールは以下の GitHub のリポジトリにアップロードしてありますので、是非動かしながら確認してください。Ansible のバージョンは 1.4 です。

https://github.com/nulab/ansible-sample
それでは早速はじめましょう。

モジュールとは

Ansible におけるモジュールは、処理の最小単位で ansible コマンドや Playbook のタスクで指定されたモジュールが処理対象のサーバに転送されて、サーバ上で実行されます。
ansible_module
ですので、標準のモジュールを利用していても Ansible のバージョンが異なれば、仮に同じモジュールであっても中身が違うという事がありえます。作業者が複数いる場合には Ansible そのものや、自作モジュールのバージョンの統一といった点に注意が必要です。

Ansible が標準的に提供している モジュール群 (モジュールライブラリ) は Python で記載されていますが、モジュールを作る観点では、サーバで実行可能であればどのような言語で記述されていても構いません。

Ansible のモジュールは

  • ansible.cfg の library で指定されたディレクトリ
  • 環境変数 ANSIBLE_LIBRARY で指定されたディレクトリ
  • コマンドラインで –module-path で指定されたディレクトリ
  • 実行ディレクトリ直下にある library という名前のディレクトリ

から検索されます。プロジェクト固有のものは playbook を実行するディレクトリの直下に library を作るという最後の方法がバージョン管理的にも実行のしやすさからいっても便利でしょう。 逆に例えば複数のサービスで共有のモジュールは個別バージョン管理を行い、ansible.cfg や ANSIBLE_LIBRARY で追加する形が良いでしょう。

モジュールの入出力

実行時に渡される引数は、モジュールには引数が格納されたファイル名として渡され、その中身は以下のようになります。

引数名1=値1 引数名2=値2 ...

大抵のモジュールは引数を取る事になると思いますので、まず最初にこの引数を正しく受け取る処理を行う必要があります。Python の場合には Ansible が提供しているクラスを利用することで、より簡単に引数を取得することが出来ます。

次に出力ですが、原則 JSON フォーマットにて出力することが推奨されています。処理が成功した場合には、changed というプロパティに boolean の値を設定することで、変更が行われたかどうかを Ansible 側に返すことができます。また、処理に失敗した場合は failed というプロパティに真を設定し、あわせて msg というプロパティに失敗した理由を返すことが推奨されています。

尚、主に shell でのモジュールに向けて、以下のような簡易フォーマットも許可されています。

key=value rc=0 changed=true favcolor=green

これ以外のフォーマットを出力として返すとエラーになってしまいます。

モジュールのデバッグ

Ansible を直接 Git のリポジトリから clone して利用している場合、test-module というユーティリティを使って、開発中のモジュールをローカル環境で実行しながらデバッグする事ができます。

$ git clone https://github.com/ansible/ansible.git
$ source ansible/hacking/env-setup
$ chmod 755 ansible/hacking/test-module

この clone した ansible の場所を ANSIBLE_HOME として、現在開発中のモジュールが library/clipboard とすると、以下のような形でデバッグ実行します。尚、テストとはいえローカルでは実際に処理が走りますのでその点は注意してください。

$ $ANSIBLE_HOME/hacking/test-module -m library/clipboard -a "message='hello world'"
* module boilerplate substitution not requested in module, line numbers will be unaltered
***********************************
RAW OUTPUT
rc=0 changed=false msg='no message found'

***********************************
PARSED OUTPUT
{
    "changed": false, 
    "msg": "no message found", 
    "rc": 0
}

また、最終的にサーバ側で実行されたモジュールファイルの中身を確認したい場合は、

ANSIBLE_KEEP_REMOTE_FILES=1

を指定して実行を行う事でサーバ側に転送されたファイルを残しておくことが出来ます。モジュールは、ssh でログインしたユーザの

  • $HOME/.ansible/tmp/ansible-<タイムスタンプ>-<乱数>

の中に保存されています。(ANSIBLE_REMOTE_TEMP を指定している場合はそのディレクトリ配下) こちらはモジュールの開発時だけでなく、運用時に問題が起こった場合にもデバッグに活用できるでしょう。

シェルスクリプトでモジュールを作る

シェルスクリプトのサンプルとして、ローカルで稼働させて指定したメッセージをそっとクリップボードにコピーするようなモジュールを作ってみましょう。実行のイメージは以下のような形です。

 $ ansible -m clipboard -i hosts local -a "message='hello ansible module'"

これを実行した後にペーストを実行すると message という引数に渡された「hello ansible module」がペーストされるはずです。スクリプトの全体は以下の通りとなります。

#!/bin/bash
eval $(perl -lne 'while(/\s?([^=\s]+)\s?=\s?(\"(?:\\\"|[^\"])+\"|\x27(?:\x27\\\x27\x27|[^\x27])+\x27|\S+)\s?/g){print "$1=$2"}' $1)

is_exist()
{
    (type $1 > /dev/null 2>&1) && echo "yes"
}

setup_command()
{
    if [ "$(is_exist pbcopy)" == "yes" ]; then 
        cmd="pbcopy"
    elif [ "$(is_exist xsel)" == "yes" ]; then
        cmd="xsel --input --clipboard"
    elif [ "$(is_exist putclip)" == "yes" ]; then
        cmd="putclip"
    fi

    if [ -z "${cmd}" ]; then
        printf "failed=true msg='clipboard copy command not found'\n"
        exit 1
    fi    
}

setup_command
if [ -z "${message}" ]; then
    printf "rc=0 changed=false msg='no message found'"
    exit 0
fi

echo -n "${message}" | ${cmd}
printf "rc=$? changed=true cmd=\'${cmd}\' message=\'$message\'\n"

shell でモジュールを書くときのポイントは、まずファイルで渡された引数を評価するところです。clipboard スクリプトでは、

eval $(perl -lne 'while(/\s?([^=\s]+)\s?=\s?(\"(?:\\\"|[^\"])+\"|\x27(?:\x27\\\x27\x27|[^\x27])+\x27|\S+)\s?/g){print "$1=$2"}' $1)

というようにして、各々の「変数名=値」を eval にて評価することで shell 内で変数として扱えるようにしています。この際、もし何らかの ansible の不具合があり、

a=b rm -fr /*

といった引数のファイルが来た場合にそのまま評価してしまうと大変な事になってしまいます。これは極端な例ですが、不慮のコマンド実行を避けるため、シェルで変数宣言として評価出来る以下の文字のみを受け取るための処理をしています。= の前後の空白は省くようにしてます。

  • a=bcd # 空白を含まない文字列
  • a=”b \”c=d” # ダブルクォートで囲まれた文字列 ( 空白やエスケープしたダブルクォートは許可 )
  • a=’b ‘\”c=d’ # シングルクォートで囲まれた文字列 ( 空白や ‘\” によるシングルクォートは許可 )

また、この処理はスクリプト内の変数の値を上書きしてしまう可能性があるため、一番最初に呼び出すようにしています。

それ以外の部分はごくごく普通の shell スクリプトです。最終的な返り値だけ上で記述したような、フォーマットとルールに従って出力を行います。

クリップボードコピーのコマンドが見つからなかったとき

printf "failed=true msg='clipboard copy command not found'\n"

引数が与えられなかったとき ( changed=false で正常終了 )

printf "rc=0 changed=false msg='no message found'"

正しく実行されたとき ( changed=true で正常終了 )

printf "rc=$? changed=true cmd=\'${cmd}\' message=\'$message\'\n"

次回の Python で記述するほうが便利なユーティリティなどもあり基本はそちらがおススメではありますが、お決まりのビルド処理などを記述する場合には shell でも十分な事もあると思います。

Ansible モジュール作成の基本についてご紹介しました。次回の Python でのモジュール作成についてもお楽しみに!

より良いチームワークを生み出す

チームの創造力を高めるコラボレーションツール

製品をみる