Quantcast
Channel: プラグイン - Carpe Diem
Viewing all articles
Browse latest Browse all 3

Go言語でプラグイン機構【RPC編】

$
0
0

概要

オブジェクト指向についてClean Architectureでは

OOとは「ポリモーフィズムを使用することで、システムにあるすべてのソースコードの依存関係を絶対的に制御する能力」である。

これにより、アーキテクトは「プラグインアーキテクチャ」を作成できる。これは、上位レベルの方針を含んだモジュールを下位レベルの詳細を含んだモジュールから独立させることである

と説明されています。

このようにソフトウェアの拡張性を持たせるためにプラグイン機構を用意することは非常に大切です。

今回はhashicorpのgo-pluginを使って実現してみます。

プラグインの利用箇所

プラグイン機構にすることでメリットがありそうなケースってなんだろう?と思って簡単にいくつか挙げてみます。

こんなところでしょうか?
fluentdのようなETLツールはプラグイン開発が非常に活発ですね。
プラグイン機構にすることで自分は本体の堅牢さ・パフォーマンスに注力し、プラグイン開発は他の開発者に作ってもらってスピード開発といった両立が可能になります。

go-plugin

アーキテクチャ

以下のようになっています。

f:id:quoll00:20190702235026p:plain

図に示している通り、Host側がクライアント、Plugin側がサーバです。

フローとしては以下です。

  1. HostがPluginをサブプロセスとして起動
  2. Pluginはサーバを起動してコネクトするのに必要な情報をHostに返す
  3. 受け取った情報を元にコネクションを貼る
  4. HostからRPCメソッドを呼び出す
  5. Pluginは↑のレスポンスを返す
  6. 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!!

サンプルコード

今回書いたコードはこちらです。

github.com

その他

2016年の動画ですがミシェル橋本さんが語っていたプラグイン構想です。

www.youtube.com

まとめ

Goでのプラグイン実装をしてみました。
中身はnet/rpcを使っていますが、net/rpcはGo 1.8でfreezeされており、go-pluginではそこで課題となっているTLSやmultiplexed connectionなどをサポートしています。
パフォーマンス的にはnet/rpcはgrpcの4倍するとの検証もあるので、まだまだ十分使えるものだと思います。

ソース


Viewing all articles
Browse latest Browse all 3

Latest Images

Trending Articles





Latest Images