9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Terraform の plugin を理解する

Posted at

この前、Terraform の Azure Provider に機能追加のプルリクエストを送りました。受けれられるといいな。

Screen Shot 2017-09-25 at 2.31.27 AM.png

さて、アプリはできたのですが、一つだけどうやってやっているのかが想像つかない箇所がありました。それは、plugin です。terraform のプラグインは、バイナリを作って

terraform init

とやると読み込まれます。しかも、terraform のプラグインである Azure Provider の方は、明らかに実行用のライブラリです。んーなんで?と思いました。その秘密は、というハシコープで書かれている、go-plugin というプラグインを使っていることでした。基本的な仕組みは、内部で、rpc を使って、クライアントサーバー型になっているという構造になっています。

それだけでは、よくわからないので、サンプルを作って理解してみましょう。

 Terraform っぽいサンプル

サンプルは、Terraform を理解するためなので、Terraformっぽく作っています。

.
├── common
│   └── provider.go
├── main.go
├── plugin
│   ├── azure_provider
│   └── azure_provider.go

こんな感じです。plugin の方が、Azure Provider 相当です。こちらの方も個別でバイナリを作ります。ポイントは、common クライアントとサーバーの両方で使うインターフェイスや struct を置いてあることです。そこからみてみましょう。

common

package provider

import (
	"net/rpc"

	plugin "github.com/hashicorp/go-plugin"
)

type ProviderRPC struct{ client *rpc.Client }

func (p *ProviderRPC) Create(scheme *Scheme) string {
	var resp string
	err := p.client.Call("Plugin.Create", scheme, &resp)
	if err != nil {
		panic(err)
	}
	return resp
}
func (p *ProviderRPC) Update(scheme *Scheme) string {
	var resp string
	err := p.client.Call("Plugin.Update", scheme, &resp)
	if err != nil {
		panic(err)
	}
	return resp
}
func (p *ProviderRPC) Delete(scheme *Scheme) string {
	var resp string
	err := p.client.Call("Plugin.Delete", scheme, &resp)
	if err != nil {
		panic(err)
	}
	return resp
}
func (p *ProviderRPC) Get(id string) *Scheme {
	var resp *Scheme
	err := p.client.Call("Plugin.Get", id, resp)
	if err != nil {
		panic(err)
	}
	return resp
}

type ProviderRPCServer struct {
	Impl Provider
}

func (s *ProviderRPCServer) Create(args *Scheme, resp *string) error {
	*resp = s.Impl.Create(args)
	return nil
}

type Scheme struct {
	Default interface{}
}

type Provider interface {
	Create(scheme *Scheme) string
	Update(scheme *Scheme) string
	Delete(scheme *Scheme) string
	Get(id string) *Scheme
}

type ProviderPlugin struct {
	Impl Provider
}

// Server
func (p *ProviderPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
	return &ProviderRPCServer{Impl: p.Impl}, nil
}

// Client
func (ProviderPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
	return &ProviderRPC{client: c}, nil
}

長々としていますが、ポイントは、シンプルで、Schema という構造体があって、それを rpc で転送します。 Schema は例えば、リソース(Log Analytics とか、 SQL サーバーとか)の定義情報が入っているイメージです。それぞれによって違うので、 interface{} 型にしています。


type Scheme struct {
	Default interface{}
}

type Provider interface {
	Create(scheme *Scheme) string
	Update(scheme *Scheme) string
	Delete(scheme *Scheme) string
	Get(id string) *Scheme
}

type ProviderPlugin struct {
	Impl Provider
}

あとは、Azure だけではなくて、いろんなプロバイダが使えるように、インタフーェイスをきっていますが、ポイントは使えるメソッドは同じにしています。他には、RPC用のクライアントも書いています。もちろん先ほどのスキーマを使ってやりとりをしています。ProviderPlugin 構造体の中で使うようになっています。

type ProviderRPC struct{ client *rpc.Client }

func (p *ProviderRPC) Create(scheme *Scheme) string {
	var resp string
	err := p.client.Call("Plugin.Create", scheme, &resp)
	if err != nil {
		panic(err)
	}
	return resp
}

Provider

最終的にこの場所が呼ばれます。これは、Create の例です。本来ここに、Azure SDK を使って、実際にインフラを作成します。ここでは、Scheme からデータを受け取って、メッセージを表示しています。go-plugin の方では、サンプルが単純なデータのやりとりだけだったので、構造体にしてみました。

type AzureProvider struct {
	logger hclog.Logger
}

func (p *AzureProvider) Create(scheme *provider.Scheme) string {
	p.logger.Debug("Creating Azure Resources ...")
	p.logger.Debug("message: " + scheme.Default.(string))
	return "123"
}

ここは、サーバーが呼ばれると、"azure" というキーがきたら、実行されるようになっています。

func main() {
	logger := hclog.New(&hclog.LoggerOptions{
		Level:      hclog.Trace,
		Output:     os.Stderr,
		JSONFormat: true,
	})
	azureProvider := &AzureProvider{
		logger: logger,
	}
	var pluginMap = map[string]plugin.Plugin{
		"azure": &provider.ProviderPlugin{Impl: azureProvider},
	}
	logger.Debug("Now Azure Provider Serving")

	plugin.Serve(&plugin.ServeConfig{
		HandshakeConfig: handshakeConfig,
		Plugins:         pluginMap,
	})
}

