とってもイケてる香りのする terratest を試してみる  （１）Getting Started

仕事で terratest を試すことになったので、学んだことを書き留めておく。

# terratest とは

[terratest](https://github.com/gruntwork-io/terratest) は　gruntworkが作成した インフラくストラクチャのテストツール。名前からするとおそらくterraform のテストを書くために生まれたようだが、Docker、Packer、Kubernetes、helm にも対応しており、Azure, AWS, GCP の主要なクラウドプロバイダにも対応しており、インフラのテストを書くのならこれでカバーできるんじゃね？という雰囲気が漂っている。本当かどうか試してみよう。

- Testing Terraform code
- Testing Packer templates
- Testing Docker images
- Executing commands on servers over SSH
- Working with AWS APIs
- Working with Azure APIs
- Working with GCP APIs
- Working with Kubernetes APIs
- Testing Helm Charts
- Making HTTP requests
- Running shell commands
- And much more


こちらの動画を見ると、全体像が見れてわかりやすい。

* [How to test infrastructure code](https://www.infoq.com/presentations/automated-testing-terraform-docker-packer/)

# インストール

Go のテストのライブラリとして使うようになっているので、特にインストールなどは必要ない。ただし、terraform を使うなら、事前にterraform がインストールしてある必要があり、PATHの上に存在することが前提条件です。

# Getting Started

Readmeに書いているGetting Started で雰囲気を見てみる。事前準備として、terratest のコードをクローンしておこう。

```bash
$ git clone git@github.com:gruntwork-io/terratest.git
```

テスト用のサンプルを動かしてみよう。

```bash
$ mkidir helloworld
$ cd helloworld
$ mkdir examples
$ mkdir test
```

`examples`には、テスト対象のスクリプトを入れる。クローンしたterratest から一番簡単なサンプルをコピーしておこう。また、`test`には、テストコードをコピーする。

```bash
$ cp ../terratest/examples/terraform-basic-example/* examples
$ cp ../terratest/test/terraform_basic_example_test.go test
```

次に、test ディレクトリに移動して、`go module`を初期化しておく。ちなみに、Go を持っていない人は、[GO: Getting Started](https://golang.org/doc/install)のページからダウンロードしてください。Go Moduleを使うので、1.13 以上にすると良いと思います。私は`go version go1.13.4 linux/amd64` を使っています。

`github.com/TsuyoshiUshio/Spike` の部分は、モジュールをイニシャライズするところです。今回のテストのためのプロジェクトがあるなら、そのリポジトリにしてもいいですが、試したいだけでしたら、他のと被らなそうな github.com の架空のリポジトリにしておけばいいと思います。

```bash
$ cd test
$  go mod init github.com/TsuyoshiUshio/Spike
```

あとは普通にテストを実行します。サンプルにある主要なテストクラスを見てみましょう。とてもシンプルで、

## テストコード

* `terraform.Options` を設定する
* `terraform.InitAndApply` で、Options で定義した内容を variables として渡して`terraform init`, `terraform apply` を実行する
* defer `terraform.Destroy` でテストが終わった後に、`terraform destroy` を実行してリソースを消す

という感じです。気になると思うので、`terraform.Options` で設定できる内容を見てみましょう。試していませんが、リトライを設定できたり、SSHAgentを設定出来たりととてもいい感じですね！


### terraform.Options

```go
type Options struct {
	TerraformBinary          string                 // Name of the binary that will be used
	TerraformDir             string                 // The path to the folder where the Terraform code is defined.
	Vars                     map[string]interface{} // The vars to pass to Terraform commands using the -var option.
	VarFiles                 []string               // The var file paths to pass to Terraform commands using -var-file option.
	Targets                  []string               // The target resources to pass to the terraform command with -target
	EnvVars                  map[string]string      // Environment variables to set when running Terraform
	BackendConfig            map[string]interface{} // The vars to pass to the terraform init command for extra configuration for the backend
	RetryableTerraformErrors map[string]string      // If Terraform apply fails with one of these (transient) errors, retry. The keys are text to look for in the error and the message is what to display to a user if that error is found.
	MaxRetries               int                    // Maximum number of times to retry errors matching RetryableTerraformErrors
	TimeBetweenRetries       time.Duration          // The amount of time to wait between retries
	Upgrade                  bool                   // Whether the -upgrade flag of the terraform init command should be set to true or not
	NoColor                  bool                   // Whether the -no-color flag will be set for any Terraform command or not
	SshAgent                 *ssh.SshAgent          // Overrides local SSH agent with the given in-process agent
	NoStderr                 bool                   // Disable stderr redirection
}
```

### テストクラス

テストクラスの主要ポイントは解説しています。`t.Parrallel()` はテストをパラレル実行するためのメソッドです。ちなみに、terraform に書かれている内容は単純にローカルにファイルを出力するためのものです。

_terraform_basic_example_test.go_

```hcl
package test

import (
	"testing"

	"github.com/gruntwork-io/terratest/modules/terraform"
	"github.com/stretchr/testify/assert"
)

// An example of how to test the simple Terraform module in examples/terraform-basic-example using Terratest.
func TestTerraformBasicExample(t *testing.T) {
	t.Parallel()

	expectedText := "test"
	expectedList := []string{expectedText}
	expectedMap := map[string]string{"expected": expectedText}

	terraformOptions := &terraform.Options{    /// Point 1. Parameter
		// The path to where our Terraform code is located
		TerraformDir: "../examples",

		// Variables to pass to our Terraform code using -var options
		Vars: map[string]interface{}{
			"example": expectedText,

			// We also can see how lists and maps translate between terratest and terraform.
			"example_list": expectedList,
			"example_map":  expectedMap,
		},

		// Variables to pass to our Terraform code using -var-file options
		VarFiles: []string{"varfile.tfvars"},

		// Disable colors in Terraform commands so its easier to parse stdout/stderr
		NoColor: true,
	}

	// At the end of the test, run `terraform destroy` to clean up any resources that were created
	defer terraform.Destroy(t, terraformOptions) // Point 2. Destroy

	// This will run `terraform init` and `terraform apply` and fail the test if there are any errors
	terraform.InitAndApply(t, terraformOptions) // Point 3. Init and Apply

	// Run `terraform output` to get the values of output variables
	actualTextExample := terraform.Output(t, terraformOptions, "example")
	actualTextExample2 := terraform.Output(t, terraformOptions, "example2")
	actualExampleList := terraform.OutputList(t, terraformOptions, "example_list")
	actualExampleMap := terraform.OutputMap(t, terraformOptions, "example_map")

	// Verify we're getting back the outputs we expect
	assert.Equal(t, expectedText, actualTextExample)
	assert.Equal(t, expectedText, actualTextExample2)
	assert.Equal(t, expectedList, actualExampleList)
	assert.Equal(t, expectedMap, actualExampleMap)
}
```
## テスト実行

テストの実行は普通の go のコードとかわりません。ちなみに　
`-v` はテストを実施したときに詳細にレポートするためのフラグで、テストレポートを出したいときは必須です。あと、timeout ですが、通常の go test のタイムアウト時間は10分なので、デプロイ系のコードなら一発で過ぎてしまうので、長めにしています。

```
$ go test -v -timeout 30m
　　　：
--- PASS: TestTerraformBasicExample (4.31s)
PASS
ok      github.com/TsuyoshiUshio/Spike  4.331s
```

# デバッグと CI に必須のバイナリ

先ほどのテストを実施すると、stdoutにたくさんのメッセージがでてきます。これをCIパイプラインで使うとかなると、どうやって、テストレポートを出そうか？と思うかもしれません。そのためのバイナリもこのツールで用意されています。バージョン番号は[Release](https://github.com/gruntwork-io/terratest/releases)で、`terratest_log_parser` のバイナリを持ったバージョンの最新を指定すればよいでしょう。下記のコマンドを実行して、バイナリをどこかPATHが通っているところにおいておけばセットアップ環境です。

```bash
$ curl --location --silent --fail --show-error -o terratest_log_parser https://github.com/gruntwork-io/terratest/releases/download/v0.23.0/terratest_log_parser_linux_amd64
$ chmod +x terratest_log_parser
```

あとは、テスト実行の時に、標準出力をファイルに出力しておいて、`terratest_log_parser` をかますだけです。

```bash
$ go test -timeout 30m | tee test_output.log
$ terratest_log_parser -testlog test_output.log -outputdir test_output
```

これで、ディレクトリができて、ログ、JUnit 形式のXMLファイル、詳細のログが出力されますので、CIでもテスト結果のレポートを表示するのは簡単そうです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/2038ca7c-4b30-6c73-2a6c-e64e024ab8c9.png)

# 今回のまとめ

まずは、基本的なところを理解して、実行してみました。とても簡単にできる印象です。CIも最初から考えてあるし、インストールも簡単なのもうれしいです。

# 次回

次回からは、具体的にクラウドプロバイダーにデプロイするテストや、ベストプラクティスで述べられている内容に関して検証していきたいと思います。