EMQXプラグインの開発
このページでは、EMQXモノレポ外でカスタムEMQXプラグインを開発する手順を説明します。
EMQX公式プラグインは通常、EMQXモノレポ内で開発されます。詳細はEMQXプラグイン開発ガイドをご覧ください。
前提条件
開始する前に、以下を準備してください。
- EMQXのフックの知識
makeを含む動作するビルド環境(例:build_essential)- rebar3
- 対象とするEMQXリリースと同じメジャーバージョンのErlang/OTP。詳細はDockerの
org.opencontainers.image.otp.version属性や使用バージョンが記載された.tool-versionsファイル(例:https://github.com/emqx/emqx/blob/e5.9.0-beta.4/.tool-versions)を参照してください。Erlang/OTPのバージョン管理には[ASDF](https://asdf-vm.com/)の利用を推奨します。あるいは、[こちらのコマンド](https://github.com/emqx/emqx-builder/blob/main/show-latest-images.sh)でemqx-builderイメージを取得できます。
スタンドアロンプラグイン開発
スタンドアロンプラグイン開発には以下の2つのスタイルがあります。
- rebar3テンプレート: emqx-plugin-templateを使ってプラグインプロジェクトを生成します。このスタイルは
rebar3のみを使用します。 - Gitサブモジュールスタイル: EMQX 6.0以降で、プラグインを独立したリポジトリに保持し、EMQXをGitサブモジュールとして追加してモノレポのツールでビルドします。
Gitサブモジュールスタイル
プラグインのソースコードがプライベートであるなど、別リポジトリにプラグインを保持しつつEMQXモノレポのツールでビルドしたい場合にこのスタイルを使います。
EMQXをサブモジュールとして追加します。
bashgit submodule add --depth 1 git@github.com:emqx/emqx.git emqx対象バージョンに対応するEMQXブランチ(例:EMQX 6.0なら
release-60)をチェックアウトします。プラグインリポジトリをサブモジュールの
plugins/ディレクトリにシンボリックリンクします。bashln -s ../.. emqx/plugins/{plugin_name}EMQXサブモジュールからプラグインパッケージをビルドします。
bashcd emqx make plugin-{plugin_name}
.tar.gzの成果物はemqx/_build/plugins/以下に生成されます。
rebar3テンプレートスタイルの場合は以下の手順に進んでください。
プラグインテンプレートのインストール
EMQXはカスタムプラグイン作成を簡単にするためにemqx-plugin-templateを提供しています。新しいプラグインを作成するには、rebar3テンプレートとしてemqx-plugin-templateをインストールします。
Linux環境では以下のコマンドでemqx-plugin-templateをダウンロードしてください。
$ mkdir -p ~/.config/rebar3/templates
$ pushd ~/.config/rebar3/templates
$ git clone https://github.com/emqx/emqx-plugin-template
$ popdTIP
REBAR_CACHE_DIR環境変数が設定されている場合、テンプレートディレクトリは$REBAR_CACHE_DIR/.config/rebar3/templatesになります。詳細はこちらのissueをご参照ください。
インストール確認は以下で行います。
$ rebar3 new help出力にemqx-plugin (custom)がテンプレートとして表示されれば成功です。
プラグインスケルトンの生成
インストール済みテンプレートを使って新しいプラグインプロジェクトを生成します。
$ rebar3 new emqx-plugin my_emqx_pluginこれによりmy_emqx_pluginディレクトリに動作するスケルトンが作成されます。
ディレクトリ構成
rebar3 new emqx-pluginコマンドは、emqxを依存に含む標準的なErlangアプリケーションを以下のように作成します。
my_emqx_plugin
├── LICENSE
├── Makefile
├── README.md
├── erlang_ls.config
├── priv
│ ├── config.hocon.example
│ ├── config_i18n.json.example
│ ├── config_schema.avsc.enterprise.example
│ └── config_schema.avsc.example
├── rebar.config
├── scripts
│ ├── ensure-rebar3.sh
│ └── get-otp-vsn.sh
└── src
├── my_emqx_plugin_app.erl
├── my_emqx_plugin.app.src
├── my_emqx_plugin_cli.erl
├── my_emqx_plugin.erl
└── my_emqx_plugin_sup.erlsrc:プラグインのOTPアプリケーションのコードpriv:プラグインの設定ファイルやスキーマ(例ファイル含む)rebar.config:アプリケーションのビルドおよびリリースパッケージ化に使うrebar3設定ファイルMakefile:プラグインビルドのエントリポイントscripts:Makefile用の補助スクリプト。注意: テンプレートはemqxに依存しているため、カスタム版のrebar3が必要で、付属の./scripts/ensure-rebar3.shでインストールできます。README.md:ドキュメント用プレースホルダーLICENSE:プラグイン用サンプルライセンスファイル
設定ファイルrebar.configの理解
rebar.configはプラグインのビルドとリリースパッケージ化に使われます。内容を確認し、プラグインの要件に応じて調整してください。
重要なセクションは以下です。
- 依存関係(
deps)セクション - リリースセクション(
relx) - プラグイン説明(
emqx_plugin)セクション
depsセクションではプラグインが依存する他のOTPアプリケーションを追加できます。
{deps,
[
...
%% これは私のプラグインの依存関係です
{map_sets, "1.1.0"}
]}.テンプレートではmap_setsが1つの依存として追加されています。不要なら削除可能です。依存関係の詳細はrebar3依存関係ドキュメントを参照してください。
relxセクションではリリース名とバージョン、リリースに含めるアプリケーションのリストを指定します。
{relx, [ {release, {my_emqx_plugin, "1.0.0"},
[ my_emqx_plugin
, map_sets
]}
...
]}.通常はdepsセクションのランタイム依存アプリケーションをリリースに追加します。
リリース名とバージョンは、プラグインがEMQXにインストールされた際の識別子として使われます。APIやCLIでプラグインを指定する際の一意のID(例:my_emqx_plugin-1.0.0)となります。
プラグイン説明セクションでは、プラグインに関する追加情報を指定します。
{emqx_plugrel,
[ {authors, ["Your Name"]}
, {builder,
[ {name, "Your Name"}
, {contact, "your_email@example.com"}
, {website, "http://example.com"}
]}
, {repo, "https://github.com/emqx/emqx-plugin-template"}
, {functionality, ["Demo"]}
, {compatibility,
[ {emqx, "~> 5.0"}
]}
, {description, "Another amazing EMQX plugin"}
]
}srcディレクトリの概要
srcディレクトリはプラグインのOTPアプリケーションのコードを含みます。
my_emqx_plugin.app.src
標準的なErlangアプリケーション記述ファイルで、リリース時にmy_emqx_plugin.appにコンパイルされます。
- アプリケーションのバージョンはリリースバージョンと異なっても構いません。
applicationsセクションに特に注意してください。プラグインはOTPアプリケーションとしてビルドされるため、プラグインの開始・停止・再起動はこのOTPアプリケーションの操作と同じです。プラグインが他のアプリケーションに依存する場合は、必ずこのapplicationsセクションに記載してください。
my_emqx_plugin_app.erl
プラグインのアプリケーションを開始・停止するためのapplicationビヘイビア(start/2とstop/1関数)を実装するメインモジュールです。
start/2関数でよく行う処理は以下です。
- EMQXのフックポイントへの登録
- CLIコマンドの登録
- 監督ツリーの起動
オプションで_app.erlモジュールはon_config_changed/2とon_health_check/1のコールバック関数を実装できます。
on_config_changed/2はDashboard、API、CLI経由でプラグイン設定が変更された時に呼ばれます。on_health_check/1はプラグインの状態が要求された時に呼ばれ、プラグインの状態を返すことができます。
その他のファイル
my_emqx_plugin_cli.erlモジュールはプラグインのCLIコマンドを実装します。登録されるとemqx ctlコマンド経由で呼ばれます。
my_emqx_plugin_sup.erlはプラグインの典型的なスーパーバイザーを実装します。
my_emqx_plugin.erlはプラグインのメインモジュールで、プラグインのロジックを実装します。スケルトンでは簡単なログ出力を行うデモ用のフックをいくつか実装しています。その他のモジュールもプラグインに追加可能です。
注意
アプリケーションモジュールやファイル名は任意ですが、以下の条件は満たす必要があります。
- アプリケーション名はプラグイン名と同じであること
- アプリケーションモジュール(
_app)は{plugin_name}_appという名前であること
privディレクトリの概要
privディレクトリはプラグインの設定ファイルやスキーマを格納します。
config.hocon
プラグインの初期設定をHOCON形式で記述したファイルです。config.hocon.exampleを参照用に利用できます。
config_schema.avsc
プラグイン設定のスキーマをAvro形式で定義したファイルです。存在する場合、EMQXは設定更新時にこのスキーマに基づいて検証を行います。config.hoconがスキーマに合致しない場合、リリースビルドは失敗します。
さらに、このファイルにはUIヒントを含めることができ、EMQXダッシュボードでの対話的な設定が可能になります。参考例はconfig_schema.avsc.enterprise.exampleをご覧ください。
config_i18n.json
プラグイン設定UIの翻訳をJSON形式で記述したファイルです。例:
{
"$key": {
"zh": "中文翻译",
"en": "English translation"
},
...
}翻訳はconfig_schema.avscのUIヒントで参照されます。詳細はconfig_i18n.json.exampleおよびconfig_schema.avsc.enterprise.exampleを参照してください。
プラグインの実装
スケルトンが準備できたら、プラグインのロジック実装を開始します。通常、以下のロジックが必要です。
- フックとCLIコマンドの実装
- 設定更新の処理
- ヘルスチェックの処理
フックとCLIコマンドの実装
EMQXは様々なイベントに対するフックポイントを定義しています。任意のアプリケーション(プラグインを含む)はこれらのフックポイントにコールバックを登録し、イベントに反応したりデフォルト動作を変更できます。
よく使われるフックポイントはスケルトンファイルに含まれています。フックポイントの一覧、引数、期待される戻り値はEMQXコードに記載されています。
フックポイントにコールバックを登録するにはemqx_hooks:add/3関数を使います。以下のパラメータを指定してください。
- フックポイント名
- コールバックモジュールと関数、およびEMQXが渡す追加引数(あれば)
- コールバックの優先度(通常は最優先の
?HP_HIGHEST)
登録解除はemqx_hooks:del/2でフックポイント名とコールバックモジュール/関数を指定します。
例として、client.authenticateとclient.authorizeフックポイントの登録・解除は以下のように行います。
-module(my_emqx_plugin).
...
hook() ->
emqx_hooks:add('client.authenticate', {?MODULE, on_client_authenticate, []}, ?HP_HIGHEST),
emqx_hooks:add('client.authorize', {?MODULE, on_client_authorize, []}, ?HP_HIGHEST).
unhook() ->
emqx_hooks:del('client.authenticate', {?MODULE, on_client_authenticate}),
emqx_hooks:del('client.authorize', {?MODULE, on_client_authorize}).通常、フックはプラグインの開始・停止に合わせて有効化・無効化するため、start/2とstop/1関数内でhook/unhookを呼びます。
start(_StartType, _StartArgs) ->
{ok, Sup} = my_emqx_plugin_sup:start_link(),
my_emqx_plugin:hook(),
{ok, Sup}.
stop(_State) ->
my_emqx_plugin:unhook().コールバック関数のシグネチャはフックポイント仕様で確認できます。例:
-callback 'client.authorize'(
emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), allow | deny
) ->
fold_callback_result(#{result := allow | deny, from => term()}).
-callback 'client.authenticate'(emqx_types:clientinfo(), ignore) ->
fold_callback_result(
ignore
| ok
| {ok, map()}
| {ok, map(), binary()}
| {continue, map()}
| {continue, binary(), map()}
| {error, term()}
).コールバック関数の実装例:
%% クライアントIDがA-Z、a-z、0-9、アンダースコアのみの場合に接続を許可
on_client_authenticate(_ClientInfo = #{clientid := ClientId}, Result) ->
case re:run(ClientId, "^[A-Za-z0-9_]+$", [{capture, none}]) of
match -> {ok, Result};
nomatch -> {stop, {error, banned}}
end.
%% クライアントは/room/{clientid}形式のトピックのみサブスクライブ可能、他のトピックにはパブリッシュ可能
on_client_authorize(_ClientInfo = #{clientid := ClientId}, subscribe, Topic, Result) ->
case emqx_topic:match(Topic, <<"/room/", ClientId/binary>>) of
true -> {ok, Result};
false -> stop
end;
on_client_authorize(_ClientInfo, _Pub, _Topic, Result) -> {ok, Result}.スケルトンアプリでは、フックはmy_emqx_plugin:load/1で登録、my_emqx_plugin:unload/0で解除されます。
設定更新の処理
ユーザーがプラグイン設定を更新すると、プラグインアプリケーションのon_config_changed/2コールバックが呼ばれます。
このコールバックでは通常以下を行います。
- 新しい設定の検証
- プラグインが起動中なら変更に対応する処理
設定検証時はアプリケーションがまだ起動していない可能性があるため、ステートレスなチェックを行い、ノード間で不整合が起きるような環境依存チェックは避けてください。
プラグインが起動中の場合、設定変更を適用できます。一般的なパターンは以下です。
- アプリケーション起動時に設定を扱う
gen_serverを起動 - そのサーバー(例:
my_emqx_plugin_config_server)が現在の設定を読み込み状態を初期化 on_config_changed/2で設定を検証し、新設定をmy_emqx_plugin_config_serverに送信- サーバーが起動中なら状態を新設定で更新、起動していなければ何もしない
ヘルスチェックの処理
on_health_check/1コールバックはEMQXがプラグインの状態を要求した際に呼ばれます。プラグインは以下のように状態を報告できます。
- 正常なら
okを返す - 問題があればバイナリの理由を含む
{error, Reason}を返す
外部リソースに依存するプラグインではこのコールバックが重要です。
詳細はスケルトンアプリのmy_emqx_plugin_app:on_health_check/1を参照してください。
TIP
この関数はプラグイン起動中に呼ばれますが、起動や停止中の並行処理のために呼ばれることもあります。
実装例はカスタムプラグインロジックの実装に多数あります。
プラグインパッケージのビルド
以下のコマンドでプラグインのリリースを作成します。
$ cd my_emqx_plugin
$ make relこれによりプラグインリリース_build/default/emqx_plugin/my_emqx_plugin-1.0.0.tar.gzが作成されます。このパッケージはプラグインのプロビジョニング/インストールに利用可能です。
パッケージ構成
プラグインがリリースとしてビルドされると、パッケージ構成は以下のようになります。
└── my_emqx_plugin-1.1.0.tar.gz
├── map_sets-1.1.0
├── my_emqx_plugin-0.1.0
├── README.md
└── release.jsontarballにはコンパイル済みアプリケーション(rebar.configのrelxセクションで指定したもの)、README.md、プラグインのメタデータを含むrelease.jsonが含まれます。
{
"hidden": false,
"name": "my_emqx_plugin",
"description": "Another amazing EMQX plugin.",
"authors": "Anonymous",
"builder": {
"name": "Anonymous",
"contact": "anonymous@example.org",
"website": "http://example.com"
},
"repo": "https://github.com/emqx/emqx-plugin-template",
"functionality": "Demo",
"compatibility": {
"emqx": "~> 5.7"
},
"git_ref": "unknown",
"built_on_otp_release": "27",
"emqx_plugrel_vsn": "0.5.1",
"git_commit_or_build_date": "2025-04-29",
"metadata_vsn": "0.2.0",
"rel_apps": [
"my_emqx_plugin-0.1.0",
"map_sets-1.1.0"
],
"rel_vsn": "1.1.0",
"with_config_schema": true
}プラグイン拡張APIとUI
プラグインはEMQXプラグインAPIゲートウェイを通じてカスタムHTTPエンドポイントを公開でき、オプションでダッシュボードにネイティブUIを埋め込むことも可能です。
プラグインHTTP API
プラグインAPIゲートウェイは以下のパスでプラグインへのリクエストをルーティングします。
/api/v5/plugin_api/{plugin_name}/...これらのリクエストを処理するには、プラグインアプリモジュールでon_handle_api_call/4を実装し、メソッドやパスでディスパッチします。実装例はplugins/emqx_username_quota/src/emqx_username_quota_app.erlやemqx_username_quota_api.erlを参照してください。
コールバック仕様
on_handle_api_call(Method, PathRemainder, Request, Context) -> Result| パラメータ | 説明 |
|---|---|
Method | get | post | put | patch | delete |
PathRemainder | {plugin_name}以降のパスのバイナリセグメントのリスト(パーセントデコード済み) |
Request | query_string、headers、body(GET/DELETE以外はJSON)を含むマップ |
Context | 認証メタデータやネームスペース情報を含むマップ |
受け入れ可能な戻り値:
{ok, StatusCode, Headers, Body}{error, StatusCode, Headers, Body}{error, not_found}
ダッシュボードのプラグインネイティブUI
emqx_pluginメタデータにindexフィールドが含まれる場合、EMQXダッシュボードはプラグインのネイティブUIをiframeで表示します。ダッシュボードはプラグインAPIのベースパスを付加します。
/api/v5/plugin_api/{plugin_name}{index}例:index: "/ui"なら/api/v5/plugin_api/{plugin_name}/uiになります。
ネイティブUIを無効にするには、indexフィールドを省略するか空文字に設定してください。