client

プロバイダのコンフィグを書いて、サーバー側と同じ設定にします。

var handshakeConfig = plugin.HandshakeConfig{
	ProtocolVersion:  1,
	MagicCookieKey:   "PROVIDER_PLUGIN",
	MagicCookieValue: "azure",
}

var pluginMap = map[string]plugin.Plugin{
	"azure": &provider.ProviderPlugin{},
}

func main() {
	logger := hclog.New(&hclog.LoggerOptions{
		Name:   "client",
		Output: os.Stdout,
		Level:  hclog.Debug,
	})
	client := plugin.NewClient(&plugin.ClientConfig{
		HandshakeConfig: handshakeConfig,
		Plugins:         pluginMap,
		Cmd:             exec.Command("./plugin/azure_provider"),
		Logger:          logger,
	})
	defer client.Kill()
	rpcClient, err := client.Client()
	if err != nil {
		log.Fatal(err)
	}
	raw, err := rpcClient.Dispense("azure")
	if err != nil {
		log.Fatal(err)
	}
	azureProvider := raw.(provider.Provider)
	message := "Hello"
	scheme := provider.Scheme{
		message,
	}
	fmt.Println(azureProvider.Create(&scheme))
}

一旦、設定をしてしまうと、まるでローカル呼び出しのようにサーバーにアクセスできていますね!
サーバーと言っても、terraform では同じマシンにある前提ですが、それでもとても便利な仕組みです。

fmt.Println(azureProvider.Create(&scheme))

プログラムをコンパイル、実行する

  • go-plugin-sample に全ソースコードを環境付きで置いておきました。

Plugin のコンパイル

$ go build -o ./plugin/azure_provider ./plugin/azure_provider.go

これで、plugin のバイナリができます。

main のコンパイル

$ go build -o terrappoi .

プラグインとは全く個別に、別の環境としてコンパイルして、バイナリができます。
実行してみましょう。

実行結果

バッチリ、RPC 通信して、うまくデータもやりとりできているのがわかりますね!すごくいい感じ!

invincible:go-plugin-sample ushio$ ./terrappoi
2017-09-25T02:25:43.516-0700 [DEBUG] client: starting plugin: path=./plugin/azure_provider args=[./plugin/azure_provider]
2017-09-25T02:25:43.521-0700 [DEBUG] client: waiting for RPC address: path=./plugin/azure_provider
2017-09-25T02:25:43.531-0700 [DEBUG] client.azure_provider: Now Azure Provider Serving: timestamp=2017-09-25T02:25:43.530-0700
2017-09-25T02:25:43.532-0700 [DEBUG] client.azure_provider: plugin address: timestamp=2017-09-25T02:25:43.532-0700 address=/var/folders/5b/rt6_lb397_3d92vp67g98pgr0000gn/T/plugin663688024 network=unix
2017-09-25T02:25:43.534-0700 [DEBUG] client.azure_provider: Creating Azure Resources ...: timestamp=2017-09-25T02:25:43.534-0700
2017-09-25T02:25:43.534-0700 [DEBUG] client.azure_provider: message: Hello: timestamp=2017-09-25T02:25:43.534-0700
123
2017-09-25T02:25:43.537-0700 [DEBUG] client: plugin process exited: path=./plugin/azure_provider

ハマったところ

最初下記のようなエラーが出ていました。

 ./terrappoi
2017-09-24T02:56:32.393-0700 [DEBUG] plugin: starting plugin: path=./plugin/azure_provider args=[./plugin/azure_provider]
2017-09-24T02:56:32.397-0700 [DEBUG] plugin: waiting for RPC address: path=./plugin/azure_provider
2017-09-24T02:56:32.406-0700 [DEBUG] plugin.azure_provider: Now Azure Provider Serving: timestamp=2017-09-24T02:56:32.406-0700
2017-09-24T02:56:32.406-0700 [DEBUG] plugin.azure_provider: plugin address: timestamp=2017-09-24T02:56:32.406-0700 address=/var/folders/5b/rt6_lb397_3d92vp67g98pgr0000gn/T/plugin930697090 network=unix
2017-09-24T02:56:32.410-0700 [DEBUG] plugin: plugin process exited: path=./plugin/azure_provider
panic: gob: local interface type *interface {} can only be decoded from remote interface type; received concrete type Scheme

goroutine 1 [running]:
github.com/TsuyoshiUshio/go-plugin-sample/common.(*ProviderRPC).Create(0xc420214140, 0x14548c0, 0xc42021c460, 0xc42021c460, 0xc420214140)
        /Users/ushio/Codes/rpc/src/github.com/TsuyoshiUshio/go-plugin-sample/common/provider.go:15 +0xcc
main.main()
        /Users/ushio/Codes/rpc/src/github.com/TsuyoshiUshio/go-plugin-sample/main.go:50 +0x4fd

特にこの部分。

panic: gob: local interface type *interface {} can only be decoded from remote interface type; received concrete type Scheme

なんだこれ?という感じですが、内部で、gob というパッケージを使っていますが、その gob は、シリアライズ、デシリアライズをします。その際に、型があっていないというエラーです。結構、型は完全一致しなくてもいいのですが、特定の時に、エラーが出るようです。下記のサイトがおすすめ。

Resource

上記以外

9
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?