bashスクリプトではじめての自作Ansibleモジュール

# はじめに

Ansibleのモジュールはいくつかの簡単なルールさえ知っていれば自分で作って追加することができる。

そのルールに従うように既存のシェルスクリプトに対して多少修正を加えてやれば、それをAnsibleのモジュールにすることもできる。
また、Ansibleのplaybookは文法が許す表現力という点でどうしても書きづらい処理が現れることがある。そのような箇所はモジュールとして外出しにしてやることで簡単に書けることがある。

[公式ドキュメント](http://docs.ansible.com/developing_modules.html)には主にPythonでのモジュールの書き方が書かれているが、ルールに従っていればPythonでなくても良く、ここではbashスクリプトをAnsibleモジュールにする方法を説明する。

# Ansibleモジュールのルール

Ansibleモジュールとするには次のルールに従うだけで良い。

1. playbookがあるディレクトリに作ったlibraryという名前のディレクトリにスクリプトファイルを置く
2. スクリプトファイルの1行目にshebang行を書く
3. スクリプトファイルが最後に行う標準出力への出力を"key=value"（スペース区切りで複数可）とする

以下でもう少し説明する。

## モジュールを配置するディレクトリ

モジュールとなるスクリプトを配置するlibraryという名前のディレクトリを作る必要がある。
libraryディレクトリを作る場所はどこでも良いというわけではなく、playbookが置かれているディレクトリに作らなければならない。
またplaybookを使用しない、つまり、ansible-playbookコマンドではなくansibleコマンドを実行する場合にはカレントディレクトリに作る。

モジュールとするスクリプトファイルはこのlibraryディレクトリの中に置くが、スクリプトファイル名には拡張子を付けない。付けても良いがモジュール名は単純にファイル名と同じなので、モジュール名も拡張子付きの名前になるだけだ。

つまりlibraryディレクトリにmodule1というスクリプトファイルを置けばモジュール名はmodule1になるし、module2.shというスクリプトファイルを置けばモジュール名はmodule2.shになる。

置いたスクリプトファイルに実行権限を付与する必要はない。

### 参考

libraryディレクトリの代わりに、[公式ドキュメント](http://docs.ansible.com/developing_modules.html)にあるように、環境変数ANSIBLE_LIBRARY、あるいはansibleコマンドやansible-playbookコマンドの--module-path(-M)オプションでモジュールを配置するディレクトリを指定しても良い。

libraryディレクトリの中にさらにディレクトリを作って、そこにスクリプトファイルを置いても認識されるので、モジュールをカテゴリごとにディレクトリ分けするなどもできる。

参照： [自作Ansibleモジュールをライブラリ化するときのディレクトリ構成](http://qiita.com/mrk_21/items/b50e0709a5afb8021bc0)

スクリプトファイル名には拡張子を付けない、と書いたが唯一の？例外となるのはPowerShellスクリプトの拡張子である「.ps1」である。module3.ps1というスクリプトファイルをlibraryディレクトリに置くとモジュール名はmodule3あるいはmodule3.ps1のどちらであっても呼び出される。

## shebang行

モジュールとなるスクリプトファイルの1行目にはインタプリタがどれであるかを定義したshebang行を置く必要がある。といってもスクリプトファイルには元々書いている人が多いだろう。
bashスクリプトなら`#!/usr/bin/bash`や`#!/usr/bin/env bash`で良い（CentOSの場合）。

## 標準出力への出力

標準出力へはkey=value（複数出力する場合はスペース区切り）のフォーマットで出力しないと、モジュール実行時にエラーになる。
つまり以下のようなフォーマットで標準出力に出力する必要がある。

```
a=10 b=20 c=30
```

このkey=valueの組は最低1つ以上出力されないとエラーになる。
bashスクリプトなら標準出力へは、echoを使って出力するのが最も一般的だろう。

なお、どうやら最初に現れたkey=value以前の出力は無視するようなので、key=valueの組はスクリプトの終了部で出力してやればよい。
最初に出力されたkey=valueより後にkey=valueのフォーマットに従わない文字列が標準出力に出力された場合はエラーになる。

また、標準エラー出力には何が出力されていても無視するようで、特にエラーにはならない。

### 参考

key=valueの区切り文字はスペース以外に改行でも良い。
そのため、key=valueを出力するechoを複数回実行するのも良い。

key=valueのフォーマットを使わず、JSONフォーマットで出力することもできる。

# というわけで、hello world

結局のところ、もっとも簡単な自作モジュールを作って実行するには、Ansibleがインストールされたコンピュータで次を行うだけで良い。

```
# mkdir library
# echo '#!/usr/bin/bash' > library/helloworld
# echo 'echo "hello=world"' >> library/helloworld
# ansible localhost -m helloworld
```

出力は以下のようになる。

```
localhost | success >> {
    "hello": "world"
}
```

先程実行したechoで2行書いただけだが、一応library/helloworldの中身がこうなっているというのも示しておく。

```bash:library/helloworld
#!/usr/bin/bash
echo "hello=world"
```

# モジュールの実行を失敗させる

ここまでのルール通りに作ったAnsibleモジュールの実行は常に失敗しない。
これはスクリプトから0以外でexitした場合であってもステータスはok(success)になり、failedにはならないという意味だ。

しかし、スクリプトが途中でエラーになった場合はステータスをfailedとなるようにしたい場合はどうすれば良いだろうか。

まず1つの手として、標準出力に出力する内容をkey=valueでないものにするとエラーになることを逆手に取る、という方法がある。
これは既存のスクリプトをAnsibleモジュールとして流用し、かつ元々エラー時には標準出力に何らかの文字列を出力するようになっている場合に、修正箇所が少なくなるので有用な手かもしれない。

もう1つ、標準出力に出力する文字列に`rc=<0以外の数字>`を含ませる、という方法がある。まあお作法的にはこちらの方が正しい方法だ。
「0以外の数字」はexitの数字と同じで良いだろう。
`rc=0`とした場合はステータスはokとなる。

## モジュールの実行でchangedにする

playbook中のモジュール実行でステータスがokの場合は、さらにchangedかどうかが最終的な出力にレポートされる。

changedだったかをモジュールから返すには、標準出力に出力する文字列に"changed=1"を含ませる。実際には1でなくても、0やfalseでなければ何でもよい。

## モジュールのサンプル

それではここで、「/srv/mod」というファイルがなければ作る（そしてchangedを返す）、あれば何もしない、ただしそもそもディレクトリ「/srv」がなければエラーとするモジュールの例を示す（名前をm1とする）。

```bash:library/m1
#!/usr/bin/bash

FILENAME=/srv/mod
DIRNAME=`dirname $FILENAME`

if [ ! -d $DIRNAME ]; then
  echo "rc=1 msg='dir not found'"
  exit 1
elif [ -f $FILENAME ]; then
  echo "rc=0 changed=0"
  exit 0
fi

touch $FILENAME
echo "rc=0 changed=1"
```

# オプションを指定する

Ansibleで元々提供されているほぼすべてのモジュールは、いくつかのオプションを指定できるようになっている。
playbook内だと「<モジュール名>: 」の後に、ansibleコマンドだと-aオプションで指定するあれである。

当然自作のモジュールでも同様にオプションを指定して使いたいことは良くある。

モジュールにオプションを指定した時、それはスクリプトファイルとは別のファイルに書かれ、スクリプトファイルとともに操作される側のコンピュータに渡される。
指定したオプションをコマンドライン引数としてスクリプトが実行されるわけではない。

代わりに第1引数には、このオプションが書かれたファイルのパスが渡されている。つまりbashスクリプトでは次のようにすれば、渡されたオプションを変数$OPTSに格納することができる。

```
read OPTS < $1
```

なお、モジュールの実行終了後にはスクリプト、オプションが書かれたファイルはともに操作される側のコンピュータ上から削除される。
デバッグなどの目的で、これらのファイルが削除されないことを望む場合、以下のように実行する。

```
ANSIBLE_KEEP_REMOTE_FILES=1 ansible -vvv (以下省略)
```

あるいは、

```
ANSIBLE_KEEP_REMOTE_FILES=1 ansible-playbook -vvv (以下省略)
```

これらファイルのパスは-vvvオプションによって表示される。通常は`$HOME/.ansible/tmp/ansible-tmp-<時刻を表す数字の羅列>-<よくわからない数字の羅列>`というディレクトリが作られ、ここに置かれる。

Ansibleで元々提供されているモジュールは、オプションもkey=valueフォーマットで指定することが多いが、これはpythonでモジュールを作る用にAnsible側で用意されたユーティリティがkey=valueフォーマットを期待するからである。
そんなユーティリティなどないbashスクリプト製のAnsibleモジュールではkey=valueフォーマットである必要は特に感じない。

## モジュールのサンプル

それでは先程のm1モジュールでは「/srv/mod」固定だった箇所を、次のm2モジュールでは-fオプションで指定できるようにした。

```bash:library/m2
#!/usr/bin/bash

read OPTS < $1

GETOPT=`getopt -q -o f: -- $OPTS`
eval set -- "$GETOPT"

while :
do
  case "$1" in
    -f)
      FILENAME=$2
      shift 2
      ;;
    --)
      shift
      break
      ;;
    *)
      shift
      break
      ;;
  esac
done

if [ -z $FILENAME ]; then
  echo "rc=2 msg='option: -f <filename>'"
  exit 2
fi

DIRNAME=`dirname $FILENAME`

if [ ! -d $DIRNAME ]; then
  echo "rc=1 msg='dir not found'"
  exit 1
elif [ -f $FILENAME ]; then
  echo "rc=0 changed=0"
  exit 0
fi

touch $FILENAME
echo "rc=0 changed=1"
```

オプションの処理にはgetoptを使用した。
\$@をgetoptに食わせる場合はダブルクォートで囲んで"\$@"ととするが、ここでの$OPTSはダブルクォートで囲ってはならないことに注意。

このモジュールは一例として以下のように実行する。

```
# ansible localhost -m m2 -a '-f /srv/mod'
```

# さいごに

既存のbashスクリプトを移植する場合について。

ここまで見てきたように、既存のbashスクリプトであってもオプションを処理する箇所と標準出力に出力する箇所を何とかすればAnsibleモジュールとして移植することができる。

オプションを処理する箇所はそれほど大した修正は要らないように思える。
しかし、大き目のbashスクリプトなどではexitのある箇所（少なくとも正常終了した箇所）すべてで標準出力に出力する処理を追加するのは多少面倒かもしれない。私はやったことがないがtrapを使うと楽かもしれない。

また、bashスクリプトの中で別のbashスクリプトを呼ぶようなものを移植する際には、その別のbashスクリプトをあらかじめ操作対象のコンピュータにコピーするなどの措置が必要になるだろう。