規則、ヒント、および落とし穴

モジュールの設計および開発を行う際に、以下の基本的な規則およびヒントに従って、読みやすく使用可能なコードを作成します。

モジュールのスコープ設定

特に、自分のモジュールを Ansible Core に組み込みたい場合は、各モジュールに十分な (ただし多すぎない) ロジックと機能が含まれていることを確認してください。これらのガイドラインが難しいと感じたら、「本当にモジュールを書く必要があるのかどうか」を検討してください。

  • 各モジュールには、簡潔で十分に機能が定義されている必要があります。基本的には、1 つのことを十分に行うという UNIX の哲学に従ってください。
  • 既存のモジュールに listinfo 状態のオプションを追加しないでください。新しい _info モジュールまたは _facts モジュールを作成します。
  • モジュールは、使用する API またはツールの基礎となるすべてのオプションを把握する必要はありません。たとえば、必要なモジュールパラメーターの安定した値を文書化できないと、モジュールは Ansible Core には組み込まれません。
  • モジュールは、リソースと対話するためのロジックの多くを網羅しなければなりません。複雑な API に関する軽量ラッパーにより、ユーザーは Playbook にロジックを過剰にオフロードしなければなりません。Ansible を複雑な API に接続する場合は、API の個々の小さな部分と対話する 複数のモジュールを作成 してください。
  • これはコードの重複や相違を招き、統一性や予測不可能性が低くなり、保守が難しくなります。モジュールはビルディングブロックであるべきです。「どのようにしてモジュールに他のモジュールを実行させることができるのか」という質問が浮かんでくる場合は、それがロールを作成する理由になります。

モジュールインターフェースの設計

  • モジュールがオブジェクトに対処している場合は、可能な限りそのオブジェクトのパラメーターを name とするか、またはエイリアスとして name を受け入れます。
  • ブール値のステータスを受け付けるモジュールは、yesnotruefalse などのもの、あるいはユーザーが出力する可能性のあるものは何でも受け付ける必要があります。AnsibleModule の共通コードは、type='bool' でこれをサポートします。
  • action/command は必須で、宣言的ではありませんが、同じ方法を表示する方法もあります。

一般的なガイドラインおよびヒント

  • 各モジュールは 1 つのファイルにまとめて自己完結させる必要があり、Ansible で自動転送できるようにします。
  • モジュール名は、単語の区切り文字として、ハイフンやスペースの代わりにアンダースコアを使用する必要はあります。ハイフンやスペースを使用すると、Ansible がモジュールをインポートできなくなります。
  • モジュールを開発する際には必ず hacking/test-module.py スクリプトを使用してください。よくある落とし穴について警告してくれます。
  • インストールに固有のファクトを返すローカルモジュールがある場合は、このモジュールに適切な名前が site_facts になります。
  • 依存関係を排除するか、最小限にします。モジュールに依存関係がある場合は、モジュールファイルの冒頭で文書化し、依存関係のインポートに失敗した場合は JSON エラーメッセージを発生させます。
  • ファイルに直接書き込まないようにします。一時ファイルを使用してから、ansible.module_utils.basicatomic_move 関数を使用して、更新された一時ファイルを所定の場所に移動させます。これにより、データの破損を防ぎ、ファイルの正しいコンテキストが保持されるようになります。
  • キャッシュを作成しないでください。Ansible は中央のサーバーや権限を持たないように設計されているため、さまざまなパーミッション、オプション、場所を指定して実行しないことを保証することはできません。中央の権限が必要な場合は、それを Ansible の上に置いてください (例: bastion/cm/ci サーバーまたは Tower を使用)。モジュールには組み込まないでください。
  • RPM でモジュールをパッケージ化する場合は、/usr/share/ansible の制御マシンにモジュールをインストールします。RPM のモジュールのパッケージ化は任意です。

関数およびメソッド

  • 各関数は簡潔にし、意味のある作業量を記述する必要があります。
  • 「Don’t repeat yourself (繰り返さないこと)」は、通常、適している哲学です。
  • 関数名にはアンダースコアを使用します (my_function_name)。
  • 各関数の名前は、それが何をするかを記述する必要があります。
  • 各関数にはドキュメント文字列 (docstring) が必要です。
  • コードの入れ子を多用しすぎている場合、それは通常、ループ本体が関数であることから利益が得られる可能性のある兆候です。私たちの既存のコードの一部は、時としてこのような例としては最適ではありません。

