自作のAnsible Collection をAnsible Galaxy へ公開 する

## 目次
[1. はじめに](#1-はじめに)
[2. 開発環境と準備](#2-開発環境と準備)
[3. Module の開発](#3-module-の開発)
[4. テスト](#4-テスト)
[5. 公開](#5-公開)
[6. 終わりに](#6-終わりに)

## 1. はじめに
今更ながら、Ansible Collection を自作し、[Ansible Galaxy](https://galaxy.ansible.com)に公開するまでの手順を一通り追ってみたいと思い立ち、ゼロからAnsible Collection を作成してみました。

Module の機能としては（車輪の再発明ではあるのですが）練習のため、簡単なLinux の設定情報取得を目的としたものを作成します。

最低限の実装・テストを行い、公開できる状態のAnsible Collection の作成を目指します。
また、筆者はAnsible Collection の開発初心者であるため、備忘も兼ねて、多少冗長でもできるだけステップごとの手順を詳しく記していきます。
ただし、開発の部分は一般的なModule と大差ないため、[4. テスト](#4-テスト)に重点を置いています。

※ 開発の手順はドキュメントに詳しく記載されています。詳細は下記のリンクを参照ください。

本稿は多くの部分でAnsible [公式ドキュメント](https://docs.ansible.com) の説明を参考にしています。コミュニティに関わるすべての方に感謝申し上げます。

## 2. 開発環境と準備
早速ですが、本題に入っていきます。

### 2.1. 開発環境
開発環境は以下の通りです。

| 種別 | ソフトウェア名 | バージョン(パッケージ名） |
| ---- | ------ | ------- |
| OS   | RHEL   | 9.2 |
| Ansible-Core | ansible-core | ansible-core-2.14.9-1.el9.x86_64 |
| Python | Python3 | python3-3.9.18-1.el9_3.1.x86_64
| エディタ(On LapTop) | VSCode | 1.66.0 |

特別なものは特に使用していません。
RHEL を使っているのは単に筆者の好みです。

### 2.2 準備
それでは、Ansible Collection の雛形を作っていきます。
次のコマンドで、Collection の雛形ができます。

~~~
$ ansible-galaxy collection init jkasama.linux_ut
~~~

今回は、`jkasama` と言う名前空間に、`linux_ut` と言うCollection を作成していきます。
コマンドが成功すると、下記のような雛形が出力されます。

~~~
jkasama/
└── linux_ut
    ├── docs
    ├── galaxy.yml
    ├── meta
    │   └── runtime.yml
    ├── plugins
    │   └── README.md
    ├── README.md
    └── roles
~~~

各ファイル・ディレクトリの役割を紹介していきます。

#### 1. galaxy.yml
* Ansible Collection が属するnamespace, Collection の名称、バージョン情報、作者、ライセンスなど、collection を公開する上で必要な情報を記述するファイルです

* 自作Collection をローカルで使用するだけであればあまり気にしなくても良いですが、配布（buildも含む）する際に機械的に内容を参照/チェックされます

galaxy.yml ファイルの記述方法は[こちら](https://docs.ansible.com/ansible/latest/dev_guide/collections_galaxy_meta.html)

#### 2. meta/runtime.yml
* Ansible Collection の動作に必要なAnsible バージョン情報などを記載します
* galaxy.yml 同様、build の際にansible-galaxy が内容をチェックします
* Collection を配布する上で、require_ansibles 属性は、編集必須です※

※ デフォルトではコメントアウトされており、正しく記述されていないとgalaxy にアップロードした際のチェックでエラーになります。

meta/runtime.yml の記述方法は[こちら](https://docs.ansible.com/ansible/latest/dev_guide/developing_collections_structure.html#runtime-yml)

#### 3. pulugins
* moduleやcallbackなどのPlugins コード本体, および　それらが使用するユーティリティを記述したPythonコードを配置します。
* 作成するPlugin の種類に応じて、次のようなサブディレクトリを切って利用します。（名称は決まっており、その一部を下表に示します。サポートされているサブディレクトリ名の一覧は、pluginsディレクトリ直下に自動生成されるREADME.md をご覧ください。）

  | ディレクトリ名 | 役割 |
  | ---------- | --- |
  | modules    | Module 本体のソースコードを配置するディレクトリ。各コードはmain 関数を持つ。 |
  | module_utils | Module間で共有するコードを配置するディレクトリ。|
  | lookup | Lookup Plugins 本体のソースコードを配置するディレクトリ。|
  | callback | Callback Plugins 本体のソースコードを配置するディレクトリ |
  
* plugins/modules 配下にmy_module.py があるとすると、Playbook から次のような名称でModule を利用可能となります。(plugins/plugins も同様)

※ 言わずもがなですが、Collection のPATH が通っていることが前提です。(ANSIBLE_COLLECTION_PATHS 環境変数や、ansible.cfg で制御します。）
~~~
- name: MY Module
  {namespace}.{collection_name}.my_module:
~~~

* module_utils ディレクトリ以下のコードは、plugins などの他のコードからも参照できます。
  インポートする際は、下記のような`improt` 文を書きます。

~~~
from ansible_collection.{namespace}.{collection_name}.plugins.module_utils.{module_name} import xxx
~~~

より詳細な内容は、[Developer Guilde](https://docs.ansible.com/ansible/latest/dev_guide/developing_collections_structure.html#plugins-directory) を参照してください。

#### 4. roles
* Collection に含まれる Ansible Role を配置します。（Role についての紹介は割愛します。）
* Ansible Role は以下のコマンドで雛形を作成します。

~~~
$ cd {path-to-roles}
$ ansible-galaxy role init myrole
~~~

* 上記のように作成したAnsilbe Role は、Playbook からは、例えば以下のように利用できます。

~~~
# Collection Role Import
- name: Import MyRole
  ansible.builtin.import_role:
    name: {namespace}.{collection_name}.myrole
~~~

雛形に追加する形でコードを作成、編集していきます。次節から早速コードを書いていきましょう。

## 3. Module の開発
ここではModule 本体を作成していきます。
要件は以下の通りです。

* 対象のLinux OS のデフォルトのロケール情報/言語コードを取得する。（変更はしない）
* 現在のLocale 設定の一覧を取得する。
* 設定可能な（Localegen 済みの）Locale の一覧を取得する。
* 取得に失敗した情報については空文字列または空のList を返す。

内容は一般的なAnsible Module 開発と同様です。

### 3.1 ソースコード

ソースコードは[こちら](https://github.com/jkasama/jksm.linux_ut/blob/main/jkasama/linux_ut/plugins/modules/get_locale.py)


以下、重要な部分のみ抜き出して紹介していきます。

~~~
Path: jksm/linux_ut/plugins/module/get_locale.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: Contributors of this Ansible Collections
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

~~~

ソースコード冒頭です。
ソースコードの前半は、Plugins を書く時の決まり文句が並びます。
1行目から
 * Shebang
 * Encoding
 * Copyright
 * `__futue__` インポート（python2 系との互換性のため）
 * `__metaclass__` 宣言（同上）

です。下2行は、`ansible-test sanity` のチェック対象です。

次は、本Module の動作仕様を記述する変数の宣言です。
~~~
DOCUMENTATION = r"""
---
module: get_locale
short_description: Get System Locale Information
description:
- Get System Locale Information and Return Them as structured Data(json format)
options: {}
seealso:
- module: ansilbe.builtin.debug
author:
- jkasama
version_added: 0.1.0
requirements:
- python >= 3.9                   　　　　　　　　　　　　　　　　　　　　　　　　
- ansible >= 2.11
notes:
- Tested on RHEL9.2
"""

EXAMPLES = r"""
---
- name: Gather System Locale Information
  jkasama.linux_ut.get_locale:
"""

RETURN = r"""
---
system_locale:
  type: str
  description: Default System Locale
  returned: success
language_code:
  type: str
  description: Language Code
  returned: On success
locale_detail:
  type: dict
  description: System Locale Detail Information
  returned: success
available_locales:
  type: list
  description: List of Available Locales in target systems. (localectl list-locales commands outputs)
  returned: success
"""
~~~

ここでは、DOCUMENT, EXAMPLES, RETURN を宣言しています。
これらの変数は、Module の動作には直接影響はしませんが、ansible-doc コマンドの出力に利用されます。

特に、公開する予定があるものについては、これらの変数を正しく編集することが重要です。
また、Python コード内では単なる文字列として宣言しますが、最終的にはansible-doc の処理中にYAML パーサーで読み込まれるため、正しいYAML 文法に則り記載する必要があります。

残りはコード本体です。

[Ansible 開発ガイド](https://docs.ansible.com/ansible/latest/dev_guide/index.html) に従ってコードを書いていきます。（上記DOCUMENTATION, EXAMPLES, RETURN の書き方も載っています。）

Ansible Module 開発特有の処理としては以下の部分が挙げられます。

~~~
from ansible.module_utils.basic import AnsibleModule ・・・ ①
    .
    .
    .

async def main():
    argument_spec = {}　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　・・・ ②
    module = AnsibleModule( 
        argument_spec=argument_spec,
    )                                                ・・・ ③
~~~

① で、AnsibleModule クラスをインポートし、③でインスタンスを作成します。
このインスタンスを通して、Playbook で宣言したパラメータ（module.params (dict)に格納）にアクセスできます。
② でargument_spec という辞書型データを宣言し、③ でコンストラクタに渡しています。

今回作成したModule は情報取得のためのモジュールであり、パラメータがないため空の辞書で十分ですが、パラメータがある場合はそのスキーマや制約事項をargument_spec (など）で定義する必要があります。

argument_spec や、AnsilbeModule クラスのコンストラクタ引数については[こちら](https://docs.ansible.com/ansible/latest/dev_guide/developing_program_flow_modules.html#argument-spec)を参照してください。

処理の完了や、エラー終了は下記のように行います。

* 正常終了時
~~~
result = {
    "msg": ...
    "changed": False,
    ... 
}                                           ・・・ ①
module.exit_json(**result)                  ・・・ ②
~~~

① で実行結果を宣言します。スキーマは比較的自由に決められますが、"changed" フィールドは重要です。Ansible は正常終了したtask について、このフィールドをもとに ok / changed の判定を行います。

② でAnsilbeModule クラスの正常終了メソッドを実行し、処理が完了します。

* 異常終了時（タスク失敗）
~~~
result = {
    "msg": ...,
    ... 
}
module.fail_json(**result)                  ・・・ ①
~~~
異常終了時もほぼ同様ですが、①で異常終了用のメソッドを実行します。

これにより、タスクは失敗したとみなされ、Ansible Playbook 側でハンドリングされない限り、対象サーバーへのタスクは終了します。

ソースコードの残りの部分は、一般的なPython コードのため割愛します。

次節では、`ansible-test` コマンドを用いた、Ansible Collection のテスト方法について紹介します。

### 4. テスト
Ansible Collection のテストには、`ansible-test` コマンドを利用します。
`ansible-test` コマンドは、次の下記の3つのテストを提供します。

* [sanity](https://docs.ansible.com/ansible/latest/dev_guide/testing_sanity.html)
* [units](https://docs.ansible.com/ansible/latest/dev_guide/testing_units.html)
* [integration](https://docs.ansible.com/ansible/latest/dev_guide/testing_integration.html)
 
各テストを実行していきましょう。

#### 4.1. sanity テスト
sanity テストは、Ansible Collection に含まれるPlugins, Role のコードに対しLintなどの静的なテストを実行します。
また、`galaxy.yml`や、`meta/runtime.yml` といったメタデータがきちんと記述されていることをチェックします。

`ansible-test sanity --list` を実行すると、実行されるテストの一覧を取得できます。

それでは早速、jkasama.linux_ut に対し、sanity テストを実行しましょう。

~~~
$ pwd
/home/{username}/ansible_collections/jkasama/linux_ut  ・・・ ①
$ ansible-test sanity --docker                         ・・・ ②
Starting new "ansible-test-controller-SzgALU2D" container.
Adding "ansible-test-controller-SzgALU2D" to container database.
Running sanity test "action-plugin-docs"
Running sanity test "ansible-doc"
.
.
.
Running sanity test "compile" on Python 2.7
ERROR: Found 1 compile issue(s) on python 2.7 which need to be resolved:
ERROR: plugins/modules/get_locale.py:61:9: SyntaxError: async def get_available_locales(str_code):
See
・
・
・
FATAL: The 2 sanity test(s) listed below (out of 46) failed. See error output above for details.
compile --python 2.7
import --python 2.7
FATAL: Command "podman exec ansible-test-controller-J71UoCYl /usr/bin/env ANSIBLE_TEST_CONTENT_ROOT=/root/ansible_collections/jkasama/linux_ut LC_ALL=en_US.UTF-8 /usr/bin/python3.11 /root/ansible/bin/ansible-test sanity --containers '{}' --truncate 225 --color yes --host-path tests/output/.tmp/host-6x888w_p --metadata tests/output/.tmp/metadata-akoz_hrc.json" returned exit status 1.
~~~

① で、テストの実行ディレクトリを確認しました。
`ansible-test`コマンドは、Ansible Collection のルートディレクトリで実行する必要があります。

② で、sanity テストを実行します。`--docker` オプションを付与することで、テストがコンテナ内で実行されるため、環境依存の問題を解消し、テストの信頼性向上が期待されます。
なお、--controller docker:{image} オプションを付与すれば、カスタムのイメージを利用できます。
いずれも付与しなかった場合は、ローカルにインストールされているPythonをベースに実行され、インストールされていないPythonバージョンに対するテストはスキップされます。

テストの結果は不合格でした。python2.7 における構文エラーが認められますが、今回はpyhon3.9以上と宣言しているため、ここではエラーを無視しましょう。

`tests/sanity/ignore-2.14.txt` に下記のエントリを追加します。
(2.14 は、Ansible-Core のバージョンです。）

~~~
plugins/modules/get_locale.py import-2.7
plugins/modules/get_locale.py compile-2.7
~~~

これで、`get_locale.py` に対する python2.7 での`import`, `compile`テストのエラーを無視するようになります。
当然ながら、エラーは安易に無視すべきではないため、エントリには注意しましょう。

それでは再度実行します。

~~~
$ ansible-test sanity --docker
Starting new "ansible-test-controller-iVMd6ClS" container.
Adding "ansible-test-controller-iVMd6ClS" to container database.
Running sanity test "action-plugin-docs"
Running sanity test "ansible-doc"
Running sanity test "changelog"
Running sanity test "compile" on Python 2.7
Running sanity test "compile" on Python 3.5
Running sanity test "compile" on Python 3.6
Running sanity test "compile" on Python 3.7
Running sanity test "compile" on Python 3.8
Running sanity test "compile" on Python 3.9
Running sanity test "compile" on Python 3.10
Running sanity test "compile" on Python 3.11
Running sanity test "empty-init"
Running sanity test "future-import-boilerplate"
Running sanity test "ignores"
Running sanity test "import" on Python 2.7
Running sanity test "import" on Python 3.5
Running sanity test "import" on Python 3.6
Running sanity test "import" on Python 3.7
Running sanity test "import" on Python 3.8
Running sanity test "import" on Python 3.9
Running sanity test "import" on Python 3.10
Running sanity test "import" on Python 3.11
Running sanity test "line-endings"
Running sanity test "metaclass-boilerplate"
Running sanity test "no-assert"
Running sanity test "no-basestring"
Running sanity test "no-dict-iteritems"
Running sanity test "no-dict-iterkeys"
Running sanity test "no-dict-itervalues"
Running sanity test "no-get-exception"
Running sanity test "no-illegal-filenames"
Running sanity test "no-main-display"
Running sanity test "no-smart-quotes"
Running sanity test "no-unicode-literals"
Running sanity test "pep8"
Running sanity test "pslint"
Running sanity test "pylint"
Running sanity test "replace-urlopen"
Running sanity test "runtime-metadata"
Running sanity test "shebang"
Running sanity test "shellcheck"
Running sanity test "symlinks"
Running sanity test "use-argspec-type-path"
Running sanity test "use-compat-six"
Running sanity test "validate-modules"
Running sanity test "yamllint"
~~~

エラーは出力されず、テストに合格しました。
なお、出力を見ればわかるように、`compile on Python2.7`テスト自体は実行されています。
`--skip-test` オプションで、`compile` や`import`テストをスキップすることも可能ですが、Python バージョン単位でのスキップができないため、筆者はignore ファイルへのエントリを選択しています。

#### 4.2. unit テスト
unit テストでは、`plugins` ディレクトリ配下のPython コードに対するUnit テストを実行します。

`ansible-test unit`コマンドでは、`pytest` ベースのunit テストが実行されます。
したがって、`test_{code_name}.py` の形式のファイル名のコードがテストコードとして実行されます。

それでは早速、テストコードを記述していきましょう。
ここでは単純に、各メソッドの正常性確認のみを行います。実際にはモックを使ってエラーハンドリングをチェックするなど、異常系のコードも必要になるでしょう。

`tests/units/plugins/test_get_locale.py` を作成します。

~~~
from ansible_collections.jkasama.linux_ut.plugins.modules.get_locale import (
    get_available_locales,
    get_locale_details,
    gather_commands_coroutines,
)

import pytest
import asyncio
import locale


current_locale = locale.getlocale()[1]


@pytest.fixture
def event_loop():
    return asyncio.get_event_loop()


def test_method_get_available_locales_success(event_loop):
    list = event_loop.run_until_complete(get_available_locales(current_locale))

    if list is not None:
        assert len(list) > 0
    else:
        assert list is None


def test_method_get_locale_details_success(event_loop):
    dict = event_loop.run_until_complete(get_locale_details(current_locale))

    assert "LANG" in dict.keys()
    assert "LC_ALL" in dict.keys()


def test_gather_get_locale_details_and_available_locale(event_loop):
    list = event_loop.run_until_complete(gather_commands_coroutines(current_locale))

    assert len(list) == 2
~~~

はじめの行で、テスト対象のパッケージのimport を行っています。
上述の通り、Ansible Collection の名前空間、名称を用いてimport することができ、相対import関連のエラーには遭遇しにくくなります。（筆者の未熟ゆえ、pytest の対象をimport する時によく混乱するため、統一的に扱えるのは大変ありがたいです。）
テストコードの中身は、通常のpytest コードと同様です。

それでは、テストを実行します。実行ディレクトリはsanity テストと同様です。

~~~
$ ansible-test units --docker
WARNING: Skipping unit tests on Python 2.7 because it is only supported by module/module_utils unit tests. No module/module_utils unit tests were selected.
WARNING: Skipping unit tests on Python 3.5 because it is only supported by module/module_utils unit tests. No module/module_utils unit tests were selected.
・
・
・
Unit test controller with Python 3.9
============================= test session starts ==============================
platform linux -- Python 3.9.5, pytest-7.1.3, pluggy-1.0.0
rootdir: /root/ansible_collections/jkasama/linux_ut, configfile: ../../../ansible/test/lib/ansible_test/_data/pytest/config/default.ini
plugins: forked-1.4.0, mock-3.10.0, xdist-2.5.0
gw0 I / gw1 I
gw0 [3] / gw1 [3]

...                                                                      [100%]
- generated xml file: /root/ansible_collections/jkasama/linux_ut/tests/output/junit/python3.9-controller-units.xml -
============================== 3 passed in 0.90s ===============================
Unit test controller with Python 3.10
============================= test session starts ==============================
platform linux -- Python 3.10.8, pytest-7.1.3, pluggy-1.0.0
rootdir: /root/ansible_collections/jkasama/linux_ut, configfile: ../../../ansible/test/lib/ansible_test/_data/pytest/config/default.ini
plugins: forked-1.4.0, mock-3.10.0, xdist-2.5.0
gw0 I / gw1 I
gw0 [3] / gw1 [3]

...                                                                      [100%]
- generated xml file: /root/ansible_collections/jkasama/linux_ut/tests/output/junit/python3.10-controller-units.xml -
============================== 3 passed in 0.88s ===============================
Unit test controller with Python 3.11
============================= test session starts ==============================
platform linux -- Python 3.11.0, pytest-7.1.3, pluggy-1.0.0
rootdir: /root/ansible_collections/jkasama/linux_ut, configfile: ../../../ansible/test/lib/ansible_test/_data/pytest/config/default.ini
plugins: forked-1.4.0, mock-3.10.0, xdist-2.5.0
gw0 I / gw1 I
gw0 [3] / gw1 [3]

...                                                                      [100%]
- generated xml file: /root/ansible_collections/jkasama/linux_ut/tests/output/junit/python3.11-controller-units.xml -
~~~

sanity テストと同様、`--docker` オプションを付与することにより、テストがコンテナ環境で実行されます。
コンテナ環境には、Python2.7 ~ Python3.11 がインストールされており（2024/02 時点）、各バージョンのPythonでそれぞれテストが実行されます。（楽でいいですね）

なお、Ansible-Core のバージョン依存と思われますが、今回実行したunit テストは python3.9 以上でのみ実行されていることがわかります。

テストレポートは`tests/output/junit/` 以下に出力されます。

#### 4.3. integration テスト
integration テストでは、実際にテストターゲット（コンテナ環境）に対しRole やModule を実行し、機能のテストを実行します。

Ansible Collection にintegration テストを追加・実行する手順については、[公式ドキュメント](https://docs.ansible.com/ansible/latest/community/collection_contributors/collection_integration_add.html#collection-creating-integration-tests)が例を交えて詳しく説明しています。

ここでは、手順にしたがって、テストケースの作成から追ってみます。

integration テストの作成では、`tests/integration/target/` ディレクトリ以下に、テストに用いるPlaybook を追加していきます。

概して、integration テストを追加するには、下記の手順に従います。

* setup 用Playbook の作成
* setup 方法の指定
* テストの記述
* 実行

それでは早速、手順にしたがってテストを追加していきましょう。

##### 4.3.1. setup 用Playbook の作成
テストの対象環境のセットアップ（例：依存パッケージのインストールなど）に利用するPlaybook を書いていきます。
setup 用Playbook は、`setup_` で始まるディレクトリに作成します。
ここでは、`tests/integration/targets/setup_get_locale/tasks/main.yaml` を作成します。

今回は、単純にシステムロケールを取得するだけのmodule であるため、特別なセットアップは不要です。
したがって、特段Playbook を書く必要もないのですが、動作を確認するため、単なるdebug コードを追加しましょう。

より実践的な例は、上述の公式ドキュメントを参照してください。

~~~
- name: Debug Setup Environment
  ansible.builtin.debug:
    msg: ansible-test integration get_locale modules
~~~

##### 4.3.2. setup 方法の指定
`get_locale` のテスト本体は、`tests/integration/targets/get_locale` 配下に作成していきます。

テストのセットアップ方法は、`tests/integration/targets/get_locale/meta/` に記述します。

~~~
$ touch tests/integration/targets/get_locale/meta/main.yaml
~~~

上記ファイルに下記のエントリを追加します。

~~~
dependencies:
  - setup_get_locale
~~~

これで、テスト本体の実行前に、`setup_get_locale` 配下に記述したPlaybook が実行されます。

##### 4.3.3. テストの記述
テスト本体を記述していきます。
`tests/integration/targets/get_locale/tasks/` 配下に、Playbook を作成しましょう。

~~~
$ touch tests/integration/target/get_locale/tasks/main.yaml
~~~

今回は、例によって正常系テストを記述します。

~~~
- name: Gather system Locale Informations
  jkasama.linux_ut.get_locale:
  register: result

- name: Debug
  ansible.builtin.debug:
    var: result

- name: Assertions
  ansible.builtin.assert:
    that:
      - "{{ result.language_code | length() > 0 }}"
      - "{{ result.locale_details | length() > 0 }}"
      - "{{ result.system_locale == 'UTF-8' }}"
~~~

前段で、get_locale モジュールを実行し、後段で結果に対するassertion を行っています。
実行環境に依存するため、UTF-8 かどうかのチェックはあまり意味がありませんが、動作確認を兼ねて記述しています。

##### 4.3.4. テスト実行
テストを実行しましょう。やはり例によってコンテナ環境で実行しています。

~~~
$ ansible-test integration --docker
Starting new "ansible-test-controller-STMMpnL3" container.
Adding "ansible-test-controller-STMMpnL3" to container database.
Installing requirements for Python 3.11 (controller)
Running get_locale integration test role
[WARNING]: running playbook inside collection jkasama.linux_ut

PLAY [testhost] ****************************************************************

TASK [Gathering Facts] *********************************************************
ok: [testhost]

TASK [setup_get_locale : Debug Setup Environment] ******************************
ok: [testhost] => {
    "msg": "ansible-test integration get_locale modules"
}

TASK [get_locale : Gather system Locale Informations] **************************
ok: [testhost]

TASK [get_locale : Debug] ******************************************************
ok: [testhost] => {
    "result": {
        "available_locales": null,
        "changed": false,
        "failed": false,
        "language_code": "en_US",
        "locale_details": {
            "LANG": "",
            "LANGUAGE": "",
            "LC_ADDRESS": "en_US.UTF-8",
            "LC_ALL": "en_US.UTF-8",
            "LC_COLLATE": "en_US.UTF-8",
            "LC_CTYPE": "en_US.UTF-8",
            "LC_IDENTIFICATION": "en_US.UTF-8",
            "LC_MEASUREMENT": "en_US.UTF-8",
            "LC_MESSAGES": "en_US.UTF-8",
            "LC_MONETARY": "en_US.UTF-8",
            "LC_NAME": "en_US.UTF-8",
            "LC_NUMERIC": "en_US.UTF-8",
            "LC_PAPER": "en_US.UTF-8",
            "LC_TELEPHONE": "en_US.UTF-8",
            "LC_TIME": "en_US.UTF-8"
        },
        "message": "Gather Loales Infomations End Successfully.",
        "system_locale": "UTF-8"
    }
}

TASK [get_locale : Assertions] *************************************************
ok: [testhost] => {
    "changed": false,
    "msg": "All assertions passed"
}

PLAY RECAP *********************************************************************
testhost                   : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
~~~

セットアップ用のPlaybookが実行された後に、テスト本体が実行されていることがわかります。

チェックにも合格しました。想定通り動作していそうです。

ちなみに、`available_locales` フィールドが`null` なのは、コンテナ環境では想定された挙動です。（ソースコードを見ていただければわかりますが、subprocess.run でlocalectl コマンドを呼び出しているためです。このコマンドは、コンテナ内からは正常に実行できません.）

テスト対象のコンテナは、例えば、次のようなオプションでベースイメージのディストリビューションを選択できます。

~~~
$ ansible-test integration --controller docker:fedora36
$ ansible-test integration --controller docker:ubuntu2204
~~~

デフォルトで選択可能なディストリビューションや、独自のコンテナイメージの利用方法は、`ansible-test` のヘルプを参照してください。

~~~
$ ansible-tests integration --help
~~~

### 5. 公開
最後に、Ansible Galaxy へ公開しましょう。
まずは、ローカルでbuild します。

~~~
$ pwd
/home/{username}/ansible_collections/jkasama/linux_ut
$ ansible-galaxy collection build
Created collection for jkasama.linux_ut at /home/{username}/ansible_collections/jkasama/linux_ut/jkasama-linux_ut-0.1.0.tar.gz
~~~

tar ball が出力されました。`galaxy.yaml`ファイル内で指定した値がファイル名に反映されます。

Ansible Galaxy に公開するにはこの出力をアップロードします。

ansible.cfg の[galaxy]セクションを正しく設定します。
[ドキュメント](https://docs.ansible.com/ansible/2.9_ja/galaxy/user_guide.html)

~~~
[galaxy]
server_list = galaxy

[galaxy_server.galaxy]
url=https://galaxy.ansible.com
token=<Your Token>
~~~

トークンは、Ansible Galaxy のGUIから取得できます。

次のコマンドで、先ほど作成したtar ballをアップロードします。
~~~
$ ansible-galaxy collection publish jkasama-linux_ut-0.1.0.tar.gz
Publishing collection artifact '/home/kasama/ansible_collections/jkasama/linux_ut/jkasama-linux_ut-0.1.0.tar.gz' to galaxy https://galaxy.ansible.com/api/
Collection has been published to the Galaxy server galaxy https://galaxy.ansible.com/api/
Waiting until Galaxy import task https://galaxy.ansible.com/api/v3/imports/collections/018d93a6-6a5e-71f2-b7fb-d0de9d98489b/ has completed
[WARNING]: Galaxy import warning message: No changelog found. Add a CHANGELOG.rst, CHANGELOG.md, or changelogs/changelog.yaml file.
[WARNING]: Galaxy import warning message: Ignore files skip ansible-test sanity tests, found ignore-2.14.txt with 2 statement(s)
Collection has been successfully published and imported to the Galaxy server galaxy https://galaxy.ansible.com/api/
~~~

出力を見ると、`ansible-test sanity`が自動実行されていることがわかります。ignore-2.14.txt に従い、無視したエラーがあることを示すWARNING が出力されていますが、想定通りです。
（CHANGELOG.md はもちろん書くべきですが、今回は省略しています。）

以上で、公開が成功しました。[こちら](https://galaxy.ansible.com/ui/namespaces/jkasama/)からアクセスできます。

### 6. 終わりに
以上が、筆者がAnsible Collection をゼロから作成し、公開までを実施した道のりです。

初めてだったこともあり、多少冗長でも全ステップを記載してみました。知っている方には退屈だったかもしれません。

1つのModule からなる最小限のCollection でも、きちんとテストを書いていくとやはり労力はかかりますね。各ステップをスムーズに実装できるよう精進が必要そうです。
せっかくCollection を作ったので、CI環境を整えつつ、少しずつ拡張していけたらと思います。

少しでも、お読みになった方の参考になれば幸いです。