概要
オブジェクト指向についてClean Architectureでは
OOとは「ポリモーフィズムを使用することで、システムにあるすべてのソースコードの依存関係を絶対的に制御する能力」である。
これにより、アーキテクトは「プラグインアーキテクチャ」を作成できる。これは、上位レベルの方針を含んだモジュールを下位レベルの詳細を含んだモジュールから独立させることである
と説明されています。
このようにソフトウェアの拡張性を持たせるためにプラグイン機構を用意することは非常に大切です。
今回はhashicorpのgo-pluginを使って実現してみます。
プラグインの利用箇所
プラグイン機構にすることでメリットがありそうなケースってなんだろう?と思って簡単にいくつか挙げてみます。
こんなところでしょうか?
fluentdのようなETLツールはプラグイン開発が非常に活発ですね。
プラグイン機構にすることで自分は本体の堅牢さ・パフォーマンスに注力し、プラグイン開発は他の開発者に作ってもらってスピード開発といった両立が可能になります。
go-plugin
アーキテクチャ
以下のようになっています。
図に示している通り、Host側がクライアント、Plugin側がサーバです。
フローとしては以下です。
- HostがPluginをサブプロセスとして起動
- Pluginはサーバを起動してコネクトするのに必要な情報をHostに返す
- 受け取った情報を元にコネクションを貼る
- HostからRPCメソッドを呼び出す
- Pluginは↑のレスポンスを返す
- HostがPluginのプロセスをkill
使い方
- net/rpcを利用
- CLIツール
- 認証をプラガブルに
として作ってみます。
共通
var HandshakeConfig = plugin.HandshakeConfig{ ProtocolVersion: 1, MagicCookieKey: "BASIC_PLUGIN", MagicCookieValue: "hello", } const AuthPluginName = "authPlugin"type Auth interface { Authenticate() bool } type AuthRPCClient struct { client *rpc.Client } func (a *AuthRPCClient) Authenticate() bool { var resp bool// using net/rpc client's synchronous call. err := a.client.Call("Plugin.Authenticate", new(interface{}), &resp) if err != nil { panic(err) } return resp } type AuthRPCServer struct { Impl Auth } // Authenticate conforms to the requirements of the net/rpc server method.// https://golang.org/pkg/net/rpc/func (s *AuthRPCServer) Authenticate(args interface{}, resp *bool) error { *resp = s.Impl.Authenticate() returnnil } type AuthPlugin struct { Impl Auth } func (p *AuthPlugin) Server(*plugin.MuxBroker) (interface{}, error) { return&AuthRPCServer{Impl: p.Impl}, nil } func (AuthPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { return&AuthRPCClient{client: c}, nil }
ポイント
HandshakeConfig
はフロー2
のネゴシエーションに必要な設定- 実装部分ではnet/rpcを使うのでそれに則る
err := a.client.Call("Plugin.Authenticate", new(interface{}), &resp)
func (s *AuthRPCServer) Authenticate(args interface{}, resp *bool) error
- net/rpcのgodocに例を交えて説明されているので、一読しておくとすんなり理解できる
AuthPlugin
はgo-pluginで使うためのインタフェースを実装した形
Host側
func main() { logger := hclog.New(&hclog.LoggerOptions{ Name: "plugin", Output: os.Stdout, Level: hclog.Info, }) client := plugin.NewClient(&plugin.ClientConfig{ HandshakeConfig: common.HandshakeConfig, Plugins: pluginMap, Cmd: exec.Command("./plugin/auth"), Logger: logger, SyncStdout: os.Stdout, }) defer client.Kill() rpcClient, err := client.Client() if err != nil { log.Fatal(err) } raw, err := rpcClient.Dispense(common.AuthPluginName) if err != nil { log.Fatal(err) } auth := raw.(common.Auth) if auth.Authenticate() { fmt.Println("success!!") } else { fmt.Println("fail!!") } } var pluginMap = map[string]plugin.Plugin{ common.AuthPluginName: &common.AuthPlugin{}, }
ポイント
- ClientConfig
Cmd: exec.Command("./plugin/auth")
でフロー1
のサブプロセス起動をセットpluginMap
の名前とDispense()
時の引数名は一致させる
client.Client()
でサブプロセスを起動し、RPC connectionを確立Dispense()
で↑のconnectionをベースにプラグイン呼び出し用のconnectionを確立defer client.Kill()
でサブプロセスを終了させる
Plugin側
プラグイン側の実装です。
認証なし
認証なしなのでAuthenticate()
の中身にはロジックを入れず常にtrue
を返します。Serve()
を実行するとRPCサーバが起動します。
type PasswordAuth struct { logger hclog.Logger } func (p *PasswordAuth) Authenticate() bool { returntrue } func main() { logger := hclog.New(&hclog.LoggerOptions{ Level: hclog.Info, Output: os.Stderr, JSONFormat: true, }) auth := &PasswordAuth{ logger: logger, } // pluginMap is the map of plugins we can dispense.var pluginMap = map[string]plugin.Plugin{ common.AuthPluginName: &common.AuthPlugin{Impl: auth}, } plugin.Serve(&plugin.ServeConfig{ HandshakeConfig: common.HandshakeConfig, Plugins: pluginMap, }) }
パスワード認証
パスワード認証をAuthenticate()
の中身に入れます。
簡単のためパスワードは固定値で。
const ( password = "hoge" ) type PasswordAuth struct { logger hclog.Logger } func (p *PasswordAuth) Authenticate() bool { fmt.Print("password:") scanner := bufio.NewScanner(os.Stdin) scanner.Scan() return scanner.Text() == password } func main() { logger := hclog.New(&hclog.LoggerOptions{ Level: hclog.Info, Output: os.Stderr, JSONFormat: true, }) auth := &PasswordAuth{ logger: logger, } // pluginMap is the map of plugins we can dispense.var pluginMap = map[string]plugin.Plugin{ common.AuthPluginName: &common.AuthPlugin{Impl: auth}, } plugin.Serve(&plugin.ServeConfig{ HandshakeConfig: common.HandshakeConfig, Plugins: pluginMap, }) }
検証
まず本体をビルドします。
$ go build
go-plugin-rpc
というバイナリができました。
認証なし
pluginをauth
という名前でビルドします。
$ cd plugin $ go build -o auth ./no-auth
Host側を実行します。
認証入れてないので常にsuccessです。
$ ./go-plugin-rpc success!!
パスワード認証
pluginをauth
という名前でビルドします。
$ cd plugin $ go build -o auth ./password
Host側を実行します。
正しいパスワードを入力すると通りますが、
$ ./go-plugin-rpc password:hoge success!!
違うパスワードを入力すると通らなくなります。
$ ./go-plugin-rpc password:fuga fail!!
サンプルコード
今回書いたコードはこちらです。
その他
2016年の動画ですがミシェル橋本さんが語っていたプラグイン構想です。
まとめ
Goでのプラグイン実装をしてみました。
中身はnet/rpcを使っていますが、net/rpcはGo 1.8でfreezeされており、go-pluginではそこで課題となっているTLSやmultiplexed connectionなどをサポートしています。
パフォーマンス的にはnet/rpcはgrpcの4倍するとの検証もあるので、まだまだ十分使えるものだと思います。