公式チュートリアルには載っていなかったので、自作Terraform Providerを作るときのユニットテストの書き方をメモしておく。
なお、最初にコメントしておくと今回の記事はかなり説明を省略しているので各Providerにコミットしたことがあるか自作Providerを作った人じゃないとわからなそう…
TL;DR
- TerraformはProvider経由で各種サービスのリソースを操作する
- SDKを使えば自作Providerを作ることも可能
- 公式ガイドに載っていないが、
schema.Resource#TestResourceData
メソッドを使うとテストが書きやすい - ユニットテストを書けば動作確認も簡単にできるので開発がはかどる!
// schema.Resource.ReadContextに設定する関数のテスト
func Test_dataSourceYourObjectRead(t *testing.T) {
// *schema.ProviderData.ConfigureContextFuncの戻り値にしているオブジェクト
m := providerConfigureReturnedObject
// *schema.Provider.DataSourcesMapに設定するResource
d := dataSourceYourObject().TestResourceData()
got := dataSourceYourObjectRead(context.TODO(), d, m)
if diff := cmp.Diff(got, tt.want); diff != "" {
t.Errorf("dataSourceYourObjectRead: (-got +want)\n%s", diff)
}
}
Terraform Providerの概要
Terraformはプロパイダーを経由して各種リソースを操作する。 AWSプロパイダーやGCPプロパイダー経由でマネージドサービスを管理している方も多いだろう。
TerraformはIaaS以外もプロパイダーさえあれば宣言的に管理できる。 公式で提供されているプロパイダーだけでもこれだけある。
また、Goが書けるならばSDKを使って自作のプロパイダーを作成し、レジストリに公開することもできる、
自作プロパイダーは次の公式ガイドを読みながら実装することができる。
日本語資料がよいならソフトウェアデザイン11月号の「作品で魅せるGoプログラミング」に解説が載っている。
自作Provider実装時の動作確認のめんどくささ
ホビープロジェクトとしてTerraformの自作Providerを作っている。
Providerはterraform
コマンドが呼び出すサププロセスとなる。
そのため、作るのは簡単だが動作確認がめんどくさい。
さきほど紹介した資料ではどちらにも一度ビルドしてTerraform経由で起動する動作確認方法が記載されている。
$ pwd
/Users/budougumi0617/go/src/github.com/budougumi0617/terraform-provider-hashicups
# 一度ビルドする
$ make install
go build -o terraform-provider-hashicups
mv terraform-provider-hashicups ~/.terraform.d/plugins/hashicorp.com/edu/hashicups/0.2/darwin_amd64
# tfファイルを用意したディレクトリに移動する
$ cd examples
# ビルドしたら必ずinitしないといけない
$ terraform init
# ここまでやってやっとapplyして動作確認ができる
$ terraform apply --auto-approve
この方法だと毎回ビルドをしたあと異なるディレクトリでterraform init && terraform apply
をする必要があり時間もかかる。
また、デバッガのアタッチも難しい。そのためユニットテストで動作確認をしたい。
自作Providerのユニットテストを書く
自作Providerの実装構造
Terraformで自作プロバイダーを実装する流れはざっくり次の通り。
- データあるいはリソースの構造を定義する
- データあるいはリソースに対して、CRUD操作にひとつひとつ対応する関数を実装する
CRUD操作に対応するそれぞれの関数は次のシグネチャを持つ関数として実装する。
// See Resource documentation.
type CreateContextFunc func(context.Context, *ResourceData, interface{}) diag.Diagnostics
// See Resource documentation.
type ReadContextFunc func(context.Context, *ResourceData, interface{}) diag.Diagnostics
// See Resource documentation.
type UpdateContextFunc func(context.Context, *ResourceData, interface{}) diag.Diagnostics
// See Resource documentation.
type DeleteContextFunc func(context.Context, *ResourceData, interface{}) diag.Diagnostics
(つまり同じシグネチャなのだが、)この関数の引数にわたすオブジェクトを用意すれば各XxxxContextFunc
に対応するCRUD操作のユニットテストが実行できる。
テスト対象の自作データの定義
今回テストの対象になるデータリソースの定義を確認する。
import (
"context"
"fmt"
pixela "github.com/ebc-2in2crc/pixela4go"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
func dataSourceGraphs() *schema.Resource {
return &schema.Resource{
ReadContext: dataSourceGraphsRead,
Schema: map[string]*schema.Schema{
"graphs": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Computed: true,
},
"name": {
Type: schema.TypeString,
Computed: true,
},
// ...
},
},
},
},
}
}
// この関数をテストしたい
func dataSourceGraphsRead(
ctx context.Context, d *schema.ResourceData, m interface{},
) diag.Diagnostics {
client := m.(*pixela.Client)
var diags diag.Diagnostics
// Do anything...
return diags
}
また、ProviderのConfigureContextFunc
に登録している関数は次の通り。
import (
"context"
"fmt"
pixela "github.com/ebc-2in2crc/pixela4go"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
func providerConfigure(
_ context.Context, d *schema.ResourceData,
) (interface{}, diag.Diagnostics) {
un := d.Get("username").(string)
if un == "" {
return nil, diag.FromErr(fmt.Errorf("not find username"))
}
token := d.Get("token").(string)
if token == "" {
return nil, diag.FromErr(fmt.Errorf("not find token"))
}
return pixela.New(un, token), diag.Diagnostics{}
}
dataSourceGraphsRead
関数をテストするには、*schema.ResourceData
オブジェクトを入手する必要がある。
*schema.ResourceData
オブジェクトは、dataSourceGraphs
関数の戻り値である*schema.Resource
オブジェクトから入手することができる。
func (*Resource) TestResourceData
func (r *Resource) TestResourceData() *ResourceData
TestResourceData Yields a ResourceData filled with this resource’s schema for use in unit testingTODO: May be able to be removed with the above ResourceData function.
第1引数のcontextは適当なcontextでよく、第3引数はConfigureContextFunc
に登録するproviderConfigure
関数の戻り値で良い。
今回の場合だと第3引数はproviderConfigure
関数が返す*pixela.Client
オブジェクトになる。
これらを踏まえると、次のようにtestを書くことができる。
package pixela
import (
"context"
"os"
"testing"
pixela "github.com/ebc-2in2crc/pixela4go"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
)
func Test_dataSourceGraphsRead(t *testing.T) {
tests := [...]struct {
name string
want diag.Diagnostics
}{
{
name: "confirmClientResponse",
want: nil,
},
}
for _, tt := range tests {
usename := os.Getenv("PIXELA_USERNAME")
token := os.Getenv("PIXELA_TOKEN")
if usename == "" || token == "" {
t.SkipNow()
}
t.Run(tt.name, func(t *testing.T) {
m := pixela.New(usename, token)
d := dataSourceGraphs().TestResourceData()
got := dataSourceGraphsRead(context.TODO(), d, m)
if diff := cmp.Diff(got, tt.want); diff != "" {
t.Errorf("dataSourceGraphsRead: (-got +want)\n%s", diff)
}
})
}
}
これでunittestとして動作確認ができるようになった。 また、デバッガを接続して動作確認もできるので、データの流れ方も確認することができる。
終わりに
前提知識の補足をほとんど書いていないが、terraformのProviderのユニットテストを書く方法をまとめた。
terraform-provider-awsなどを確認すると、もっとテスト用の関数を多用しているので、他にもどんなテストが書けるか読んでおきたい。
参考
- https://github.com/hashicorp/terraform-plugin-sdk
- Providers - Terraform by Hashicorp
- Call APIs with Terraform Providers | Terraform - Hashicorp Learn
- https://github.com/hashicorp/terraform-provider-aws