hashicorp/go-pluginを試しかけたメモ
GolangでPlugin機構を使ってみたくなった。
Golangは1binaryが利点の1つだと思っているけど、
本体とPluginを分離することによりPlugin開発速度と本体の堅牢さの両立を
維持したいケースもあると思います。
例えばmackerelio/mackerel-agent-pluginsなんかは良い例。
他にPlugin機構を持っているケースとしてHashicorpプロダクトが思い浮かんだので
ちょっと調べたところ hashicorp/go-pluginというライブラリを見つけたので触ってみた。
go-pluginはTerraformで使われている。
ちなみにHashicorpのPluginに関する話としてmitchellさんの動画がある。
2016 Kickoff - Hashicorp & Go Plugin Architecture
今年の2月のものだけど、これまでのHashicorpのPluginに対する開発歴史や今後も語られていて面白い。
今のTerraformがv5で、今後はよりSecureかつユーザフレンドりな要素を入れたv6を作って
VaultでもユーザがPluginを作れるようにしたいそう。
ということで、hashicorp/go-plugin/exampleを試してみる。
このexampleは単体で動くようになっているので、ビルドして実行すれば動く
$ go build main.go $ ./main Hello!
内部的にはmain()
の中から再度mainバイナリをpluginとして起動すると
mainPlugin()
がPlugin側として動作するものらしい。
しかしこれだとPluginの恩恵が感じられないので本体とPluginが分離した状態で動かしたい。
もっとも簡単にやるならmain.goを複製してplugin.goを作成し、
mainPlugin()をmain()にして不要なimport文を削れば動く・・・のだが
それでは意味がないので試行錯誤した。
なお、完全に理解したとは言い難い(特に言語的に)のでご容赦ください。
最終的にした構成はmain.go
plugin1.go
plugin/plugin.go
plugin/greeter_plugin.go
plugin/plugin.go
package plugin import ( "os/exec" "github.com/hashicorp/go-plugin" ) var handshakeConfig = plugin.HandshakeConfig{ ProtocolVersion: 1, MagicCookieKey: "BASIC_PLUGIN", MagicCookieValue: "hello", } func NewClient(cmd string) (c *plugin.Client) { return plugin.NewClient(&plugin.ClientConfig{ HandshakeConfig: handshakeConfig, Plugins: pluginMap, Cmd: exec.Command(cmd, "plugin"), }) } // pluginMap is the map of plugins we can dispense. var pluginMap = map[string]plugin.Plugin{ "greeter": &GreeterPlugin{}, } func Serve(g Greeter) { plugin.Serve(&plugin.ServeConfig{ HandshakeConfig: handshakeConfig, Plugins: PluginMap(g), }) } func PluginMap(g Greeter) map[string]plugin.Plugin { return map[string]plugin.Plugin{ "greeter": &GreeterPlugin{PluginFunc: g}, } }
本体とPlugin双方で使うHandshake、pluginMapの部分をまとめている。
NewClientのexec.Commandの部分はPluginパスを起動元から受けて実行する形にした。
本体側の入力に応じて実行するPluginを変えることもできるし、
hashicorp/go-plugin
にDiscoveryの仕組みも入っているので色々できそう。
Serve()はPlugin側で定義したものを受け取ってセットする。
元はここがPlugin用に定義してるものを直接読んでいたために、最後まで一番ハマった。
雑に直した感あってベターなのかよくわからない。
plugin/greeter_plugin.go
package plugin import ( "net/rpc" "github.com/hashicorp/go-plugin" ) // Greeter is the interface that we're exposing as a plugin. type Greeter interface { Greet() string } type GreeterPlugin struct{ PluginFunc Greeter } func (p *GreeterPlugin) Server(*plugin.MuxBroker) (interface{}, error) { return &GreeterRPCServer{Impl: p.PluginFunc}, nil } func (GreeterPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { return &GreeterRPC{client: c}, nil } // Here is an implementation that talks over RPC type GreeterRPC struct{ client *rpc.Client } func (g *GreeterRPC) Greet() string { var resp string err := g.client.Call("Plugin.Greet", new(interface{}), &resp) if err != nil { // You usually want your interfaces to return errors. If they don't, // there isn't much other choice here. panic(err) } return resp } // Here is the RPC server that GreeterRPC talks to, conforming to // the requirements of net/rpc type GreeterRPCServer struct { // This is the real implementation Impl Greeter } func (s *GreeterRPCServer) Greet(args interface{}, resp *string) error { *resp = s.Impl.Greet() return nil }
Greeter interfaceに関わる部分を集約。
Plugin側で定義したコマンドを受け取るためにGreeterPluginに要素生やして
GreeterRPCServerに渡している。
本来はPluginとのI/F定義が集約されるので一番大事なのだが、
まだ理解しきってないので今後の課題です・・・
plugin1.go
package main import ( "github.com/a3no/go-plugin-example/plugin" ) func main() { plugin.Serve(new(GreeterHello)) } // Here is a real implementation of Greeter type GreeterHello struct{} func (GreeterHello) Greet() string { return "Hello!Hello!!" }
plugin起動時にGreeter interfaceの実体を渡している。
main.go
package main import ( "fmt" "io/ioutil" "log" "github.com/a3no/go-plugin-example/plugin" ) func main() { // We don't want to see the plugin logs. log.SetOutput(ioutil.Discard) // We're a host! Start by launching the plugin process. client := plugin.NewClient("./plugin1") defer client.Kill() // Connect via RPC rpcClient, err := client.Client() if err != nil { log.Fatal(err) } // Request the plugin raw, err := rpcClient.Dispense("greeter") if err != nil { log.Fatal(err) } // We should have a Greeter now! This feels like a normal interface // implementation but is in fact over an RPC connection. greeter := raw.(plugin.Greeter) fmt.Println(greeter.Greet()) }
ラップしたNewClientを呼ぶように変えたくらいで元のmain()と大差なし
これでそれぞれビルドして動かせば動く
$ go get github.com/a3no/go-plugin-example/plugin $ go build plugin1.go $ go run main.go Hello!Hello!!
なんとか分割したもののPluginとのI/Fを自在に定義できるようにならないと
柔軟には使えないと思うのでもうちょいどうにかしたい。