Terraform の provider を試しにつくってみたときのTipsと苦労話をまとめてみました。
前提
今回つくったのは deploygate provider です。
まだまだ、作成途中ですが、アプリへのテスターの招待と削除の機能は実装しました。
これを作るにあたって、 APIクライアントとしてこちらを使わせていただきました。
参考にさせていただいたサイト
- 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:のパラメータだけを設定すれば、動くのでわりと簡単に動くからです。
func dataSourceAppCollaborator() *schema.Resource {
return &schema.Resource{
Read: dataSourceAppCollaboratorRead,
これが、resourceになると、 Read:/Create:/Update:/Delete:のパラメータが必要になり、それぞれにメソッドを作る必要があります。
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
のオプションがつかないと実行されないみたいです。
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 の項目を依存関係に入れておくといいと思います。
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>
にないと実行できないみたいです。
ちなみに、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}
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
することが可能です。
"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{}
にキャストして、目的の値を取得するという流れになります。
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のエラーが出て、どこを修正すればいいのか検討がつきませんでした。
var usersList string
usersList = d.Get("users").(string)
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 を記述して、環境変数で値を設定してテストしました。
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"),
),
},
},
})
}
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
}
`
# 環境変数で設定
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 するようにしてみます。
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
を設定します。
$ export TF_LOG=DEBUG
$ terraform init
$ terraform plan
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
こんな感じで、ログが出てくれます。