terraform providerを作ったときのTips


Terraform の provider を試しにつくってみたときのTipsと苦労話をまとめてみました。

## 前提

今回つくったのは deploygate provider です。
まだまだ、作成途中ですが、アプリへのテスターの招待と削除の機能は実装しました。

- https://github.com/fnaoto/terraform-provider-deploygate

これを作るにあたって、 APIクライアントとしてこちらを使わせていただきました。

- https://github.com/recruit-mp/go-deploygate

参考にさせていただいたサイト

- https://learn.hashicorp.com/tutorials/terraform/provider-setup
- https://github.com/hashicorp/terraform-provider-aws
- https://github.com/jmatsu/terraform-provider-slack
- https://github.com/wizaplace/terraform-provider-algolia
- https://febc-yamamoto.hatenablog.jp/entry/terraform-custom-provider-01
- https://febc-yamamoto.hatenablog.jp/entry/terraform-custom-provider-02
- https://medium.com/spaceapetech/creating-a-terraform-provider-part-1-ed12884e06d7

## Tipsと苦労話

### どこから作ればいいのか分からない

基本的には、providerから作って、data、resourceの流れがいいと思います。
なぜ、`dataから作る`のかというと、以下のように、Read:のパラメータだけを設定すれば、動くのでわりと簡単に動くからです。

```golang

func dataSourceAppCollaborator() *schema.Resource {
	return &schema.Resource{
		Read: dataSourceAppCollaboratorRead,
```

これが、resourceになると、 Read:/Create:/Update:/Delete:のパラメータが必要になり、それぞれにメソッドを作る必要があります。

```golang

func resourceAppCollaborator() *schema.Resource {
	return &schema.Resource{
		Read:   resourceAppCollaboratorRead,
		Create: resourceAppCollaboratorCreate,
		Update: resourceAppCollaboratorUpdate,
		Delete: resourceAppCollaboratorDelete,
```

ちなみに、provider は main.go と provider.go だけあれば動くので、一旦それをつくって make isntall をすればバイナリは作成可能です。 `terraform providers schema -json` などで providerが使用可能なのを確認できます。

### テストが動かない

たとえば、こういうテストが書かれていた場合、 `go test` ではテストが実行されません。
これは、`TF_ACC=1`のオプションがつかないと実行されないみたいです。

```golang

package deploygate

import (
	"fmt"
	"testing"

	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)

func Test_DataSourceAppCollaborator_basic(t *testing.T) {
	resource.ParallelTest(t, resource.TestCase{
		PreCheck:  func() { Test_DGPreCheck(t) },
		Providers: testDGProviders,
		Steps: []resource.TestStep{
			{
				Config: testDataSourceAppCollaboratorConfig,
				Check: resource.ComposeTestCheckFunc(
					testDataSourceAppCollaborator("data.deploygate_app_collaborator.current"),
				),
			},
		},
	})
}
```

Makefileにも `test` と `testacc` が用意されていました。
ちなみに、 `testacc` は provider のバイナリを使ってテストを実行しているみたいです。
なので、テスト前に install の項目を依存関係に入れておくといいと思います。

```make

test: 
	go test -i $(TEST) || exit 1                                                   
	echo $(TEST) | xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4                    

testacc: install
	TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 120m  
```

### バイナリが更新されない

`make install` を実行しても、terraform から provider を呼び出せないことがありました。
terraform provider は `~/.terraform.d/plugins/<OS>_<ARCH>` にないと実行できないみたいです。

- https://www.terraform.io/docs/extend/how-terraform-works.html#discovery

ちなみに、Makefileはこんな感じだったので、修正しました。

```makefile:修正前

install: build
	mkdir -p ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH}
	mv ${BINARY} ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH}
```

```makefile:修正後

install: build
	mkdir -p ~/.terraform.d/plugins/${OS_ARCH}
	mv ${BINARY} ~/.terraform.d/plugins/${OS_ARCH}
```

### `schema.TypeSet` で設定したstateの値を取得できない