Python のヒント

  • URL を取得する場合は、ansible.module_utils.urlsfetch_url または open_url を使用します。urllib2 は使用しない。TLS 証明書をネイティブに検証しないでください。これは https に対して安全ではありません。
  • 通常の実行をラップする main 関数を含めます。
  • 条件から main 機能を呼び出して、ユニットテストにインポートします。以下に例を示します。
if __name__ == '__main__':
    main()

共有コードのインポートおよび使用

  • 可能な限り共有コードを使用します (wheel を再実装しないでください)。Ansible には、Python の共通コード AnsibleModule に加えて、多くの一般的なユースケースやパターンに対応した ユーティリティー が用意されています。また、複数のモジュールに適用されるドキュメント用のドキュメントフラグメントを作成することもできます。
  • ansible.module_utils コードは、他のライブラリーをインポートするのと同じ場所にインポートしてください。
  • 他の python モジュールのインポートにはワイルドカード (*) を使用しないでください。代わりに、インポートする関数 (例: from some.other_python_module.basic import otherFunction) を一覧表示します。
  • カスタムパッケージを try/except でインポートし、インポートエラーを捕捉し、main()fail_json() で処理します。例:
import traceback

from ansible.basic import missing_required_lib

LIB_IMP_ERR = None
try:
    import foo
    HAS_LIB = True
except:
    HAS_LIB = False
    LIB_IMP_ERR = traceback.format_exc()

次に main() で、argspec の直後に行います。

if not HAS_LIB:
    module.fail_json(msg=missing_required_lib("foo"),
                     exception=LIB_IMP_ERR)

また、モジュールの DOCUMENTATION ブロックrequirements セクションの依存関係を文書化します。

モジュール障害の処理

モジュールが失敗したときに、何が問題だったのかをユーザーが理解できるようにします。AnsibleModule の共通 Python コードを使用している場合は、fail_json を呼び出すと自動的に 失敗した 要素が含まれます。モジュールの失敗動作を適切なものにするために、以下を行います。

  • msg に、文字列の説明とともに failed のキーを指定します。これを行わないと、Ansible は標準の戻りコードを使用します (ゼロは成功、ゼロ以外はfailure)。
  • トレースバック (スタックトレース) は発生させません。Ansible はスタックトレースを扱うことができ、解析できないものは自動的に失敗した結果に変換しますが、モジュールの失敗時にスタックトレースを発生させるのはユーザーフレンドリーではありません。
  • sys.exit() は使用しないでください。モジュールオブジェクトから fail_json() を使用してください。

例外 (バグ) を正常に処理

  • 前もって検証します。早めに失敗し、有用で明確なエラーメッセージを返します。
  • 防御的なプログラミングを使用します。モジュールにはシンプルなデザインを使用し、エラーを適切に処理し、直接のスタックトレースを回避します。
  • 予測可能な方法で失敗します。失敗がどうしても避けられない場合は、最も期待される方法で失敗します。基礎となるツール、またはシステムの一般的な動作方法を模倣します。
  • 実行内容に関する有用なメッセージを表示して、それに例外メッセージを追加します。
  • キャッチオール例外は使用しないでください。基になる API で試行されたアクションに関する非常に優れたエラーメッセージがない限り、これらの例外はほとんど役に立ちません。

正確で有益なモジュール出力を作成

モジュールは有効な JSON のみを出力しなければなりません。正確で有用なモジュール出力を作成するには、以下のガイドラインに従ってください。

  • トップレベルの戻り値の型をハッシュ (ディレクトリー) にします。
  • 複雑な戻り値をトップレベルのハッシュ内に入れ子にします。
  • トップレベルの戻り値ハッシュ内にリストや単純なスカラー値を組み込みます。
  • モジュールの出力を標準エラーに送らないでください。システムが標準エラーと標準出力をマージし、JSON の解析を妨げるためです。
  • 標準エラーを捕捉し、標準出力に JSON 形式の変数として返します。これがコマンドモジュールの実装方法です。
  • モジュール内で print("some status message") は絶対にしないでください。有効な JSON 出力を生成しないためです。
  • 変更がない場合でも、有用なデータを常に返します。
  • 状態/アクションに影響を及ばさないために、戻り値は一貫したものにしてください (モジュールによっては非常に乱雑なものもあります)。
  • 戻り値は再利用可能なものにする。ほとんどの場合は読むことはありませんが、処理して再利用できるものにします。
  • diff モードの場合は diff を返す。これは、特定のモジュールでは意味をなさないため、すべてのモジュールに必要なわけではありませんが、該当する場合には使用してください。
  • Python の標準的な JSON エンコーダーおよびデコーダーライブラリー を使用して、戻り値を JSON としてシリアライズできるようにします。基本的な python の型 (string、int、dicts、lists など) はシリアライズ可能です。
  • exit_json() 経由でオブジェクトを返さないようにしてください。代わりに、オブジェクトから必要なフィールドをディクショナリーのフィールドに変換して返します。
  • 多数のホストからの結果が一度に集約されるため、モジュールは関連する出力だけを返すべきです。ログファイルの内容全体を返すのは、一般的には悪い形式です。

