Go言語で実装するプラグイン機構

ソフトウェアに拡張性を持たせる時にプラグイン機構を持たせる事は一般的ですが、それを実現する方法は結構バラバラなのかなと思います。例えば、

  1. C言語等の.so/.dllを読み込む方法
  2. Nodejsのような言語での単なるimport
  3. TCPやUnixソケットを利用してRPC通信を行う方法

などが有るのかなと思います。1番目・2番目は、関数の呼び出し速度等のオーバーヘッドが少なく高速ですが、言語等の制約が大きくなる・メモリを共有することによるセキュリティリスクが発生します。そこで、提供するインターフェースを制約出来る場合は、3番目の手法が多く使われるようです。

Go言語で開発されている、hashicorp/terraform cloudfoundry/cli は共に3番目のRPC通信でプラグイン機構を実装しています。その中でもterraformで使用されているプラグイン機構は、言語への依存が低いという特徴があり、興味深いので調べてみました。

hashicorp/go-pluginを試す

https://github.com/hashicorp/go-plugin

HashiCorpが開発しているライブラリで、Packer, Terraform, Nomad, Vault等の内部で利用されています。RPCにgRPCを使用していて、ホスト側のライブラリはGo言語ですが、クライアント(プラグイン側)の言語依存がほぼ無いのが特徴です。Go+Go/Go+Nodejs/Go+Python等の構成が非常に簡単に実現できます。また、実装も非常に軽いです。

試しにGoのホストと、Scalaのプラグインという構成で雑に試してみました。

サンプルリポジトリ: https://github.com/kamijin-fanta/go-plugin-example

このような簡単なサービスを実装してみました。サービスの定義・実装は素のgRPCとして実装するので、言語に合ったツールを使用します。Scalaでのサーバ実装には、protocの代わりに ScalaPB のコード生成を使用しました。traitが自動的に生成されるので、それぞれのメソッド(ここではSayHello)を実装するだけです。

// サービス定義
syntax = "proto3";
package com.example.protos;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
// サーバ実装
class GreeterImpl extends GreeterGrpc.Greeter {
  override def sayHello(req: HelloRequest) = {
    val reply = HelloReply(message = "Hello " + req.name)
    Future.successful(reply)
  }
}
var Handshake = plugin.HandshakeConfig{
  ProtocolVersion:  1,
}
var PluginMap = map[string]plugin.Plugin{
  "greeter": &GreeterGRPCPlugin{},
}
client := plugin.NewClient(&plugin.ClientConfig{
  HandshakeConfig:  Handshake,
  Plugins:          PluginMap,
  Cmd:              exec.Command(os.Getenv("PLUGIN")),
  AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
})

rpcClient, _ := client.Client()
raw, _ := rpcClient.Dispense("greeter")
service := raw.(Greeter)
result, _ := service.Hello("fukuyama") // Hello fukuyama

多くのコードを省略しているので、詳細はリポジトリよりコードを探してください。

通信フロー

hashicorp/go-pluginでは、TCPのgRPCを用いて通信を行いますが、ホストはポート番号等をどうやって知っているのでしょうか?簡単に図にしてみました。

  1. ホスト側はプラグインをForkExecする
  2. プラグインはネゴシエーションのための文字列を標準出力でホストに伝える (APIバージョン/接続プロトコル/Listenアドレスが含まれる)
  3. ホスト側のAPIバージョンと一致していれば、指定されたアドレスへgRPC接続を行う
  4. サービスのヘルスチェックを行う
  5. プラグインの通信を行う
  6. 通信が終了する際にホストはプラグインのプロセスを終了する

Negotiation

ネゴシエーションの為にプラグインからホストに送られる文字列は、1|1|tcp|127.0.0.1:1234|grpcの様な形式です。

CORE-PROTOCOL-VERSION | APP-PROTOCOL-VERSION | NETWORK-TYPE | NETWORK-ADDR | PROTOCOL
  • CORE-PROTOCOL-VERSION go-plugin自体のAPIバージョンで、現在は1
  • APP-PROTOCOL-VERSION ユーザが用意するAPIのバージョン
  • NETWORK-TYPE unixかtcpが指定可能
  • NETWORK-ADDR 接続先のアドレス
  • PROTOCOL grpcかnetrpcが指定可能

Health Check

ヘルスチェックは、gRPCを通じて行われます。プラグイン側はHealthサービスを実装している必要があります。とりあえず面倒なら常に status: ServingStatus.SERVING を返しておけば良いです。

message HealthCheckRequest {
  string service = 1;
}

message HealthCheckResponse {
  enum ServingStatus {
    UNKNOWN = 0;
    SERVING = 1;
    NOT_SERVING = 2;
  }
  ServingStatus status = 1;
}

service Health {
  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
}

health.proto: https://github.com/grpc/grpc/blob/master/src/proto/grpc/health/v1/health.proto

雑記

これを調べるきっかけは、Grafanaを調べている時に見つけたBackendPluginsでした。Grafanaのプラグインの種類は他に、Palnel Plugin, Datasource Pluginの2種類有るのですが、いずれもJSで記述するフロントエンドのプラグインです。認証等のコードを差し込めるように用意されたのがBackend Pluginsです。