`schema.Resource` で `Schema:` を設定することで、 `terraform.tfstate` にjsonの形式で状態を保存できるのですが、保存した値を取得するときに一工夫必要です。

とくに、 `schema.TypeSet` で設定した値を取得する場合は以下のようになります。

まず、 `users` は `name` と `role` から構成されている struct型の構造体リストです。
これをterraformで使える形にキャストすることが必要です。

構造体がリストの場合、 `schema.TypeSet` を使えば、そのまま `Set` することが可能です。

```go:設定
			"users": {
				Type:     schema.TypeSet,
				Required: true,
				Elem: &schema.Resource{
					Schema: map[string]*schema.Schema{
						"name": {
							Type:     schema.TypeString,
							Required: true,
						},
						"role": {
							Type:     schema.TypeInt,
							Optional: true,
						},
					},
				},
			},
```

`GetOk` を使って、取得可能かを確認後に、 `(*schema.Set).List()` で値を取り出します。
listで取り出した値を `map[string]insterface{}` にキャストして、目的の値を取得するという流れになります。

```go:取得方法

	var usersList string

	if v, ok := d.GetOk("users"); ok {
		for _, element := range v.(*schema.Set).List() {
			elem := element.(map[string]interface{})
			usersList += elem["name"].(string) + ","
		}
	}
```

最初、以下のようなGRPCのエラーが出て、どこを修正すればいいのか検討がつきませんでした。

```go

var usersList string
usersList = d.Get("users").(string)
```

```bash:エラー
Error: rpc error: code = Unavailable desc = transport is closing        
panic: interface conversion: interface {} is *schema.Set, not string
```

### Configのテストで variables が渡せない

`resource.TestStep`でConfigを渡してあげて、テストするんですが、variablesに値を渡せず苦労していました。
今回は、 Config側に variables を記述して、環境変数で値を設定してテストしました。

```go

func Test_DataSourceAppCollaborator_basic(t *testing.T) {
	resource.ParallelTest(t, resource.TestCase{
		PreCheck:  func() { Test_DGPreCheck(t) },
		Providers: testDGProviders,
		Steps: []resource.TestStep{
			{
				Config: testDataSourceAppCollaboratorConfig, // ここ
				Check: resource.ComposeTestCheckFunc(
					testDataSourceAppCollaborator("data.deploygate_app_collaborator.current"),
				),
			},
		},
	})
}
```

```go:修正後

const testDataSourceAppCollaboratorConfig = `
data "deploygate_app_collaborator" "current" {
	owner    = var.owner
	platform = var.platform
	app_id   = var.app_id
}

# variablesを追加

variable "platform" {
  type = string
}

variable "app_id" {
  type = string
}

variable "owner" {
  type = string
}
`
```

```bash:.env
# 環境変数で設定
export TF_VAR_app_id=""
export TF_VAR_owner=""
export TF_VAR_platform=""
export TF_VAR_add_user_name=""
```

### terraform 実行時の `crash.log` にログを残したい

通常時はログは残さなくていいですが、 terraform で crash が発生したときにデバッグ用にログが残れせると嬉しいなと思って、 `log.Printf` を使ってログを残せるようにしました。

試しにこんな感じで、必ず crash するようにしてみます。

```go
func providerConfigure(p *schema.Provider) schema.ConfigureFunc {
	return func(d *schema.ResourceData) (interface{}, error) {
		log.Printf("[DEBUG] ログ出しますよー: %s", "おけまる水産")
		var err error
		return nil, err
```

terraform 実行時に `TF_LOG=DEBUG` を設定します。

```bash
$ export TF_LOG=DEBUG
$ terraform init
$ terraform plan
```

```bash:crash.log

2020/12/22 15:16:24 [TRACE] GRPCProvider: Configure
2020-12-22T15:16:24.662+0900 [DEBUG] plugin.terraform-provider-deploygate: 
2020/12/22 15:16:24 [DEBUG] ログ出しますよー: おけまる水産 #わかりやすいように改行してます
2020/12/22 15:16:24 [TRACE] [walkRefresh] Exiting eval tree: provider.deploygate
```

こんな感じで、ログが出てくれます。