モジュールが標準エラー (stderr) を返したり、その他の方法で有効な JSON の生成に失敗した場合でも、実際の出力は Ansible に表示されますが、コマンドは成功しません。

Ansible の規則に準拠

Ansible の規則は、すべてのモジュール、Playbook、ロールに渡って予測可能なユーザーインターフェースを提供します。モジュール開発において Ansible の規則に従うには、以下を行います。

  • モジュール間で一貫性のある名前を使用します (レガシーとの相違が多数あるため、問題を悪化させないようにしましょう)。
  • モジュール内で一貫したパラメーター (引数) を使用します。
  • 他のモジュールでパラメーターを正規化します。Ansible とモジュールが接続する API が同じパラメーターに異なる名前を使用している場合は、パラメーターにエイリアスを追加して、ユーザーがタスクや Playbook で使用する名前を選択できるようにします。
  • 結果ディクショナリー<common_return_values>ansible_facts フィールドに *_facts モジュールからファクトを返すことで、他のモジュールからアクセスできるようにします。
  • すべての *_info モジュールおよび *_facts モジュールに check_mode を実装します。ファクト情報に基づいて条件付けを行う Playbook は、check_mode でファクトが返された場合に限り、check_mode で正しく条件付けを行います。通常は AnsibleModule のインスタンスを作成する際に supports_check_mode=True を追加します。
  • モジュール固有の環境変数を使用します。たとえば、module_utils.urls.fetch_url() での基本的な認証に module_utils.api のヘルパーを使用していて、デフォルト値を環境変数に依存している場合は、モジュール間の競合を回避するために API_<MODULENAME>_USERNAME のようなモジュール固有の環境変数を使用します。
  • モジュールのオプションはシンプルで焦点を絞ったものにします。既存のオプションに多くの選択肢や状態を読み込んでいる場合は、代わりに新しいシンプルなオプションを追加することを検討してください。
  • 可能な場合はオプションのサイズを小さくします。大きなデータ構造をオプションに渡すと、いくつかの作業が省けるかもしれませんが、モジュールに渡す前に簡単に検証できない複雑な要件が追加されてしまいます。
  • 複雑なデータをオプションに渡したいのであれば、それを可能にするエキスパートモジュールと、基礎となる API やサービスに対してより「アトミックな」操作を提供するいくつかの小さなモジュールを作成します。複雑な操作には複雑なデータが必要です。その複雑さをタスクやプレイに反映させるか、vars ファイルに反映させるかをユーザが選択できるようにします。
  • ユーザーが既存の状態を無視して最終的な状態に集中できるように、(CRUDではなく) 宣言的な操作を実装します。たとえば、started/stoppedpresent/ absent`` を使用します。
  • 最終状態が一貫したもの (冪等性) になるようにします。同じシステムに対してモジュールを連続して 2 回実行すると 2 つの異なる状態になってしまう場合は、最終的な状態が一貫しているかどうかを再設計または書き換えてみてください。できない場合には、動作とその理由を記載してください。
  • 通常は他のオプションで返されるキーに NA/None が使用されている場合でも、標準の Ansible の戻り値構造内で一貫性のある戻り値を提供します。
  • 該当する場合は、モジュールのファミリーに適用される追加のガイドラインに従います。たとえば、AWS モジュールは Amazon のガイドライン に従う必要があります。

モジュールのセキュリティー

  • シェルからユーザー入力を渡さないようにします。
  • 常に戻りコードを確認してください。
  • subprocessPopenos.system ではなく、module.run_command を常に使用しなければなりません。
  • 絶対に必要な場合を除き、シェルは使用しないでください。
  • シェルを使用しなければならない場合は、use_unsafe_shell=Truemodule.run_command に渡す必要があります。
  • use_unsafe_shell=True でモジュール内の変数がユーザー入力から指定される可能性がある場合は、pipes.quote(x) でラップしなければなりません。
  • URL を取得する場合は、ansible.module_utils.urlsfetch_url または open_url を使用します。urllib2 は使用しないてください。これは、TLS 証明書をネイティブに検証せず、https に対して安全ではありません。