我が家ではインフラのIaC化が進んでいます。
APにTP-Linkを使用しているのですが、YAMAHAやCiscoみたいに公式が出しているツールはありません。
そこでGUIをPlaywrightで動かすことで擬似的に設定を入れ込むAnsibleモジュールを作ることにしました。
なぜ今からAnsibleなのか
Ansibleはだんだん使われなくなっている気がします。
サーバーの設定という面でも、今はDockerが居座るようになり、クラウド領域に置いてもTerraformにリードされ、存在感を薄めています。
それでもOS、ミドルウェアにおいてAnsibleは優位性を誇っており、存在感は薄まりつつもなくなることはないニッチな領域になりうると思っています。
かつてはサーバーにSSHで入っていたところをAnsibleにやらせて、TerraformはAWSのAPI叩くだけみたいなところでAnsibleは職人の領域でした。
今回で言うとTP-LinkはCLIを用意しておらず、GUIだけからでそれならAnsibleでもTerraformでも微妙なところな気がしましたが、ネットワーク機器ってやはりAnsibleな気がするのでAnsibleで…
余談:今回に限るとTeraformと相性が悪い
GUIとのコミュニケーションにPlaywrightを使っています。
なぜSeleniumやPuppeteerではないかというと、
- クロスプラットフォーム
- 洗練されたDX
という2つの観点からです。
PuppetterだとJSオンリーになってしまいますがPlaywrightは一つレイヤーを挟むことでJS以外にもPythonやC#にも対応しています。また、Seleniumだといちいちwaitを挟む必要があります。
E2Eテストツールと思われがちですが、こういったところでも強みを発揮するのがPlaywrightのいいところです。
また、Playwrightが一番推しているのがJSからの操作で、その後 Python、 C#、Javaの順に手厚くなりますが、Goは入っていません。コミュニティが出しているものはありますが、質で言うと微妙…
GUIをいじるのでPlaywright、Playwrightを使うのでPython、Pythonを使うのでAnsibleという理屈でした。
APIが用意されていれば違ったかもしれません。
モデリングをどうするか
これはPython自体の話でもありますが、モデリングはどのアプローチをとっても微妙です。
特にTypeScriptやRustに慣れていると、型の取り回しがしにくいし、構造体もオブジェクトもごっちゃになったPythonのオブジェクト指向は少し気持ち悪いです。
例えば
以下のように項目があるのでそれぞれのモデルを作ります。
- [] ステータス
- [] クイック セットアップ
- [] 動作するモード
- [] ネットワーク
- [] WAN
- [] LAN
- [] MAC クローン
- [] ワイヤレス 2.4GHz
- [] 基本設定
- [] WPS
- [] ワイヤレス セキュリティ
- [] ワイヤレス MAC フィルタリング
- [] ワイヤレス詳細設定
- [] ワイヤレス統計
- [] ゲスト ネットワーク
- [] DHCP
- [] DHCP 設定
- [] DHCP クライアント リスト
- [] アドレス予約
- [] 転送
- [] 仮想 サーバー
- [] ポート トリガー
- [] DMZ
- [] UPnP
- [] セキュリティ
- [] 基本セキュリティ
- [] 高度セキュリティ
- [] ローカル管理
- [] リモート管理
- [] 保護者による制限
- [] アクセス制御
- [] ルール
- [] ホスト
- [] ターゲット
- [] スケジュール
- [] 高度経路
- [] 静的経路リスト
- [] システム経路テーブル
- [] 帯域幅制御
- [] IP & MAC バインディング
- [] バインディング 設定
- [] ARP リスト
- [] 動的 DNS
- [] IPv6
- [] IPv6 ステータス
- [] IPv6 WAN
- [] IPv6 LAN
- [] システム ツール
- [] 時刻設定
- [] 診断
- [] ファームウェア アップグレード
- [] 工場出荷時の設定
- [] バックアップ & 復元
- [] 再起動
- [] パスワード
- [] システム ログ
- [] 統計
- [] ログアウト
これにはやり方が3つあると思っています。
- Protocol Buffersを使うこと
- dataclassを使うこと
- Pydanticを使うこと
です。
Protocol Buffersを使う場合
Protocol Buffersを使う場合以下のように書けます。
syntax = "proto3";
package router;
message Status {}
message QuickSetup {}
enum OperationModeType {
WIRELESS_ROUTER = 0;
WISP = 1;
BRIDGE_MODE = 2;
REPEATER = 3;
}
message OperationMode {
OperationModeType mode = 1;
}
message Network {
message WAN {}
message LAN {
string ip_address = 1;
}
message MACClone {}
WAN wan = 1;
LAN lan = 2;
MACClone mac_clone = 3;
}
message Wireless24GHz {
message BasicSettings {}
message WPS {}
message Security {}
message MACFiltering {}
message AdvancedSettings {}
message Statistics {}
BasicSettings basic_settings = 1;
WPS wps = 2;
Security security = 3;
MACFiltering mac_filtering = 4;
AdvancedSettings advanced_settings = 5;
Statistics statistics = 6;
}
message GuestNetwork {}
message DHCP {
message Settings {}
message ClientList {}
message AddressReservation {}
Settings settings = 1;
ClientList client_list = 2;
AddressReservation address_reservation = 3;
}
message Forwarding {
message VirtualServer {}
message PortTrigger {}
message DMZ {}
message UPnP {}
VirtualServer virtual_server = 1;
PortTrigger port_trigger = 2;
DMZ dmz = 3;
UPnP upnp = 4;
}
message Security {
message BasicSecurity {}
message AdvancedSecurity {}
message LocalManagement {}
message RemoteManagement {}
BasicSecurity basic_security = 1;
AdvancedSecurity advanced_security = 2;
LocalManagement local_management = 3;
RemoteManagement remote_management = 4;
}
message ParentalControl {}
message AccessControl {
message Rules {}
message Host {}
message Target {}
message Schedule {}
Rules rules = 1;
Host host = 2;
Target target = 3;
Schedule schedule = 4;
}
message AdvancedRouting {
message StaticRouteList {}
message SystemRouteTable {}
StaticRouteList static_route_list = 1;
SystemRouteTable system_route_table = 2;
}
message BandwidthControl {}
message IPMACBinding {
message BindingSettings {}
message ARPList {}
BindingSettings binding_settings = 1;
ARPList arp_list = 2;
}
message DynamicDNS {}
message IPv6 {
message Status {}
message WAN {}
message LAN {}
Status status = 1;
WAN wan = 2;
LAN lan = 3;
}
message SystemTools {
message TimeSettings {}
message Diagnostics {}
message FirmwareUpgrade {}
message FactoryReset {}
message BackupRestore {}
message Reboot {}
message Password {
string username = 1 [default = "admin"];
}
message SystemLog {}
message Statistics {}
TimeSettings time_settings = 1;
Diagnostics diagnostics = 2;
FirmwareUpgrade firmware_upgrade = 3;
FactoryReset factory_reset = 4;
BackupRestore backup_restore = 5;
Reboot reboot = 6;
Password password = 7;
SystemLog system_log = 8;
Statistics statistics = 9;
}
message Logout {}
message RouterConfig {
Status status = 1;
QuickSetup quick_setup = 2;
OperationMode operation_mode = 3;
Network network = 4;
Wireless24GHz wireless_2_4ghz = 5;
GuestNetwork guest_network = 6;
DHCP dhcp = 7;
Forwarding forwarding = 8;
Security security = 9;
ParentalControl parental_control = 10;
AccessControl access_control = 11;
AdvancedRouting advanced_routing = 12;
BandwidthControl bandwidth_control = 13;
IPMACBinding ip_mac_binding = 14;
DynamicDNS dynamic_dns = 15;
IPv6 ipv6 = 16;
SystemTools system_tools = 17;
Logout logout = 18;
}
ただ、Protocol Buffersは3からデフォルトが使えなくなったり、そもそも前述の通りPythonの型は貧弱なので、Protocol Buffersから型を作ってもそれほど恩恵がないということがあります。
今回はProtocol Buffersを使うのは諦めました。
dataclassを使う場合
Pydanticは別途ライブラリのインポートが必要になりますが、dataclassは標準で備わっています。
Pydanticより簡単に書けます。
from pydantic import BaseModel, EmailStr
class UserModel(BaseModel):
id: int
name: str
email: EmailStr
@dataclass
class UserData:
id: int
name: str
email: str
ただ、Pydanticはデフォルトでバリデーションも効きますし、クラスの合成も便利です。
今回はPydanticにしました。
設定項目に合わせて実装することでなるべく直感的にわかりやすくしました。
モデリングの実装(一部抜粋)
# Root settings
root = Setting("Router Configuration")
# Top-level categories
status = Setting("ステータス")
quick_setup = Setting("クイック セットアップ")
network = Setting("ネットワーク")
wireless_2_4ghz = Setting("ワイヤレス 2.4GHz")
guest_network = Setting("ゲスト ネットワーク")
dhcp = Setting("DHCP")
forwarding = Setting("転送")
security = Setting("セキュリティ")
parental_control = Setting("保護者による制限")
access_control = Setting("アクセス制御")
advanced_routing = Setting("高度経路")
bandwidth_control = Setting("帯域幅制御")
ip_mac_binding = Setting("IP & MAC バインディング")
dynamic_dns = Setting("動的 DNS")
ipv6 = Setting("IPv6")
system_tools = Setting("システム ツール")
logout = Setting("ログアウト")
# Sub-settings
network.sub_settings.extend([Setting("WAN"), Setting("LAN"), Setting("MAC クローン")])
# LAN settings with default values
lan = network.sub_settings[1] # Get LAN setting
lan.add_sub_setting(ConfigSetting("IPアドレス"))
lan.add_sub_setting(ConfigSetting("サブネットマスク"))
lan.add_sub_setting(ConfigSetting("ゲートウェイ"))
lan.add_sub_setting(ConfigSetting("DNSサーバー"))
wireless_2_4ghz.sub_settings.extend(
[
Setting("基本設定"),
Setting("WPS"),
Setting("ワイヤレス セキュリティ"),
Setting("ワイヤレス MAC フィルタリング"),
Setting("ワイヤレス詳細設定"),
Setting("ワイヤレス統計"),
]
)
dhcp.sub_settings.extend(
[Setting("DHCP 設定"), Setting("DHCP クライアント リスト"), Setting("アドレス予約")]
)
forwarding.sub_settings.extend(
[
Setting("仮想 サーバー"),
Setting("ポート トリガー"),
Setting("DMZ"),
Setting("UPnP"),
]
)
security.sub_settings.extend(
[
Setting("基本セキュリティ"),
Setting("高度セキュリティ"),
Setting("ローカル管理"),
Setting("リモート管理"),
]
)
access_control.sub_settings.extend(
[
Setting("ルール"),
Setting("ホスト"),
Setting("ターゲット"),
Setting("スケジュール"),
]
)
advanced_routing.sub_settings.extend(
[Setting("静的経路リスト"), Setting("システム経路テーブル")]
)
ip_mac_binding.sub_settings.extend(
[Setting("バインディング 設定"), Setting("ARP リスト")]
)
ipv6.sub_settings.extend(
[Setting("IPv6 ステータス"), Setting("IPv6 WAN"), Setting("IPv6 LAN")]
)
system_tools.sub_settings.extend(
[
Setting("時刻設定"),
Setting("診断"),
Setting("ファームウェア アップグレード"),
Setting("工場出荷時の設定"),
Setting("バックアップ & 復元"),
Setting("再起動"),
Setting("パスワード"),
Setting("システム ログ"),
Setting("統計"),
]
)
ディレクトリ構成
tree
.
├── LICENSE
├── README.md
├── demo.yml
├── library
│ └── tplink.py
├── main.py
├── module_utils
│ ├── RouterConfig.py
│ ├── __init__.py
│ ├── config
│ ├── model
│ │ ├── AccessControl.py
│ │ ├── AdvancedRouting.py
│ │ ├── BandwidthControl.py
│ │ ├── DHCP.py
│ │ ├── DynamicDNS.py
│ │ ├── Forwarding.py
│ │ ├── GuestNetwork.py
│ │ ├── IPv6.py
│ │ ├── IpMacBinding.py
│ │ ├── Logout.py
│ │ ├── Network.py
│ │ ├── OperatingMode.py
│ │ ├── ParentalControl.py
│ │ ├── QuickSetup.py
│ │ ├── Security.py
│ │ ├── Status.py
│ │ ├── SystemTools.py
│ │ ├── Wireless2_4GHz.py
│ │ ├── __init__.py
│ └── utility.py
├── pyproject.toml
├── utility.py
└── uv.lock
実際にカスタムモジュールを作ってみる
(1) 必要な環境を準備
Python のパッケージ管理は、長らく pip が中心的な役割を担ってきました。しかし、プロジェクトが複雑化するにつれて、以下のような課題が顕在化してきました。
- 速度: 依存関係の解決やインストールに時間がかかる場合がある。
- 一貫性: 異なる環境で依存関係のバージョンを一致させるのが難しい。
- 仮想環境: 仮想環境の作成や管理がやや煩雑。
2025年ではuv を使うことで、これらの課題に対処します。特に以下の点で大きな利点を発揮します。
-
速度:
uvは Rust で書かれており、pipと比較して非常に高速に動作します。特に、キャッシュが有効な場合や、大規模な依存関係を持つプロジェクトでその差は顕著になります。これにより、開発者は環境構築にかかる時間を大幅に削減し、より開発に集中できます。 -
統合された機能:
uvは、パッケージのインストール (pipの代替)、仮想環境の管理 (venvの代替)、依存関係の解決 (pip-toolsの一部機能の代替) などの機能を一つに統合しています。これにより、複数のツールを使い分ける必要がなくなり、ワークフローが簡潔になります。 -
既存のワークフローとの互換性:
uvはpipやrequirements.txtなどの既存の標準と互換性があるため、既存のプロジェクトに段階的に導入しやすいという利点があります。完全に移行せずとも、速度が重要な部分でuvを利用するといった使い方も可能です。 -
より良いエラーメッセージ: まだ開発中ではありますが、
uvはpipよりも分かりやすいエラーメッセージの提供を目指しており、問題解決を助けることが期待されます。
歴史的に見ると、Python のパッケージ管理は distutils から始まり、setuptools、pip、そして virtualenv や pip-tools など、様々なツールが登場してきました。uv は、これらのツールの良い点を統合し、現代的なアプローチでより高速かつ効率的な開発体験を提供することを目指していると言えるでしょう。
特に、大規模なプロジェクトや、頻繁に環境を構築・再構築するような CI/CD 環境においては、uv の速度と効率性は大きなメリットとなります。
Ansible開発においてはレガシーな環境になりがちですが、これで対応することができます。
1. uv のインストール
まず、uv をインストールします。ターミナルを開いて、以下のコマンドを実行してください。
-
macOS/Linux:
curl -LsSf https://astral.sh/uv/install.sh | sh -
Windows:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
2. プロジェクトディレクトリの作成と移動
次に、新しいプロジェクトのディレクトリを作成し、そのディレクトリに移動します。そしてuvプロジェクトとして初期化します。
mkdir myproject
cd myproject
uv init example
3. リンター・フォーマッターなどの開発ツールの導入
これまではリンターやフォーマッターにflake8やblackを使っていましたが、2025年ではruffという統合ツールがあります。uvと同じ開発元で、よく使われるツールになります。
uv add -D ruff
そのほかお好みでmypyなどのツールを入れることもおすすめします。
uv add -D mypy
4. パッケージのロック
uv lockでパッケージのバージョンを固定します。requirements.txtのように数年たつとどのバージョンを入れていたか忘れる、みたいなことにはなりません。
uv lock
uv sync
5. 仮想環境のアクティベート
uvは自動で仮想環境を作ります。作成した仮想環境をアクティベートするには以下のコマンドを入力してください。
-
macOS/Linux:
source .venv/bin/activate -
Windows (cmd.exe):
.venv\Scripts\activate.bat -
Windows (PowerShell):
.venv\Scripts\Activate.ps1
アクティベートされると、ターミナルのプロンプトの先頭に (.venv) と表示されます。
6. ansibleのインストール
最後にansibleをインストールします。
uv add ansible
(2) シンプルなカスタムモジュールを作る
例えば、「指定したメッセージを標準出力に出すモジュール」を作ります。
① モジュールのPythonスクリプトを作成
ファイル名:my_echo.py
#!/usr/bin/python
from ansible.module_utils.basic import AnsibleModule
def main():
module = AnsibleModule(
argument_spec={
"message": {"type": "str", "required": True}
}
)
message = module.params["message"]
# 処理結果を返す
module.exit_json(changed=False, output=message)
if __name__ == "__main__":
main()
② モジュールを配置
作成した my_echo.py を library/ フォルダに置く(ローカル開発用)
mkdir -p ansible_modules/library
mv my_echo.py ansible_modules/library/
(3) Playbook から呼び出す
ファイル名:test_playbook.yml
- name: Test My Custom Module
hosts: localhost
tasks:
- name: Run my_echo module
my_echo:
message: "Hello, Ansible!"
register: result
- debug:
var: result
実行:
ansible-playbook -i localhost, test_playbook.yml -M ansible_modules/library
output: "Hello, Ansible!" のように表示されれば成功!
4. 2025年の開発のポイント
✅ Ansible 2.15+ に対応
最新のAnsibleでは型チェックが厳しくなっているため、argument_spec で適切な型を定義することが重要。
Python 3.9以上での開発を推奨。
✅ ansible-test を活用してテストする
ansible-test を使えば、モジュールの動作をチェックできる。
ansible-test sanity --local -v
✅ コレクション化を検討する
Ansibleは**「Ansible Collections」(モジュールのパッケージ)で管理する流れになっているので、
複数のカスタムモジュールを作るならコレクション化**するのがベスト。
ansible-galaxy collection init my_namespace.my_collection
my_collection/plugins/modules/ にモジュールを配置すれば、他の環境でも簡単に使える。
5. まとめ
- AnsibleのカスタムモジュールはPythonで作れる
argument_specでパラメータを定義し、適切にデータを入出力する- Ansible Playbook でテストする
ansible-testを活用すると品質向上- 本格的に使うならAnsible Collections化を検討
これを理解すれば、Ansibleモジュール開発をスムーズに進められるはずです! 🚀