Developing Backend Plugins: https://grafana.com/docs/plugins/developing/backend-plugins-guide/

このプラグインに採用されている仕組みが hashicorp/go-plugin でした。現在はDataSourceとRendererの2つのAPIしか用意されていないようですが、今後増えてくると拡張性が非常に増すのではないかと思います。

公式が用意している grafana/grafana-image-renderer プラグインは、TypeScriptで全て書かれています。言語に依存されない利点を活用しています。Nodejs上でgRPCサーバを用意し、今回のガイドと同じ様な実装を行ってサービスをプラグインとして提供しています。まだサードパーティのプラグインを受け入れるという感じでは無いのが悲しいですが、時間の問題だと思います。

go plugin 2019 05 17 17 50 29

↑水と油のリポジトリ

参考

続きを読む

GWの塩塚高原キャンプ場 2泊

2019年4月27日~29日のゴールデンウィーク期間中に、塩塚高原キャンプ場に行っていました。レビューという程でも無いですが、良いキャンプ場だったので紹介します。 幸運にも1週間前に予約を取ることができたので(キャンセル待ちの隙間を…)2泊しました。

塩塚高原キャンプ場|徳島県三好市の塩塚峰に広がるキャンプ場

四国の徳島県・愛媛県の県境に位置する高原に広がるキャンプ場です。 キャンプ場までは、住んでいる大阪から250kmの道のりでした。

DSC 0887

淡路SAの駐車場が行列だったので、室津PA下りへ立ち寄った図。海に空の青が映って非常に綺麗でした。ここまで1時間以上走っているので、空気が美味しいです。

DSC04221

高速道路を降りてからのキャンプ場までの道のりはたいへん険しく、多くが車1台が通るのが精一杯の山道でした。キャンプ場が有る標高800mまで登り切ると、壮大な景色が広がります。どこまで見渡しても空と山。こちらの写真は管理等からの眺めです。ポツポツと見える赤い屋根はバンガローなどです。管理等とキャンプサイト・バンガローの高度差も結構あるので、車での移動が楽でしょう。

DSC04232

キャンプサイトはこちら。全サイトに水道・電源が有ります。電源は追加で500円とのことで、貧乏心が使うことを許しませんでした。

DSC04243

元気に日陰を作ってくれるタープですが、2日目の夜の嵐で崩れてしまうのでした… 天気予報では1m/sくらいだったのに… 高原の天気は読みづらい…

DSC 0896

このキャンプ場の自分にとっての1番の魅力は、星空だと感じます。1日目は雲ひとつ無い晴天で、一面の星々を拝むことができました。視界をリアルにするために、辺りの景色も写しました。街明かりが一切入ってこない環境での天体観測が何年ぶりかなどと考えながら楽しんでいました。

ちなみに、夜間は静寂時間という時間が設定されており、21時くらいからみんな静かになってきます。就寝時間というわけではないですが、静かにする目安時間が決まっているのは良いですね。

DSC 0924

2日目は塩塚山へのハイキングと、温泉に行っていました。山頂付近まで車で移動できるので、超ヌルゲーです。車を降りて最短20分で山頂ですが、各々1時間コースにしたりいろいろ調整ができます。足腰が弱くても安心!

DSC 0938 DSC 0943

山頂付近に展望台がありますが、まさかのフリーWiFiが吹かれていました。一体どれだけヌルくなれば気が済むんだ… ちなみに、WiFiの機械の横についてる黒いやつがライブカメラです。ライブカメラ用に光ファイバー引いてきて、ついでにWiFi吹いたって感じっぽいです。

DSC 0950

決してゆるキャン△に影響された訳では無いんですが、カメラに写っているタイミングで親にURLを送るなどしてました。決してゆるキャン△に影響された訳では無いんですが。

UIからグルグル操作もできて楽しいです。 http://mgis.city-miyoshi.jp/mgis/mgs/lc_jv2.php?lcid=11&id=446

IMG 5474

下山中もいろいろな動植物を観察することができてとても良かったです。花も草も鳥も居てGOOD。

DSC 0969

猿もいました…………………………

DSC04254

2日を通しての気温ですが、最低気温が-1度・最高気温が13度程度でした。 寝袋はコールマンの封筒型を使用しています。こちらに自宅で使っている毛布を上下に挟み込んで寝ていたのですが、逆に熱くなって服を脱いでいました…

コールマンの寝袋: https://www.amazon.co.jp/dp/B00SLOSYPU?tag=kamijinfanta-22

ちなみに、この温度計は水銀温度計で、温度が上下すると青い棒が上に押しやられ最高・最低気温が記録されるスグレモノです。いつもキャンプに持っていき、空きのカメラ用三脚に固定しています。

DSC 0933

とても充実した2泊3日の旅を行うことができました。これから更に追加で2泊別の場所に行くわけですが、それはまたの機会に…

キャンプは良いぞ。

続きを読む