はじめてのAnsibleのモジュール開発

はじめに

Ansibleのモジュールは、ansibleコマンドやansible-playbookコマンドから利用されるスクリプトです。

モジュールは、引数を受け取り、何らかの処理を実行した後に、標準出力にJSON形式で実行結果を出力します。

+---------+                               +----------------+
| ansible +---{引数を格納したファイル}--->| module program |
+----^----+                               +--------+-------+
     |                                             |
     +----------{戻り値:stdout(JSON形式}-----------+

引数

引数は以下のようにハンドリングされます。(※v2系の場合)

  • モジュールには、引数としてkey=value形式のパラメータを渡すことができます
  • 引数は、パラメータファイル(ファイル名:args)に書き込まれます
  • Ansibleは、モジュールスクリプト実行時の引数(sys.argv[1])として、このファイル名を指定することで、パラメータファイル名をモジュールに渡します
  • モジュールは、このファイルを実行時に読み込んで、パラメータを利用します

戻り値

JSON形式のデータ構造を標準出力(stdout)に出力することで、モジュールの実行結果をansibleコマンドやansible-playbookコマンドに渡すことができます。

はじめてのAnsibleモジュール

本家のチュートリアルのサンプルモジュール(timetest.py)は、以下のとおりです。このモジュールは、実行環境に対して変更を加えないので、changedパラメータはFalseとして設定しています。

※本家のコードに対してchangedパラメータを追加しています

#!/usr/bin/python

import datetime
import json

date = str(datetime.datetime.now())
print json.dumps({
    "changed": False,
    "time" : date
})

このサンプルモジュールは、引数は受け取らず、現在時刻を取得して戻り値として返します。この戻り値は、前述の通りJSON形式である必要があるため、json.dumps()を利用して、データ構造をJSON形式に変換して標準出力に出力しています。

このサンプルモジュール(timetest.py)をカレントディレクトリに配置して、ansibleコマンドを実行してみましょう。

% echo "127.0.0.1 ansible_connection=local" | tee hosts
127.0.0.1 ansible_connection=local
% ansible localhost -i hosts -M . -m timetest.py
localhost | SUCCESS => {
    "changed": false,
    "time": "2017-01-19 14:01:30.328960"
}

想定通りの実行結果(現在時刻)が得られました。

モジュールにパラメータを指定する

モジュールに渡されたパラメータは、モジュールと同じディレクトリに“args”というファイル名で配置されます。Ansibleは、モジュール実行時に、このファイル名を最初の引数に指定して実行します。(※v2系の場合)

#!/usr/bin/python
"""
Description:
  Sample module for checking how to handle command-line options.
"""
from __future__ import print_function

import json
import shlex
import sys


def main():
    result = dict()
    result['changed'] = False
    result['message'] = 'Hello, World!'

    with open(sys.argv[1], 'rb') as args_file:
        args_buffer = args_file.read()

    for arg in shlex.split(args_buffer):
        (key, value) = arg.split('=')
        if key == 'name':
            result['message'] = 'Hello, {name:s}!'.format(name=value)

    print(json.dumps(result))


if __name__ == '__main__':
    main()

それでは、以下のサンプルモジュール(paramtest.py)をカレントディレクトリに配置して、ansibleコマンドを実行してみましょう。

% ansible localhost -i hosts -M . -m paramtest.py -a name=Hideki
localhost | SUCCESS => {
    "changed": false,
    "message": "Hello, Hideki!"
}

argsファイルの実際の内容は、以下のようにスペースで区切られた key=value形式となっています。このサンプルモジュールも、実行環境に変更を加えないため、changedパラメータはFalseとしています。

_ansible_version=2.2.1.0 _ansible_selinux_special_fs='['"'"'fuse'"'"', '"'"'nfs'"'"', '"'"'vboxsf'"'"', '"'"'ramfs'"'"']' name=Hideki _ansible_module_name=paramtest.py _ansible_verbosity=0 _ansible_syslog_facility=LOG_USER _ansible_diff=False _ansible_debug=False _ansible_check_mode=False _ansible_no_log=False

ここから、キーであるnameの値を取り出して戻り値にセットしています。

実行環境の状態を変化させる

次に、実行環境を変更するモジュールを書いていましょう。このサンプルモジュール(touchtest.py)は、以下のような仕様とします。

  • nameパラメータに指定したファイル名で空ファイルを作成する(changed: True)
  • nameパラメータに指定したファイル名と同名のファイルが存在していたら何もしない(changed: False)
#!/usr/bin/python
"""
Description:
  Sample module for checking how to change your system.
"""
from __future__ import print_function

import json
import shlex
import os
import sys


def main():
    result = dict()
    result['changed'] = False

    filename = None
    with open(sys.argv[1], 'rb') as args_file:
        args_buffer = args_file.read()

    for arg in shlex.split(args_buffer):
        (key, value) = arg.split('=')
        if key == 'name':
            filename = value

    if not os.path.exists(filename):
        result['changed'] = True
        target_fd = open(filename, 'w')
        target_fd.close()

    result['filename'] = filename
    print(json.dumps(result))


if __name__ == '__main__':
    main()

それでは、以下のサンプルモジュール(touchtest.py)をカレントディレクトリに配置して、ansibleコマンドを実行してみましょう。

まずはカレントディレクトリに存在していないファイル名(hello)を指定します。

% ansible localhost -i hosts -M . -m touchtest.py -a name=hello
localhost | SUCCESS => {
    "changed": true,
    "filename": "hello"
}

作成されたファイル(hello)のステータスを確認してみましょう。

% stat hello
16777220 44124007 -rw-r--r-- 1 saitou staff 0 0 "Jan 19 16:11:25 2017" "Jan 19 16:11:25 2017" "Jan 19 16:11:25 2017" "Jan 19 16:11:25 2017" 4096 0 0 hello

新たに空ファイルが作成されました。この処理では、実行環境に変更を加える(空ファイルが作成される)ため、changedパラメータをTrueに設定しています。

次に、全く同じ作業を実施してみます。

% ansible localhost -i hosts -M . -m touchtest.py -a name=hello
localhost | SUCCESS => {
    "changed": false,
    "filename": "hello"
}

ファイル(hello)のステータスを確認してみましょう。

% stat hello
16777220 44124007 -rw-r--r-- 1 saitou staff 0 0 "Jan 19 16:11:25 2017" "Jan 19 16:11:25 2017" "Jan 19 16:11:25 2017" "Jan 19 16:11:25 2017" 4096 0 0 hello

すでにファイルが存在しているため、os.path.exists(filename)の評価結果がTrueとなり、ファイルに関する操作がスキップされます。この場合は、実行環境が変化しませんから、changedパラメータにFalseをセットしてやります。

このようにして、1つ1つ丁寧にモジュールの中の処理で冪等性を担保してやるわけです。

Ansibleモジュールのお作法にしたがう

Ansibleが推奨しているモジュールの書式にしたがって、touchtest.pyを書き直してみましょう。

モジュールの基本構造

Ansibleでは、モジュールの作成が楽になるよう、さまざまな機能がライブラリとして提供されています。これらを利用すれば、引数のハンドリングや、戻り値のフォーマットなど、モジュールに共通する処理を自身で書く必要がなくなります。

このようなライブラリを利用する場合のモジュール基本構造が、本家のDeveloping Modulesというドキュメントで紹介されています。

from ansible.module_utils.basic import AnsibleModule

def main():
    module = AnsibleModule(
        argument_spec=dict(
            state=dict(default='present', choices=['present', 'absent']),
            name=dict(required=True),
            enabled=dict(required=True, type='bool'),
            something=dict(aliases=['whatever'])
        )
    )

if __name__ == '__main__':
    main()

お作法にしたがってモジュールを書いてみる

それでは、上記の基本構造を利用して、さきほどのtouchtest.pyを書き直してみましょう。ただ書き直すだけではなく、Ansibleのモジュールらしく、いくつか仕様を追加しています。

  • stateパラメータがpresentの場合
    • nameパラメータに指定したファイル名で空ファイルを作成する(changed: True)
    • nameパラメータに指定したファイル名と同名のファイルが存在していたら何もしない(changed: False)
  • stateパラメータがabsentの場合
    • nameパラメータに指定したファイルが存在していれば削除する(changed: True)
    • nameパラメータに指定したファイルだ存在しなければ何もしない(changed: False)

この仕様にしたがって、書き直したサンプルモジュール(myfile.py)は、以下の通りです。

#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
Description:
  Sample module for checking how to change your system.
"""

import os

from ansible.module_utils.basic import AnsibleModule


def main():
    module = AnsibleModule(
        argument_spec=dict(
            state=dict(default='present', choices=['present', 'absent']),
            name=dict(required=True)
        )
    )

    state = module.params['state']
    filename = module.params['name']

    changed = False
    if state == 'absent':
        if os.path.exists(filename):
            changed = True
            try:
                os.unlink(filename)
            except OSError as err:
                module.fail_json(msg='{err}'.format(err=err))
        module.exit_json(changed=changed, filename=filename)

    if not os.path.exists(filename):
        changed = True
        try:
            target_fd = open(filename, 'w')
        except IOError as err:
            module.fail_json(msg='{err}'.format(err=err))
        target_fd.close()
    module.exit_json(changed=changed, filename=filename)


if __name__ == '__main__':
    main()

AnsibleModuleクラスのオブジェクトインスタンスを利用することで、このクラスが提供している、引数やJSON形式での戻り値のハンドリングを行ってくれるメソッドなどが利用できるようになります。

まずは、state==presentから。1回目の実行ではファイルが作成されるので、changed: Trueとなり、2回目の実行では、すでにファイルが存在していて新たに作成されることはありませんから、changed: Falseになっています。

% ansible localhost -i hosts -M . -m myfile.py -a "name=hello state=present"
localhost | SUCCESS => {
    "changed": true,
    "filename": "hello"
}
% ansible localhost -i hosts -M . -m myfile.py -a "name=hello state=present"
localhost | SUCCESS => {
    "changed": false,
    "filename": "hello"

続いてstate==absentで実行してみましょう。1回目の実行ではファイルが削除されてchanged: True、2回目の実行ではファイルが存在していませんから、changed: Falseになっています。

% ansible localhost -i hosts -M . -m myfile.py -a "name=hello state=absent"
localhost | SUCCESS => {
    "changed": true,
    "filename": "hello"
}
% ansible localhost -i hosts -M . -m myfile.py -a "name=hello state=absent"
localhost | SUCCESS => {
    "changed": false,
    "filename": "hello"
}

作成や削除に失敗した場合の処理についても、サンプルとして記述してあります。例えばディレクトリに対してstate=absentで削除処理を適用しようとすると、削除処理に失敗して、以下のようなエラーがfail_json()で戻されます。

% file hello
hello: directory
% ansible localhost -i hosts -M . -m myfile.py -a "name=hello state=absent"
localhost | FAILED! => {
    "changed": false,
    "failed": true,
    "msg": "[Errno 1] Operation not permitted: 'hello'"
}

AnsibleModuleクラスの機能(ほんの一部)

サンプルモジュール(myfile.py)では、ほんの一部しか利用していませんが、AnsibleModuleクラスは、モジュール開発を支援するために、さまざまな共通機能を提供してくれます。

以下では、myfile.pyで利用している機能のみを紹介します。

モジュールパラメータ処理: AnsibleModule().params
    module = AnsibleModule(
        argument_spec=dict(
            state=dict(default='present', choices=['present', 'absent']),
            name=dict(required=True)
        )
    )

AnsibleModuleクラスのオブジェクトインスタンス生成時に、argument_specにモジュールパラメータを渡すことで、argsファイルに記述されているモジュールのオプションパラメータを読み込んで辞書型の変数(params)に格納してくれます。

ここでは、statenameという2つのパラメータを取得するように指定していあす。辞書型で指定するオプションパラメータの要件には、以下のようなキーワードを指定できます。

  • aliased: 互換性を確保するのに必要なパラメータの別名を記述したリスト
  • choices: stateパラメータのように"present"や"absent"などのキーワード指定うためのキーワードリスト
  • default: 無指定だった場合のデフォルト値
  • required: 指定必須のパラメータかどうか(True or False)
  • type: int、bool、listなどのような渡されるパラメータ値の型

モジュール内では、以下のように渡されたオプションパラメータを利用することができます。

    state = module.params['state']
    filename = module.params['name']
正常終了: AnsibleModule.exit_json(changed, result)

モジュールが正常終了した場合に、JSON形式でメッセージを標準出力して終了します。

    module.exit_json(changed=changed, result=filename)

モジュールの動作結果として、実行環境に何らかの変更が加わった場合は、changed_にTrueを、変更がなかった場合はFalseを設定してやります。

resultには、必要に応じて利用者に知らせたい情報を設定します。

異常終了: AnsibleModule.fail_json(msg)

モジュールが異常終了した場合に、JSON形式でメッセージを標準出力して終了します。

    module.fail_json(msg='{err}'.format(err=err))

異常終了時に利用者に知らせたい情報を、msgに設定してやります。

冪等性問題

Ansibleは、冪等性が担保されていると良く言われていますが、これもモジュールの作り次第です。

冪等性を実現するために、モジュールの作成者は、AnsibleModuleクラスが提供している共通機能を上手に活用して、冪等性の担保するコードを実装してやる必要があります。

参考情報

OpenStackとAnsible Towerでメリークリスマス

OpenStackとAnsible Towerでメリークリスマス

みなさんメリークリスマス。 この記事は、OpenStack Advent Calendar 2016の25日目の記事です。

OpenStack Advent Calendar 2016 最終日の今日は、OpenStack管理下にインスタンスを作成する際に、Ansible Towerを利用してPlaybookを適用する方法を紹介します。

OpenStackとAnsibleとAnsible Tower

OpenStack

OpenStackは、言わずと知れたクラウド基盤管理システムです。オープンな開発コミュニティ、そして全ての機能を外部から管理可能な、オープンなAPIを持っています。

このAPIを利用することで、従来のオンプレミスな仮想化基盤では難しかった、CI/CDに代表されるツールチェーンのような仕組みをインフラ分野にも適用することが可能となます。いわゆるInfrastructure as Codeを実現するのに、OpenStackが一役買ってくれるという訳ですね。

Ansible

Ansibleは、現代を代表するITオートメーションツールです。コア部分の実体は、単なるコマンドラインツールなのですが、この単純でシンプルな構造が非常に使い勝手が良く、運用の現場での利用が急速に進んでいます。

世の中では利点ばかりが強調されがちですが、シンプルであるがゆえに、Ansibleのコア部分が削ぎ落とした機能がいくつかあります。 実際に運用していると、サーバのオートスケール時の自動構築や、ツールチェーンに組み込みたい場合、そしてシステム監査のような場面で、以下の3点は、なにか対策があるといいのになぁと思ってりしています。

  1. 外部APIを持っていない
  2. コマンドの実行権限管理はOS任せ
  3. Playbookやログなどが分散してしまう

それぞれ、いろいろと考えて対処しているのですが、これを公式に解決してくれるのがAnsible Towerです。

Ansible Tower

Ansible Towerは、Playbookによるジョブ管理システムです。具体的には以下の機能を提供してくれます。現時点では有償ですが、10ノードまで管理できる無償のトライアル版が提供されいますので、今回は、このトライアル版を利用してAnsible TowerとOpenStackの連携方法についてご紹介します。

  1. コールバック機能も含めた外部API機能
  2. Gitなどのソフトウェア構成管理システム上に置いたPlaybookを利用したジョブ管理機能
  3. ジョブ実行ログの集中管理機能
  4. ユーザ・グループレベルでの実行権限管理

インストール手順は、特に難しということはありません。公式ドキュメントや、@ITコチラの記事を参照してみてください。

Tower起動させると、こんな感じのリッチなWebダッシュボードから操作できるようになります。

f:id:pyde:20161225211100p:plain

紹介する構成

testserverをデプロイする際に、Ansible Towerの管理下にあるPlaybookをジョブとして実行する方法を紹介します。

                             172.16.165.0/24
+------+--------------+---------------------+
       |.130          |.66
       |              |
       |              |
 +-----+----+   +-----+-----+
 |  router  |   | OpenStack |
 +-----+----+   +-----------+
       |
       |
       |.1                    192.168.0.0/24
+-----++---------------+--------------------+
      |.15             |
      |                |
      |                |
 +----+----+    +------+------+
 |  tower  |    | testserver  |
 +---------+    +-------------+

ジョブとして実行するPlaybook

playbookは、以下の構成でGitHubに置いて、これをAnsible Towerから利用します。

playbook
├── site.yml
└── web
    ├── files
    │   ├── index.html
    │   └── josug2016.png
    └── tasks
         └── main.yml

playbookで実行するタスク(playbook/web/tasks/main.yml)は、以下の通りです。パッケージ更新後にnginxをインストールし、コンテンツを配置した後で、最後にnginxをリスタートしています。

---
# tasks file for web

- block:
  - name: update all packages
    apt:
      upgrade: yes
      update_cache: yes
  - name: install nginx
    apt:
      name: nginx
      update_cache: no
  - name: copy contents
    copy:
      src: "{{ item }}"
      dest: "/usr/share/nginx/html/{{ item }}"
    with_items:
      - index.html
      - josug2016.png
  - name: restart nginx
    service:
      name: nginx
      state: restarted
  become: yes
  become_user: root

Ansible TowerでのOpenStack連携設定

それでは、Ansible Towerを操作して、実際にOpenStackとの連携をみてみましょう。

Ansible TowerからOpenStack環境を利用するための設定項目は4つあります。

  1. プロジェクトの作成
  2. Credentialの作成
  3. Dynamic Inventoryを利用するInventory Groupの作成
  4. Playbookを実行するJob Templateの作成

1. プロジェクトの作成

今回のAdvent Calendar用に1つプロジェクトを作成します。

SCM URLには、GitHub上に配置してあるPlaybookのパスを指定しています。

f:id:pyde:20161225211052p:plain

2. Credentialの作成

利用するCredentialは2つあります。

1つは、Dynamic InventoryからOpenStackのAPIを利用するためのopenstackと、

f:id:pyde:20161225211102p:plain

そしてもう1つは、起動したインスタンスにログインしてPlaybookを適用するためのubuntuです。

f:id:pyde:20161225211104p:plain

3. Inventory Groupの作成

OpenStack環境で管理されている仮想マシン群を、Ansibleから操作するためには Dynamic Inventory を利用するのが常套手段です。当然ながらAnsible Towerからも Dynamic Inventory が利用できます。

ここでは、すでに作成済みのCredential(openstack)を利用して、Dynamic Inventory(webservice)を作成しています。

4. Job Templateの作成

ubuntuとopenstackという2つのCredentialとInventoryを指定して、Job Templateを作成します。更に、管理対象ホストからジョブ起動させるトリガーを引けるように、Callbackを許可します。

f:id:pyde:20161225211057p:plain

このCallback URLに、操作対象ホストからcurlなどでアクセスすることにより、ジョブをキックすることができます。

curl --data "host_config_key=設定キー" https://192.168.0.241:443/api/v1/job_templates/15/callback/ --insecure

OpenStack管理下に仮想マシンインスタンスを起動

openstackコマンドで、インスタンスを起動させる際に、user-dataとして実行するスクリプト(userdata.sh)は、以下の通りです。

#!/bin/bash -v

curl --data "host_config_key=08e4d419bebc422a2bfacbd4bfd469fb" https://192.168.0.15:443/api/v1/job_templates/15/callback/ --insecure

#
# [EOF]
#

実際に、このuser-dataを利用してインスタンスを起動します。

openstack --os-cloud=josug server create \
--flavor m1.small \
--image trusty-server-cloudimg-amd64 \
--key-name ぼくの鍵 \
--nic net-id=ぼくのネットワークID \
--security-group ぼくのSecGroup \
--user-data ./userdata.sh \
testserver

起動時のTowerとの連携は以下の通りです。

  1. boot => OpenStack APIを利用して、Computeノード上にインスタンス(testserver)を起動する
  2. callback => 起動プロセスの最後に実行されるスクリプト(userdata.sh)からAnsible TowerのCallback URLにアクセスしてジョブをキックする
  3. playbook => Ansible Towerにジョブとして登録されているPlaybookを実行する
client               OpenStack               Tower             testserver(compute)
  +                      +                     +                      +
  |      1.boot          |                     |                      |
  +---------------------->                     |                      |
  |                      |                     |                      |
  |                      |                     |                      |
  |                      |                     |        1.boot        |
  |                      +-------------------------------------------->
  |                      |                     |                      |
  |                      |                     |                      |
  |                      |                     |       2.callback     |
  |                      |                     <----------------------+
  |                      |                     |                      |
  |                      |                     |                      |
  |                      |                     |       3.playbook     |
  |                      |                     +---------------------->
  |                      |                     |                      |
  |                      |                     |                      |
  +                      +                     +                      +

さらに進んだ利用方法

今回は、openstackコマンドのuser-dataオプションから、Callback URLキックするスクリプトを利用しましたが、こちらの記事のように、Heatのテンプレートからも同様にuser-dataに指定することで、操作対象ホストからCallbackでPlaybookをキックできます。オートスケール時の初期設定にも利用できて、さらに便利に使えますね。

クリスマスプレゼント間に合わず

Ansible Towerは、現在のところオープンソースではありません。しかし、Ansible WorksをRed Hatが買収されて以降、Red Hatのポリシーに従って、オープンソース化に向けた努力を続けているそうです。

きっと来年のクリスマスまでには、Ansible Towerがオープンソースソフトウェアとして広く利用できるに違いない!と期待を込めて、このカレンダー記事を書きました。

それでは皆さん、メリークリスマス:-)

cryptographyをpipインストールする際の fatal error: 'openssl/opensslv.h' file not found 対策メモ

OSX環境(10.11.6 Beta(15G12a))で、cryptographyをpipでインストールしようとした際に遭遇したエラーに対する対策メモ。

いまおもえばpastebinにでも貼っておいたほうがよかった。

問題

brewでインストールしたOpenSSLのヘッダファイル(opensslv.h)を見つけてくれず、コンパイル時にエラーとなる。

% pip install cryptography
Collecting cryptography
  Using cached cryptography-1.4.tar.gz
<...>
  building '_openssl' extension
  creating build/temp.macosx-10.11-intel-2.7/build
  creating build/temp.macosx-10.11-intel-2.7/build/temp.macosx-10.11-intel-2.7
  cc -fno-strict-aliasing -fno-common -dynamic -arch i386 -arch x86_64 -g -Os -pipe -fno-common -fno-strict-aliasing -fwrapv -DENABLE_DTRACE -DMACOSX -DNDEBUG -Wall -Wstrict-prototypes -Wshorten-64-to-32 -DNDEBUG -g -fwrapv -Os -Wall -Wstrict-prototypes -DENABLE_DTRACE -arch i386 -arch x86_64 -pipe -I/System/Library/Frameworks/Python.framework/Versions/2.7/include/python2.7 -c build/temp.macosx-10.11-intel-2.7/_openssl.c -o build/temp.macosx-10.11-intel-2.7/build/temp.macosx-10.11-intel-2.7/_openssl.o
  build/temp.macosx-10.11-intel-2.7/_openssl.c:423:10: fatal error: 'openssl/opensslv.h' file not found
  #include <openssl/opensslv.h>
           ^
  1 error generated.
  error: command 'cc' failed with exit status 1

  ----------------------------------------
  Failed building wheel for cryptography
Failed to build cryptography
Installing collected packages: cryptography
  Running setup.py install for cryptography
    Complete output from command /Users/saitou/venv/2.7.10/ansible/bin/python -c "import setuptools, tokenize;__file__='/private/var/folders/kk/ftd09wdd0sz27nbm8zwzjsdh0000gn/T/pip-build-u8oOQQ/cryptography/setup.py';exec(compile(getattr(tokenize, 'open', open)(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --record /var/folders/kk/ftd09wdd0sz27nbm8zwzjsdh0000gn/T/pip-d3jDDM-record/install-record.txt --single-version-externally-managed --compile --install-headers /Users/saitou/venv/2.7.10/ansible/include/site/python2.7/cryptography:

<...>
    building '_openssl' extension
    cc -fno-strict-aliasing -fno-common -dynamic -arch i386 -arch x86_64 -g -Os -pipe -fno-common -fno-strict-aliasing -fwrapv -DENABLE_DTRACE -DMACOSX -DNDEBUG -Wall -Wstrict-prototypes -Wshorten-64-to-32 -DNDEBUG -g -fwrapv -Os -Wall -Wstrict-prototypes -DENABLE_DTRACE -arch i386 -arch x86_64 -pipe -I/System/Library/Frameworks/Python.framework/Versions/2.7/include/python2.7 -c build/temp.macosx-10.11-intel-2.7/_openssl.c -o build/temp.macosx-10.11-intel-2.7/build/temp.macosx-10.11-intel-2.7/_openssl.o
    build/temp.macosx-10.11-intel-2.7/_openssl.c:423:10: fatal error: 'openssl/opensslv.h' file not found
    #include <openssl/opensslv.h>
             ^
    1 error generated.
    error: command 'cc' failed with exit status 1

    ----------------------------------------

<...>

対策

本家のHomebrewの項に書いてあった...

% brew install openssl
% env CRYPTOGRAPHY_OSX_NO_LINK_FLAGS=1 LDFLAGS="$(brew --prefix openssl)/lib/libssl.a $(brew --prefix openssl)/lib/libcrypto.a" CFLAGS="-I$(brew --prefix openssl)/include" pip install cryptography

恐らく一週間後には覚えてなさそうなのでメモしておく。

きょうのdevstack用local.conf - 2016.5.13時点でのmaster向け

Environment

VMware FusionのGuestOSにUbuntu16.04をインストールし、その上にdevstackでOpenStack開発環境を構築する。

  • GuestOS: ubuntu 16.04lts
  • VMware Fusion 8.1.1
  • SPEC: vCPU x 2 / MEM 8GB / HDD 100GB / Network VMNET12,13

注意)GuestOSのメモリは少なくとも4GBは確保しておく。それ以下のサイズだとOOM Killerが発動し、mysqldあたりからkillされていく...

インストールされるコンポーネント

コアコンポーネント

  • cinder
  • glance
  • keystone
  • neutron
  • nova
  • swift

その他のBigTentコンポーネント

  • barbican
  • ceilometer
  • designate
  • heat
  • magnum
  • murano

local.conf

  • 管理用NIC: ens33
  • サービス用NIC: ens34
  • PASSWORD,VERSION,PUBLIC_INTERFACEは環境に応じて変更する
  • 2回目以降はOFFLINE=True,RECLONE=Falseで実行すれば時間短縮!
[[local|localrc]]
#OFFLINE=True
RECLONE=True

HOST_IP=10.0.12.3
SERVICE_HOST=$HOST_IP
MYSQL_HOST=$HOST_IP
RABBIT_HOST=$HOST_IP
GLANCE_HOSTPORT=$HOST_IP:9292

PASSWORD=changeme
ADMIN_PASSWORD=$PASSWORD
DATABASE_PASSWORD=$PASSWORD
RABBIT_PASSWORD=$PASSWORD
SERVICE_PASSWORD=$PASSWORD
SWIFT_HASH=JRBj7ukxMG4tckek

VERSION=master
CEILOMETER_BRANCH=$VERSION
CINDER_BRANCH=$VERSION
GLANCE_BRANCH=$VERSION
KEYSTONE_BRANCH=$VERSION
HEAT_BRANCH=$VERSION
HEAT_CFNTOOLS_BRANCH=$VERSION
HEAT_TEMPLATES_BRANCH=$VERSION
HORIZON_BRANCH=$VERSION
NEUTRON_BRANCH=$VERSION
NOVA_BRANCH=$VERSION
SWIFT_BRANCH=$VERSION

# Log settings
LOGDIR=$DEST/logs
SCREEN_LOGDIR=$LOGDIR
SCREEN_HARDSTATUS="%{= rw} %H %{= wk} %L=%-w%{= bw}%30L> %n%f %t*%{= wk}%+Lw%-17< %-=%{= gk} %y/%m/%d %c"
LOGFILE=$LOGDIR/devstack.log
LOGDAYS=1
LOG_COLOR=False

# Neutron settings
disable_service n-net
ENABLED_SERVICES+=,q-svc,q-dhcp,q-meta,q-agt,q-l3,q-lbaasv2,q-fwaas,q-vpn,q-qos,q-flavors,octavia,o-cw,o-hk,o-hm,o-api
# Ceilometer settings
ENABLED_SERVICES+=,ceilometer-acompute,ceilometer-acentral,ceilometer-collector,ceilometer-api
# Swift settings
ENABLED_SERVICES+=,s-proxy,s-object,s-container,s-account
# Heat settings
ENABLED_SERVICES+=,heat,h-api,h-api-cfn,h-api-cw,h-eng
# Ceilometer settings
enable_plugin ceilometer https://git.openstack.org/openstack/ceilometer
# Magnum settings
enable_plugin magnum https://git.openstack.org/openstack/magnum $VERSION
enable_plugin magnum-ui https://git.openstack.org/openstack/magnum-ui $VERSION
# Barbican settings
enable_plugin barbican https://git.openstack.org/openstack/barbican $VERSION
# Designate settings
enable_plugin designate https://git.openstack.org/openstack/designate $VERSION
ENABLED_SERVICES+=,designate,designate-central,designate-api,designate-pool-manager,designate-zone-manager,designate-mdns
# Murano settings
MURANO_APPS=io.murano.apps.apache.Tomcat,io.murano.apps.Guacamole
enable_plugin murano https://git.openstack.org/openstack/murano $VERSION
ENABLED_SERVICES+=,murano-cfapi,g-glare

## Neutron options
Q_USE_SECGROUP=True
FLOATING_RANGE="10.0.13.0/24"
FIXED_RANGE="192.168.0.0/24"
NETWORK_GATEWAY="192.168.0.1"
Q_FLOATING_ALLOCATION_POOL=start=10.0.13.129,end=10.0.13.254
PUBLIC_NETWORK_GATEWAY="10.0.13.2"
Q_L3_ENABLED=True
PUBLIC_INTERFACE=ens34

# Open vSwitch provider networking configuration
Q_USE_PROVIDERNET_FOR_PUBLIC=True
OVS_PHYSICAL_BRIDGE=br-ex
PUBLIC_BRIDGE=br-ex
OVS_BRIDGE_MAPPINGS=public:br-ex

#
# [EOF]
#

OpenStackとAnsibleと私

OpenStack Advent Calendar 2015とAnsible Advent Calendar 2015の12月25日の記事です。

沖縄でハンズオンをやってきました

12/14-18で開催されたOkinawa Open Days 2015で、OpenStackとAnsibleとDockerを組み合わせたハンズオンセミナーをやってきました。

OpenStackとAnsibleのAdvent Calendarの最終日としてふさわしいかどうかわかりませんが、ハンズオンに向けての準備で得られた知見を、ここにメモしておきたいと思います。

"OpenStack"と"Ansible v2"

Ansibleは標準でOpenStackを操作するためのモジュールを持っています。 このモジュールを利用すれば、OpenStackが提供するAPIを利用して、仮想マシンインスタンス、ネットワーク、ボリュームなどの仮想リソースの管理をAnsibleのPlaybookを利用して実施することができます。 また、OpenStack管理下にある仮想マシンインスタンス群の管理を容易にするために、ダイナミックインベントリも提供しています。 このOpenStack向けに提供される標準モジュールとダイナミックインベントリは、v2で大きく進化しており、v1向けのモジュールよりも簡単便利に使えるようになりました。

Ansible v2向けモジュールの進化

v2向けのOpenStack用モジュールは、github上にある公式リポジトリから入手可能です。

従来のv1向けのモジュールやダイナミックインベントリは、OpenStackの各コンポーネントが提供するクライアントライブラリ(python-novaclientやpython-neutronclientなど)を直接利用していましたが、v2向けのモジュールは、shadeライブラリを利用するよう全面的に刷新されました。

v1向けOpenStack用モジュールの特徴

例えば、nova-computeモジュールでは、python-novaclientを直接利用していました。 OpenStackの各コンポーネントが提供するクライアントモジュール(pyhon-*client)を直接利用しており、OpenStack環境へのログイン情報をパラメータとして指定する必要がありました。 さらに、OpenStackのクライアントモジュールはコンポーネント毎に認証方式が微妙に違っており、この差異はAnsibleのモジュール側の実装で吸収していました。

例) nova_computeモジュール

+--------------+    +-------------------+         +-----------+
| nova_compute +----+ python-novaclient +==(API)=>+ openstack |
+--------------+    +-------------------+         +-----------+

例) quantum_networkモジュール

+-----------------+    +----------------------+         +-----------+
| neutron_network +----+ python-neutronclient +==(API)=>+ openstack |
+-----------------+    +----------------------+         +-----------+

v2向けOpenStack用モジュールで改善された点

v2向けのモジュールでは、shadeライブラリを利用しています。shadeはOpenStackクライアントライブラリ(python-*client)の上に一枚被さる形で各クライアントライブラリの認証部分など共通に利用する部分の差異を吸収して抽象化したレイヤーを、上位のプログラムに対して提供してくれます。 さらに、v1時代のモジュールのように、OpenStackへの接続情報をPlaybookに書く必要はなく、外部の設定ファイルに複数のOpenStack環境の認証情報を記述して実行時に選択することで、接続先を切り替えるも可能です。

+--------------+    +-------+    +----------------+         +-----------+
| os_* modules +----+ shade +----+ python-*client +==(API)=>+ openstack |
+--------------+    +---+---+    +----------------+         +-----------+
                        |
               +--------+---------+    +-------------------------------------+
               | os-client-config +----+ $HOME/.config/openstack/clouds.yaml |
               +------------------+    +-------------------------------------+

shadeが利用しているos-client-configについては、OSS とクラウドの狭間で: 環境変数1つで OpenStack 環境を切り替える os-client-config で、もときさんが詳しく紹介してくれています。必読です。

v1向けモジュールのClientクラスインスタンスの初期化

nova_computeモジュールと、quantum_networkモジュールの例を以下に紹介します。ログイン情報をモジュールパラメータとして指定する必要があります。 さらにpython-novaclientとpython-neutronclientのClientクラスインスタンスは、生成する際の引数が微妙に違います。

nova_compute.py

...
 41: options:
 42:    login_username:
 43:     description:
 44:        - login username to authenticate to keystone
 45:     required: true
 45:     default: admin
 46:   login_password:
 47:     description:
 48:        - Password of login user
 49:     required: true
 50:     default: 'yes'
 51:   login_tenant_name:
 52:     description:
 53:        - The tenant name of the login user
 54:     required: true
 55:     default: 'yes'
...
534: def main():
...
572:     nova = nova_client.Client(module.params['login_username'],
573:                               module.params['login_password'],
574:                               module.params['login_tenant_name'],
575:                               module.params['auth_url'],
576:                               region_name=module.params['region_name'],
577:                               service_type='compute')
...

quantum_network.py

...
 37: options:
 38:   login_username:
 39:     description:
 40:        - login username to authenticate to keystone
 41:     required: true
 42:     default: admin
 43:   login_password:
 44:     description:
 45:        - Password of login user
 46:     required: true
 47:     default: 'yes'
 48:   login_tenant_name:
 49:     description:
 50:        - The tenant name of the login user
 51:     required: true
 52:     default: 'yes'
...
130: def _get_ksclient(module, kwargs):
131:    try:
132:         kclient = ksclient.Client(username=kwargs.get('login_username'),
133:                                  password=kwargs.get('login_password'),
134:                                  tenant_name=kwargs.   get('login_tenant_name'),
135:                                  auth_url=kwargs.get('auth_url'))
...

v2向けモジュールのClientクラスインスタンスの初期化

v2向けモジュールでは、認証情報をパラメータとして直接モジュールに渡す必要はありません。shadeがos-client-configを利用して設定ファイルを取得して利用してくれるからです。 ちなみに、何もしなくてもopenrcなどによって反映された環境変数(OS_USERNAMEなど)を接続情報として利用してくれます。なにこれ便利すぎる!

v2向けのOpenStack操作用モジュール(os_*モジュール)は、認証情報をパラメータとしてセットする必要がありません。さらに、内部的にはnovaを操作する場合(例:os_nova_flavorモジュール)も、neutronを操作する場合(例:os_portモジュール)も同一の方法で利用することができます。

os_nova_flavor.py

...
201:         cloud = shade.operator_cloud(**module.params)
...

os_port.py

...
336:         cloud = shade.openstack_cloud(**module.params)
...

設定ファイル($HOME/.config/openstack/clouds.yaml)

以下の例では、libertyとdevstackの2つの環境を定義しており、os_*モジュールのcloudパラメータに環境名(ここではlibertyまたはdevstack)を指定することで、接続先となるOpenStack環境を切り替えることが可能です。 また、cloudパラメータに何もしていしなかった場合は、openrcなどで反映されている環境変数(OS_USERNAMEなど)が利用されます。

clouds:
  liberty:
    auth:
      auth_url: http://192.168.0.1:5000
      username: saitou
      password: changeme
      project_domain_id: default
      user_domain_id: default
      project_name: SYSTEM00
    identity_api_version: '3'
  devstack:
    auth:
      auth_url: http://172.16.31.1:35357
      username: admin
      password: changeme
      project_domain_id: default
      user_domain_id: default
      project_name: admin
    identity_api_version: '3'
    region_name: RegionOne

利用方法

この例では、os_*モジュールにはcloudパラメータを指定していません。 この状態だと、ansible-playbookコマンドの実行環境で反映されている環境変数(OS_USERNAMEなど)が接続情報として利用されます。

  1. 公開鍵を登録する(os_keypairモジュール)
  2. セキュリティグループを作成する(os_security_groupモジュール)
  3. セキュリティグループにルールを登録する(os_security_group_ruleモジュール)
  4. 仮想マシンインスタンスを起動する(os_serverモジュール)
  5. 仮想マシンインスタンスにフローティングIPアドレスを割り当てる(quantum_floating_ipモジュール)

比較のために利用しているquantum_floating_ipモジュールはv1向けのモジュールです。v1向けのモジュールは、実行時にOpenStack環境への接続情報がパラメータとして必要となるため、わざわざ環境変数から取得するようvarsセクションでlookup()しています。

Playbook: create_instance.yml
---
- hosts: localhost

  vars:
    ansible_python_interpreter: /home/centos/handson/bin/python
    os_auth_url: "{{ lookup('env','OS_AUTH_URL') }}"
    os_username: "{{ lookup('env','OS_USERNAME') }}"
    os_password: "{{ lookup('env','OS_PASSWORD') }}"
    os_project_name: "{{ lookup('env','OS_TENANT_NAME') }}"
    os_region_name: "{{ lookup('env','OS_REGION_NAME') }}"
    keypairs:
      - name: "step-server"
        public_key_file: "/home/centos/.ssh/id_rsa.pub"
    secgroups:
      - name: "eplite"
        desc: "secgroup for eplite"
        rules:
          - protocol: "icmp"
            port_range_min: -1
            port_range_max: -1
            remote_ip_prefix: "0.0.0.0/0"
          - protocol: "tcp"
            port_range_min: 22
            port_range_max: 22
            remote_ip_prefix: "0.0.0.0/0"
          - protocol: "tcp"
            port_range_min: 80
            port_range_max: 80
            remote_ip_prefix: "0.0.0.0/0"
          - protocol: "tcp"
            port_range_min: 3306
            port_range_max: 3306
            remote_ip_prefix: "0.0.0.0/0"
    servers:
      - name: "{{ hostname }}"
        key_name: "step-server"
        flavor: "m1.small"
        image: "Docker01"
        secgroups:
          - "eplite"
        nics:
          - net-name: "work-net"
        auto_ip: no
        ext_net: "ext-net_1214"
        int_net: "work-net"

  tasks:
    - name: import keypairs
      os_keypair:
        state=present
        name="{{ item.name }}"
        public_key_file="{{ item.public_key_file }}"
      with_items: keypairs
    - name: create security group
      os_security_group:
        state=present
        name="{{ item.name }}"
        description="{{ item.desc}}"
      with_items: secgroups
    - name: add rules to secgroup
      os_security_group_rule:
        state=present
        security_group="{{ item[0].name }}"
        protocol="{{ item[1].protocol }}"
        port_range_min="{{ item[1].port_range_min }}"
        port_range_max="{{ item[1].port_range_max }}"
        remote_ip_prefix="{{ item[1].remote_ip_prefix }}"
      with_subelements:
        - secgroups
        - rules
    - name: create servers
      os_server:
        state: present
        timeout: 200
        name: "{{ item.name }}"
        key_name: "{{ item.key_name }}"
        flavor: "{{ item.flavor }}"
        image: "{{ item.image }}"
        security_groups: "{{ item.secgroups }}"
        nics: "{{ item.nics }}"
        auto_ip: "{{ item.auto_ip }}"
      with_items: servers
    - name: create and assign floating_ip to server
      quantum_floating_ip:
        state=present
        login_username="{{ os_username }}"
        login_password="{{ os_password }}"
        login_tenant_name="{{ os_project_name }}"
        network_name="{{ item.ext_net }}"
        instance_name="{{ item.name }}"
        internal_network_name="{{ item.int_net }}"
      with_items: servers
Playbookを実行してみる

OpenStackモジュールを利用する前に、まずはshadeのインストールをしなければなりません。ここではpipを利用してvirtualenv環境にインストールしていますが、みなさんの環境にあわせてインストールしておいてください。

$ cd ~ && virtualenv develop
(develop)$ pip install shade functools32

-eオプションで指定したホスト名で、仮想マシンインスタンスを作成します。

$ source openrc
$ ansible-playbook -i ansible_hosts -e "hostname=web00" create_instance.yml

OpenStack管理機能を提供するv2向けモジュール

仮想マシンインスタンスを起動するという目的を達成するために、必要最低限の機能を提供していたv1向けモジュールに対して、v2向けのモジュールは大幅に機能追加されています。 これらv2向けモジュールのほとんど(すべて検証したわけではないけれど)は、Ansible v1.9.4でも正常動作するため、みなさんのv1環境でも恩恵を受けることができます。

是非試してみてください。

v1向けOpenStack用モジュールリスト

  • glance_image.py
  • keystone_user.py
  • nova_compute.py
  • nova_keypair.py
  • quantum_floating_ip_associate.py
  • quantum_floating_ip.py
  • quantum_network.py
  • quantum_router_gateway.py
  • quantum_router_interface.py
  • quantum_router.py
  • quantum_subnet.py

v2向けOpenStack用モジュールリスト

  • os_auth.py
  • os_client_config.py
  • os_floating_ip.py
  • os_image_facts.py
  • os_image.py
  • os_ironic_node.py
  • os_ironic.py
  • os_keypair.py
  • os_network.py
  • os_networks_facts.py
  • os_nova_flavor.py
  • os_object.py
  • os_port.py
  • os_router.py
  • os_security_group.py
  • os_security_group_rule.py
  • os_server_actions.py
  • os_server_facts.py
  • os_server.py
  • os_server_volume.py
  • os_subnet.py
  • os_subnets_facts.py
  • os_user_group.py
  • os_user.py
  • os_volume.py

刷新されたダイナミックインベントリプログラム

Ansibleのソースコードには、ダイナミックインベントリのサンプルプログラムが含まれています。v2のソースコードにも、もちろんOpenStack環境用のダイナミックインベントリプログラムが含まれているのですが、今回はこのプログラムも全面刷新され、shadeを利用するようになりました。

v1時代のOpenStack環境用ダイナミックインベントリプログラムは、あくまでもサンプル的な内容でしたが、v2は違います。一度取得した情報を一定時間キャッシュして再利用することで、問い合わせ回数を削減するなどの工夫がされており、そのまま特に手を加えることなく利用できるレベルの仕上がりで、足りない機能を自身で追加するにしても、コードがシンプルで改修もしやすくなっています。

実際に利用してみる

まずは、上記サイトからダイナミックインベントリプログラムと設定ファイルを入手します。

※以降、すでにshadeはインストール済みであるものと仮定します。

テスト環境

ここでは以下のような環境を例に、ダイナミックインベントリプログラムの利用例を紹介します。

  • OpenStackコントローラ: 10.0.0.2
  • ユーザ: saitou
  • パスワード: changeme
  • ドメイン: default
  • プロジェクト名: OOD2015
  • 環境名: liberty

設定ファイルの取得と編集

(develop)$ sudo mkdir /etc/ansible/ (develop)$ cd /etc/ansible (develop)$ sudo wget https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/openstack.yml

/etc/ansible/openstack.yml
clouds:
  liberty:
    auth:
      auth_url: http://10.0.0.2:5000
      username: saitou
      password: changeme
      project_domain_id: default
      user_domain_id: default
      project_name: OOD2015
    identity_api_version: '3'

ダイナミックインベントリプログラムの取得とテスト実行

公式サイトからダイナミックインベントリプログラムを取得して、実行権を付与しておきます。

(develop)$ cd ~
(develop)$ wget https://raw.githubusercontent.com/ansible/ansible/devel/contrib /inventory/openstack.py
(develop)$ chmod +x openstack.py

早速テストしてみます。

(develop)$ ./openstack.py --list

実行結果は以下の通り。ちゃんと _meta も使ってくれています。素敵!

{
  "": [
    "9da2a418-61bd-4118-8eac-1e3d4d639c73", 
    "9da2a418-61bd-4118-8eac-1e3d4d639c73", 
    "ea5bd227-3b9c-4a43-80ea-8feb34eb6eb5", 
    "ea5bd227-3b9c-4a43-80ea-8feb34eb6eb5", 
    "f40e9f60-2f9c-4405-99dc-50f37b988ef6", 
    "f40e9f60-2f9c-4405-99dc-50f37b988ef6"
  ], 
  "_meta": {
    "hostvars": {
      "9da2a418-61bd-4118-8eac-1e3d4d639c73": {
        "ansible_ssh_host": "10.0.1.143", 
        "openstack": {
          "HUMAN_ID": true, 
          "NAME_ATTR": "name", 
          "OS-DCF:diskConfig": "AUTO", 
          "OS-EXT-AZ:availability_zone": "nova", 
          "OS-EXT-STS:power_state": 4, 
          "OS-EXT-STS:task_state": null, 
          "OS-EXT-STS:vm_state": "stopped", 
          "OS-SRV-USG:launched_at": "2015-12-06T07:17:22.000000", 
          "OS-SRV-USG:terminated_at": null, 
          "accessIPv4": "10.0.1.143", 
          "accessIPv6": ""
...以下略...

これをansibleから利用してみます。まずはallで全インスタンスpingモジュールを適用してみる。

(develop)$ ansible all -i ./openstack.py -m ping -u centos
ea5bd227-3b9c-4a43-80ea-8feb34eb6eb5 | success >> {
    "changed": false,
    "ping": "pong"
}

9da2a418-61bd-4118-8eac-1e3d4d639c73 | success >> {
    "changed": false,
    "ping": "pong"
}

f40e9f60-2f9c-4405-99dc-50f37b988ef6 | success >> {
    "changed": false,
    "ping": "pong"
}

Ansibleのダイナミックインベントリは、ホスト名やフレーバなどの情報を元に自動でグループを作ってくれるようなので、これもテストしてみます。

(develop)$ ansible step -i ./openstack.py -m ping -u centos
9da2a418-61bd-4118-8eac-1e3d4d639c73 | success >> {
    "changed": false,
    "ping": "pong"
}

(develop)$ ansible docker7 -i ./openstack.py -m ping -u centos
ea5bd227-3b9c-4a43-80ea-8feb34eb6eb5 | success >> {
    "changed": false,
    "ping": "pong"
}

ホスト名でのグループ指定もできてる!便利! 実はshadeはAnsibleのダイナミックインベントリを処理するため(としか思えない)に利用できるOpenStackInventoryクラスを持っています。 Ansible v2に含まれるダイナミックインベントリは、このクラスを利用しているため、v1用と比較して、コード自体が非常に簡潔になっておりカスタマイズもしやすくなっていますので、カスタマイズベースとしても使う価値ありです!

まとめ

Ansible v2では、OpenStack環境との親和性が飛躍的に高まりました。 OpenStackモジュールは刷新されており、shadeのおかげでシンプルでわかりやすい構造となっています。 OpenStack環境でAnsibleを利用しているひとは、切り替える価値が十分ありますので、是非試してみてください。

とは言っても

肝心のv2ってまだリリースされてないですよねぇ サンタはリリース情報をもって来てくれないかもしれないですが、みなさん素敵なクリスマスを:-)

OpenStack管理下にあるVMのNIC間でip_forwardを行う

構成

以下のような構成を想定して、vm0eth1eth2の間でルータのようにIPフォワードをさせたい場合に必要となる設定メモ。

ちょっと事情があってserverXDefaultGateway10.0.1.1serverYDefaultGateway10.0.2.1に向けて、相互に通信させたかったのです。

-----------+---------------------------------------------------------- ext-net
           |
           |
      +----+----+
      | router0 |
      +----+----+
           |
           |.129
-----------+------+--------------------------------------------------- demo-net0 (10.0.0.0/24)
                  |.135
                  |
------------------|-----+-------------------+------------------------- demo-net1 (10.0.1.0/24)
                  |     |.1                 |.100
                  |     |                   |
------------------|-----|-----+-------------|---------------+--------- demo-net2 (10.0.2.0/24)
                  |     |     |.1           |               |.101
                  |     |     |             |               |
              +---+-----+-----+---+   +-----+-----+   +-----+-----+
              |  eth0  eth1  eth2 |   |    eth0   |   |    eth0   |
              |                   |   |           |   |           |
              |        vm0        |   |  serverX  |   |  serverY  |
              |                   |   |           |   |           |
              +-------------------+   +-----------+   +-----------+

できない...

実は...デフォルトの設定ではできないんです。SecurityGroupでごにょごにょすれば行けるのかとも思いましたが、残念ならができません。

そこで、調べてみました。

nova boot仮想マシンを起動させると、computeノード上で起動しているneutronのagentが、computeノードのiptablesに以下のような設定を投入します。

Chain neutron-linuxbri-s7780e5de-2 (1 references)
num  target     prot opt source               destination
1    RETURN     all  --  10.0.1.1             anywhere             MAC FA:16:3E:69:AD:2E /* Allow traffic from defined IP/MAC pairs. */
2    DROP       all  --  anywhere             anywhere             /* Drop traffic without an IP/MAC allow rule. */

Chain neutron-linuxbri-sbcd24cc0-d (1 references)
num  target     prot opt source               destination
1    RETURN     all  --  10.0.2.1             anywhere             MAC FA:16:3E:92:84:F1 /* Allow traffic from defined IP/MAC pairs. */
2    DROP       all  --  anywhere             anywhere             /* Drop traffic without an IP/MAC allow rule. */

Neutronでは、IP Spoofing対策の1つとして、仮想マシン自身に割り当てられているIPアドレス以外は、FORWARD時に落とすという仕組みを、上記の設定を投入することで実現しているようです。

そして、このチェーンは、以下のようにChain FORWARDに登録されます。 ちなみに、チェーン名の末尾に付与されているIDは、それぞれのtapデバイス名に由来しているので、環境により異なります。

Chain FORWARD (policy ACCEPT)
  |
  +- neutron-filter-top
  |     |
  |     +- neutron-linuxbri-local
  |
  +- neutron-linuxbri-FORWARD
        |
        +- neutron-linuxbri-sg-chain
              |
              +- neutron-linuxbri-ibcd24cc0-d
              |
              +- neutron-linuxbri-obcd24cc0-d
              |     |
              |     +- neutron-linuxbri-sbcd24cc0-d   <-- これ
              |
              +- neutron-linuxbri-i7780e5de-2
              |
              +- neutron-linuxbri-o7780e5de-2
              |     |
              |     +- neutron-linuxbri-s7780e5de-2   <-- これ
              |
              +- neutron-linuxbri-i8a0e0062-b
              |
              +- neutron-linuxbri-o8a0e0062-b
                    |
                    +- neutron-linuxbri-s8a0e0062-b

考えた対策

computeノード上で...

  1. iptables -F を実行してポリシー全消し -> まあだめだよね
  2. neutron-linuxbri-s* のルールを削除する -> まぁだめだよね
  3. iptables -I FORWARD -m physdev --physdev-is-bridged -j ACCEPTを突っ込む -> まぁだめだよね
  4. sysctl -w net.bridge.bridge-nf-call-iptables=0 でブリッジにiptablesを見せない -> 本末転倒

どれも、まぁだめだよね...

手詰まり...かと思いきや、実はちゃんと設定できました。

この機能をNeutronは提供していた

IPフォワードしてもらうために向けられてくるネットワークアドレスレンジを、neutron-linuxbri-s*チェーンに追加してやる方法ないかな...いろいろ調べてみると、ネットワークレンジを--allowed-address-pairsでポートに追加してやれば良さそうという結論に至りました。

$ neutron port-update 7780e5de-222a-4805-accc-77a380732cf3 \
--allowed-address-pairs \
type=dict \
list=true \
ip_address=10.0.2.0/24

$ neutron port-update bcd24cc0-d1d9-4016-a512-f0b12075f1c0 \
--allowed-address-pairs \
type=dict \
list=true \
ip_address=10.0.1.0/24

これを実行すると、neutron-linuxbri-s*チェーンの設定に、以下のようなポリシーが追加となります。

Chain neutron-linuxbri-s7780e5de-2 (1 references)
num   pkts bytes target     prot opt in     out     source               destination
1      112  9408 RETURN     all  --  any    any     10.0.2.0/24          anywhere             MAC FA:16:3E:69:AD:2E /* Allow traffic from defined IP/MAC pairs.     */
2        0     0 RETURN     all  --  any    any     10.0.1.1             anywhere             MAC FA:16:3E:69:AD:2E /* Allow traffic from defined IP/MAC pairs.     */
3        0     0 DROP       all  --  any    any     anywhere             anywhere             /* Drop traffic without an IP/MAC allow rule. */

Chain neutron-linuxbri-sbcd24cc0-d (1 references)
num   pkts bytes target     prot opt in     out     source               destination
1      230 19320 RETURN     all  --  any    any     10.0.1.0/24          anywhere             MAC FA:16:3E:92:84:F1 /* Allow traffic from defined IP/MAC pairs.     */
2        0     0 RETURN     all  --  any    any     10.0.2.1             anywhere             MAC FA:16:3E:92:84:F1 /* Allow traffic from defined IP/MAC pairs.     */
3        0     0 DROP       all  --  any    any     anywhere             anywhere             /* Drop traffic without an IP/MAC allow rule. */

ということで、これでcomputeノード上のVMで、ip_fowardを有効化してあげれば、ちゃんとIPフォワードしてくれました。

この機能、Havanaから実装されてるのね。知らんかった。

Ansibleの Developing Modules を和訳してみた

ちょっと訳があって、Ansibleのモジュール開発方法を解説している Developing Modules を和訳してみた。だいたいあってると思うんだけど、意図を計りかねるところもあったので、想像しながら意訳してみた。それほど外れてないと思う。

Developing Modules

Ansibleのモジュールは再利用可能な機能単位で、ansibleコマンドやansible-playbookコマンド、そしてAnsibleのAPI経由で呼び出して利用することができます。

  • 参考情報: About Modules にモジュール開発に役立つ情報がリストアップされています

モジュールは、さまざまなプログラム言語で記述できます。モジュールのサーチパスは、環境変数ANSIBLE_LIBRARYか、コマンドラインオプションの--module-pathで指定します。

ansible-modules-core, ansible-modules-extrasは、ansible本体と一緒にデフォルトのサーチパスにあるライブラリパスにインストールされますが、独自のパスを利用シたい場合は、上記のような方法で追加する必要があります。 あなたのPlaybookのトップレベルディレクトリにある libraryディレクトリは、デフォルトでモジュールのサーチパスに入っています。

モジュールを作成したら、modules-extras projectにPull Requestを出してみてください。Ansibleのモジュールプロジェクトには、より一般的に利用されているモジュール群が登録されているmodules-core projectがあります。Extrasモジュールは、定期的にCoreモジュールに昇格する場合がありますが、これらのモジュールに決定的な違いはありません。どちらもAnsible本体と一緒にパッケージングされます。

Tutorial

まず最初に、現在のシステムの時刻を取得するモジュールを作成してみましょう。 ここではPythonを利用しますが、ファイルに対するI/O制御と、標準出力への出力機能を持っている他の言語(例えば、bashなどのシェルスクリプト,C++,clojure,Rubyなどなど)で、モジュールを記述することが可能です。

Pythonでモジュールを記述する場合、Coreモジュールが共通で利用しているモジュールとして最低限必要となる基本的な機能(さまざまな関数やクラスなど)を、同じようにimportして利用することができますが、Python以外の言語では、このようなショートカットは提供されません。そこで、まずは、このような便利な利用せずにモジュールを記述してみましょう。ショートカットを利用する方法は、のちほど紹介します。

本来、このような場合だとcommandモジュールを利用すれば、この例のようにシステム時刻を取得するようなモジュールは必要ありませんが、ここでは、あえて作成してみることにします。

CoreやExtrasなどのAnsibleのモジュールのコードは、モジュールを書くための良い教材です。コードを読みましょう。目的にもよりますが、async_wrapperのように、ユーザが直接利用しないモジュールは参考にしないほうが良いでしょう。参考にするなら、serviceモジュールやyumモジュールあたりがオススメです。

それでは、Pythonで書かれたサンプルコード(timetest.py)を見てみましょう。

#!/usr/bin/python

import datetime
import json

date = str(datetime.datetime.now())
print json.dumps({
    "time" : date
})

Testing Modules

Ansibleと一緒に提供される、テストスクリプトをチェックアウトして利用します。

(注1) --recursiveオプションを付与してgit cloneを実行すると、Ansible本体と同時にCoreとExtraモジュールもあわせてチェックアウトしてくれます。

(注2)ここではv1系のAnsibleでテストするため、オリジナルのドキュメントのパスから変更しています。

$ git clone git@github.com:ansible/ansible.git --recursive
$ source ansible/v1/hacking/env-setup

test-moduleスクリプトで、先ほど作成したtimetest.pyモジュールを実行してみましょう。

$ ansible/v1/hacking/test-module -m ./timetest.py

以下のような結果が出力されます。

(注3) 2015-07-14時点でのテストスクリプト実行結果に差し替えています

* including generated source, if any, saving to: /Users/saitou/.ansible_module_generated
* this may offset any line numbers in tracebacks/debuggers!
***********************************
RAW OUTPUT
{"time": "2015-07-14 16:52:13.819876"}


***********************************
PARSED OUTPUT
{
    "time": "2015-07-14 16:52:13.819876"
}

モジュールが動作しなかった場合は、コードのtypoなどを再確認してくださいね。

Reading Input

次に、現在時刻を変更してみましょう。変更する時刻は、キーになるパラメータ名と、その値となる時刻のペアを time=<string> 形式でモジュールに渡されます。

Ansibleは、モジュールに渡されたパラメータを引数ファイルに保存します。モジュールは、この引数ファイルを読み込んで適切にパースしてやる必要があります。引数ファイルは、ただのテキストファイルですから、パーサ次第でさまざまな形式に対応できますが、ここでは基本的な key=value 形式の引数をパースすることにします。

時刻を設定するためのサンプルは、以下の通りです。

time time="March 14 22:10"

もしも、timeパラメータが指定されなかった場合は、現在時刻を取得して返すことにしましょう。

  • メモ: 時刻の設定や取得をするなら、shellモジュールを使うべきですが、ここではチュートリアルのために、あえて新たにモジュールを作成することにしました。

以下のサンプルモジュール(time)を見てください。このモジュールの動きを、コード内のコメントとして記載しています。また、このモジュールのコードは、若干冗長に感じるかもしれませんが、これは、モジュール作成のチュートリアルに利用するためです。このサンプルコードは、実際には、もう少し短くシンプルに書くことができます。

(注4) MacOS Xの場合は、rc = os.system("date -s \"%s\"" % value) を rc = os.system("date \"%s\"" % value)としてください。

#!/usr/bin/python

# import some python modules that we'll use.  These are all
# available in Python's core

import datetime
import sys
import json
import os
import shlex

# read the argument string from the arguments file
args_file = sys.argv[1]
args_data = file(args_file).read()

# for this module, we're going to do key=value style arguments
# this is up to each module to decide what it wants, but all
# core modules besides 'command' and 'shell' take key=value
# so this is highly recommended

arguments = shlex.split(args_data)
for arg in arguments:

    # ignore any arguments without an equals in it
    if "=" in arg:

        (key, value) = arg.split("=")

        # if setting the time, the key 'time'
        # will contain the value we want to set the time to

        if key == "time":

            # now we'll affect the change.  Many modules
            # will strive to be 'idempotent', meaning they
            # will only make changes when the desired state
            # expressed to the module does not match
            # the current state.  Look at 'service'
            # or 'yum' in the main git tree for an example
            # of how that might look.

            rc = os.system("date -s \"%s\"" % value)

            # always handle all possible errors
            #
            # when returning a failure, include 'failed'
            # in the return data, and explain the failure
            # in 'msg'.  Both of these conventions are
            # required however additional keys and values
            # can be added.

            if rc != 0:
                print json.dumps({
                    "failed" : True,
                    "msg"    : "failed setting the time"
                })
                sys.exit(1)

            # when things do not fail, we do not
            # have any restrictions on what kinds of
            # data are returned, but it's always a
            # good idea to include whether or not
            # a change was made, as that will allow
            # notifiers to be used in playbooks.

            date = str(datetime.datetime.now())
            print json.dumps({
                "time" : date,
                "changed" : True
            })
            sys.exit(0)

# if no parameters are sent, the module may or
# may not error out, this one will just
# return the time

date = str(datetime.datetime.now())
print json.dumps({
    "time" : date
})

それでは実行してみましょう。システムの時刻を修正します。

(注5) sudo権限が必要です。影響範囲が大きいので、テストであっても、あまり大幅なシステム時刻の変更はしないようにしましょう。

sudo ansible/v1/hacking/test-module -m ./time -a time=\"0714195315\"

実行結果は以下の通りです

(注6) 2015-07-14時点でのテストスクリプト実行結果に差し替えています

(注7) -a オプションで、time引数を与えなかった場合は、現在時刻を出力して正常終了します。

* including generated source, if any, saving to: /Users/saitou/.ansible_module_generated
* this may offset any line numbers in tracebacks/debuggers!
***********************************
RAW OUTPUT
2015年 7月14日 火曜日 19時53分00秒 JST
{"changed": true, "time": "2015-07-14 19:53:00.002979"}


***********************************
PARSED OUTPUT
{
    "changed": true,
    "time": "2015-07-14 19:53:00.002979"
}


$ sudo ansible/v1/hacking/test-module -m ./time
* including generated source, if any, saving to: /Users/saitou/.ansible_module_generated
* this may offset any line numbers in tracebacks/debuggers!
***********************************
RAW OUTPUT
{"time": "2015-07-15 11:23:53.311061"}


***********************************
PARSED OUTPUT
{
    "time": "2015-07-15 11:23:53.311061"
}

Module Provided 'Facts'

setupモジュールを利用すると、Ansibleが動作するシステムのさまざまなパラメータを取得することができます。この情報は、Playbookとtemplateモジュールで利用するtemplateの中で再利用することが可能です。さらに、Ansibleのシステムに手を加えることなしに、独自のパラメータを追加することができます。この場合、モジュールの戻り値として ansible_factsキーと、その値として、再利用したい独自パラメータを辞書型のデータを設定してやります。

{
    "changed" : True,
    "rc" : 5,
    "ansible_facts" : {
        "leptons" : 5000,
        "colors" : {
            "red"   : "FF0000",
            "white" : "FFFFFF"
        }
    }
}

これらのパラメータは、setupモジュールの実行直後から、Playbook内で利用可能となります。site_factsと呼ばれるモジュールを作成して、Playbookの先頭で実行して独自のパラメータを設定し、以降のセクションで利用するのは良い考えです。そして、われわれは、Ansibleのシステム情報を収集するセクションの改善に関して、常に門戸を開いています。

Common Module Boilerplate

最初に書いたとおり、Pythonでモジュールを記述するときは、非常に強力な共通モジュールを利用することができます。依然として、モジュールは1つのファイルとしてターゲットノードに転送されますが、パラメータファイルは不要となります。これにより、モジュールコードを簡単に記述できるだけでなく、モジュールの実行時間短縮という面でも効果があります。尚、ここでは、上記に関して詳細には説明しません。最良の学習方法は、Coreモジュールのソースコードを読むことです。

groupモジュールやuserモジュールを例として、説明が必要となる点を紹介します。重要な箇所は、以下のとおりで、モジュールの最後の部分に記述されています。

from ansible.module_utils.basic import *
if __name__ == '__main__':
    main()

さらに、興味深いのはAnsibleModuleクラスのインスタンスであるmoduleです。

module = AnsibleModule(
    argument_spec = dict(
        state     = dict(default='present', choices=['present', 'absent']),
        name      = dict(required=True),
        enabled   = dict(required=True, choices=BOOLEANS),
        something = dict(aliases=['whatever'])
    )
)

AnsibleModuleクラスは、モジュールに渡される引数や、実行結果を返す際の戻り値をハンドリングするための多くの機能を提供しており、引数のチェックなどを簡単に行うことができます。

モジュールの実行が成功した場合の戻り値を生成するコードは、以下のとおりです。

module.exit_json(changed=True, something_else=12345)

モジュールの実行が失敗した場合の戻り値を生成するコードは以下のようにシンプルなものです。このとき、必須のパラメータとして、msgにエラーの概要を設定します。

module.fail_json(msg="Something fatal happened")

その他にも、便利な機能としてmodule.sha1(path)のようなクラスががあります。詳しくは lib/ansible/module_common.py をチェックアウトして実装を確認してみてください。

構築したモジュールをテストするための最も良い方法は、hacking/test-moduleスクリプトをチェックアウトして利用することです。そして、このスクリプトが、Ansible本体以外から独立してモジュールをテスト実行するための唯一の手段です。

独自に作成したモジュールを、Ansibleのソースツリーにマージしてほしいなら、AnsibleModuleクラスを利用していることが必須要件となっています。

Check Mode

モジュールでは、オプションとしてチェックモードをサポートしています。あなたのモジュールで、チェックモードをサポートしたければ、AnsibleModuleオブジェクトのインスタンス生成時に、引数として supports_check_mode=True を渡す必要があります。チェックモードが指定されているか否かは、AnsibleModule.check_modeがTrueになっていることで判定できます。以下がその例です。

module = AnsibleModule(
    argument_spec = dict(...),
    supports_check_mode=True
)

if module.check_mode:
    # Check if any changes would be made but don't actually make those changes
    module.exit_json(changed=check_if_system_state_would_be_changed())

モジュールの開発者には、チェックモードが有効化されているときに、システム状態が変更されないことを保証する責任があります。

あなたのモジュールで、チェックモードをサポートしていない場合に、利用者がAnsibleでチェックモードを指定すると、Ansibleは単純にモジュールの実行をスキップします。

Common Pitfalls

モジュール内に、以下のようなコードを書いてはいけません。なぜなら、モジュールの出力はJSON形式でしか認められていないからです。

print "some status message"

モジュールは、いかなるメッセージも標準エラー出力に出力すべきではありません。なぜなら、最終的に標準出力と標準エラー出力はマージされてしまうので、JSON形式でのメッセージパースを妨げてしまう原因となるからです。もしも、stderrや、その他の出力が有効なJSON形式で返された場合、その出力はAnsibleの実行結果として表示されるでしょうが、実際のコマンド実行は失敗に終わります。

このスクリプトは、さまざまな問題を警告してくれます。開発中のモジュールをテストする時は、必ずtest-moduleスクリプトを利用しましょう。

Conventions/Recommendations

これまで紹介したサンプルコードを思い出しつつ、モジュール開発の際の、基本的な規則とガイドラインを以下にまとめました。

  • Playbookに配置したモジュールは、実行時に、name:パラメータに設定した文字列で呼び出されます(たとえばhandlerセクションに書くhandlerなど)。これはモジュールの別名のようなものです。
  • もしも、あなたが独自のパラメータをfactsとして返すモジュールを配置するのであれば、モジュール名は site_facts とするのが良いでしょう。
  • モジュールのbolean型の値は、デフォルトの'yes','no','on','off','true','false','1','0',1,0 の他、AnsibleModuleクラスで定義されている、choicesパラメータとboolean関数で定義をオーバーライドすることができます。
  • 依存関係を少なくするために、インクルードするモジュールは最小限にしましょう。依存関係がある場合は、モジュールの先頭にコメントとして記述します。また、インポートに失敗した場合は、ImportErrorをキャッチして、はJSONでエラーメッセージを返してください(module.fail_json(msg))。
  • モジュールは、Ansibleが自動転送できるように、必要なコードをすべて含んだ、1つのファイルで構成しなければなりません。
  • 作成したAnsibleのモジュールを、RPMでパッケージ化して配布するのもあなたの自由です。その場合は、Ansibleが動作するホストの/usr/share/ansible/以下に配置してください。
  • モジュールからの出力は、JSON形式のみが許可されています。すべての戻り値は辞書型のデータ構造でなければなりません。なお、この辞書型データはネストさせることができます。配列はスカラはサポートされませんが、たとえば配列を辞書型データの値として利用することはできます。
  • 実行時の失敗通知には、msgパラメータに設定する概要説明と、'failed'キーを一緒に含まなければなりません。トレースバックを発生させるモジュールは、一般的に見て行儀の悪いモジュールです。しかし、AnsibleModuleクラスを利用していれば、トレースバックのような解析できないメッセージを、自動的に実行失敗時の戻り値として変換し、AnsibleModuleクラスで定義されているfail_json()を呼び出して返してくれます。
  • モジュールのリターンコードは、実際には重要ではありませんが、0が成功、それ以外を失敗として利用するのは、将来も継続して保証される可能性が高いです。
  • 多くの実行対象ホストからの出力が集約できるように、モジュールは関連のある情報のみを出力ようにするべきです。あらゆる情報を含んでいるログのような出力を返すのは、お行儀が良いとはいえません。

Documenting Your Module

Coreモジュールは、DOCUMENTATIONにモジュールの概要説明がYAML形式で定義されている必要があります。YAMLの編集に対応したテキストエディタを使えば、キーワードがハイライトされるので便利です。モジュール内のDOCUMENTATION変数に文字列として代入する前に、あらかじめYAML形式に沿っているかどうかをチェックしておきましょう。

Example

概要説明のサンプルが examples/DOCUMENTATION.yml <https://github.com/ansible/ansible/blob/devel/examples/DOCUMENTATION.yml>_ にあるので、checkoutして見てみましょう。

あなたのモジュール内のDOCUMENTATION変数に、以下のような概要説明を設定します。

#!/usr/bin/python
# Copyright header....

DOCUMENTATION = '''
---
module: modulename
short_description: This is a sentence describing the module
# ... snip ...
'''

descriptionnotes フィールドの記述には、いくつかの特別なマクロを利用することができます。

U()はURL、M()はモジュール、I()はイタリック、そしてC()は等幅フォーマット表記に置換されるマクロです。C()はファイル名やオプション名に利用し、I()にはパラメータを、モジュールの名前の指定にはM(module)を利用しましょう。

コロンや'を含む、利用方法のサンプルをYAML形式で記述するのは難しいですが、これらは以下の通り、モジュール内のEXAMPLES変数にプレーンテキストで設定します。

EXAMPLES = '''
- action: modulename opt1=arg1 opt2=arg2
'''

モジュールをPull Requestするためには、EXAMPLES変数とDOCUMENTATION変数に、そのモジュールの適切な概要説明や利用方法が設定されている必要があります。

Building & Testing

作成したモジュールを'library'ディレクトリ以下に配置して、make webdocsコマンドを実行すると、'docsite/'ディレクトリ以下に'modules.html'ファイルが作成されます。

tip

もしも、あなたがYAMLの構文に問題を抱えているなら、 YAML Lint <http://www.yamllint.com/>_ を利用して、構文をチェックすることができます。

tip

シェルの環境変数ANSIBLE_KEEP_REMOTE_FILESに1を設定しておくと、Ansibleが実行後のモジュールを削除するのを防ぐことができ、モジュールのデバッグに役立ちます。

Module Paths

あなたのモジュールを、Ansibleが見つけられない問題が発生しているなら、そのモジュールが、シェルの環境変数ANSIBLE_LIBRARY_PATHのパスに含まれていることを確認してください。

あなたがモジュールプロジェクトをforkしている場合は、以下のように設定します。

ANSIBLE_LIBRARY=~/ansible-modules-core:~/ansible-modules-extras

このようにすれば、forkしたモジュールプロジェクトがAnsibleの標準のモジュールよりも先にロードされます。このような状況では、forkしたバージョンのバグを報告しないように注意が必要です。

あなたが開発しているプロジェクトに対して、Ansibleの正規のバージョンとは別のバージョン名をつけるのは悪くない考えです。どのバージョンをPullすれば良いか理解できているはずなので、間違いを犯すことはないでしょう。これは安全性の観点では悪くない考えです。

Getting Your Module Into Ansible

他のモジュールとの依存関係を最小限にした高品質なモジュールは、AnsibleのCore,Extrasリリースに含めることもできます。ただし、このようなモジュールは、Pythonで記述されAnsibleModuleクラスが提供している機能を適切に利用しており、その他の部分も首尾一貫した記述になっていなければなりません。必要ならメーリングリストで要件を確認して、github上にある Extras <https://github.com/ansible/ansible-modules-extras>_ プロジェクトにPull Requestを投げてください。Extrasプロジェクトに含まれるモジュールは、Coreモジュールに昇格するチャンスがあります。CoreとExtrasは同じ品質ですが、開発の優先度はCoreモジュールの方が、わずかに高く設定されています。

Module checklist

  • スクリプトの1行目は、つねに #!/usr/bin/python であるべきです。この場合、常に ansible_python_interpreter に設定されたPythonインタプリタが利用されます。

  • 以下のポイントを確認してください。

    • モジュールのパラメータが必須の場合は、requiredに true を、必須でない場合は false のどちらかを必ず設定します。
    • required を false に設定した場合は、default に 'null' であってもデフォルト値を設定する必要があります。
    • required を 'true' に設定した場合は、default にデフォルト値を設定する必要はありません。
    • 何も設定していないのにもかかわらず、 aliases: []choices: [] のようなパラメータを記述する必要はないので削除しましょう。
    • バージョンは浮動小数点ではなく、カレントの開発バージョン番号です。
    • モジュールの実際の引数が、ドキュメントと同じ仕様であることを確認しましょう。
    • パスワードのような機密情報を引数に指定する場合は、 no_log=True に設定しましょう。
    • 依存しているライブラリなどは、requirements=[] パラメータに記述しましょう。
    • Authorには、あなたのgithubのIDを設定しましょう。
    • U(),C(),I(),M()マクロをちゃんと使っていますよね?
    • モジュールの先頭部分のコメントで、GPLライセンスであることを記載しましょう。
    • そのモジュールはチェックモードを利用していますか? チェックモードを利用するように修正されていますか? それをドキュメントに書いておきましょう。
    • Examples: に記述されている実行例は、ちゃんと動きますか?
    • Returns: にモジュールの実行結果の戻り値の概要説明を書きましょう。
  • モジュール内では適切な例外処理を行わなければなりません。
    • 例外発生時に、どのような状態だったかというような有用なメッセージを msg に設定して返しましょう。
    • 全ての例外を一括処理するfinallyのように、有用なエラーメッセージを含んでいない例外処理方法は避けましょう。
  • モジュールには、sys.exit()を使ってはいけません。module.fail_json()を使いましょう。
  • main()内で、カスタムモジュールをインポートした場合の例外のハンドリング例は以下の通りです。HAS_LIBをmodule.fail_json()で返します。

.

try:
    import foo
    HAS_LIB=True
except:
    HAS_LIB=False
  • 戻り値のキーとして、NAやNoneが使われるような場合は、通常は他の値として定義するなど、データ構造には一貫性をもたせる必要があります。

  • そのモジュールは冪等性を担保していますか?もしも担保していなければ、ドキュメントに明記しましょう。

  • コードの末尾付近にある from ansible.module_utils.basic import * には、さまざまな関数が含まれています。これを利用すれば、デバッグのためのコード記述量を少なくできます。

  • 以下のように、以下のようなif文による条件でmain()関数を呼び出すことが可能です。

.

if __name__ == '__main__':
    main()
  • 他のモジュールとの整合させたいなら、あなたのモジュールのパラメータに aliases オプションで別名を設定することができます。

  • pep8の規約に準拠していることは、良いことですが必須ではありません。具体的に1行の長さを80文字までとする条件は、可読性の向上という観点では邪魔となります。

  • action/commandモジュールを使うは避けましょう。それと同じような機能を提供する他の方法があります。

  • あるモジュールのために利用する固有の情報を取得するような場合は、そのモジュールとは別に、モジュール名_factsモジュールを作成して、情報を収集させましょう。

  • モジュールの中から別のモジュールを実行したい...そんな時は、Playbookのroleセクションを書きましょう。

Deprecating and making module aliases

Ansible v1.8以降では、モジュール名の先頭に_を付与することで、そのモジュールを非推奨扱いにすることができます。たとえば、 old_cloud.py_old_cloud.py に変更すると、モジュールとしては有効で利用可能な状態ですが、主要なドキュメントやモジュールリストからは除外されます。

また、モジュールの別名として、_で始まるシンボリックリンクを利用して、古い名前を維持することもできます。以下のサンプルは、statモジュール呼び出す際に、別名のfileinfoを利用することができます。

EXAMPLES = '''
ln -s stat.py _fileinfo.py
ansible -m stat -a "path=/tmp" localhost
ansible -m fileinfo -a "path=/tmp" localhost
'''

参考となるサイトへのリンク

modules

モジュールに関する情報

developing_plugins

プラグインを開発するのに必要な情報

developing_api

PythonのコードからAnsibleのAPIを利用するための情報

GitHub Core modules directory

Coreモジュールのソースコードリポジトリ

Github Extras modules directory

Extrasモジュールのソースコードリポジトリ

Mailing List

開発者向けのメーリングリスト

irc.freenode.net

AnsibleのIRCチャンネル