【Terraform】モジュールに関する個人的ベストプラクティス

こちらは[【Terraform】moduleのアンチパターンとそれに対するベストプラクティス5選 \- Qiita](https://qiita.com/bigwheel/items/2b420183639416b5c6bb)とは違い、特に公式サイト書かれていたわけではないけども僕が数ヶ月Terraformを使って学んだ個人的なベストプラクティス集です。

## リソースへ名前をつけがたいときはthisを使う

あるリソースを宣言するとき、そのリソースがモジュール内で1つしかない場合は名前の付け方に苦労します。

```hcl
resource "aws_ssm_parameter" "parameter" {
  ...
}
```

リソースタイプと似たような名前をつける場合、以下のように以外と選択肢がたくさんあり面倒です。

* `aws_ssm_parameter` リソースタイプの場合
    * parameter
    * ssm_parameter
    * aws_ssm_parameter
    * ssmparameter
    * ssm-parameter
    * ...

またアプリケーション名などその用途で表す方法もあります。

```hcl
resource "aws_ssm_parameter" "for_nice_app" {
  ...
}
```

しかし、この書き方ですとこの部分を他のアプリケーションのtfファイルへコピペするときにいちいちアプリケーション名を書き換える必要があり面倒です。


ベストプラクティスですが固定名 `this` を使いましょう。これは公開モジュールの中でも最もStarの多い[EKSモジュール](https://github.com/terraform-aws-modules/terraform-aws-eks/blob/eaa4d2c6/cluster.tf#L9)や[VPCモジュール](https://github.com/terraform-aws-modules/terraform-aws-vpc/blob/master/main.tf#L29)でも多用されているテクニックです。

※ 当然ですが1つのリソースタイプを2つ以上使用する場合はそれぞれを識別できるよう意味ある名前を与えましょう。 `this` と `that` などとしてはいけません

## 公式のモジュールはフル活用する

[Hashicorp公式に認められたモジュール](https://github.com/terraform-aws-modules)を活用しましょう。
特にVPCモジュールは一般的な用途のほぼすべてをカバーできる柔軟性があります。
また一つ前のベストプラクティスにようにこれらのモジュールは良いモジュール実装をあらゆる面から体現していて自分でモジュールを書くときの参考にになります。

## examples/basicとテストを置く

再利用でき、かつ使いやすいコードにはテストコードと自動化が必須です。
まずモジュールのミニマムの動作確認ができる例をexamples/basic へ置きます([EKSモジュールの例](https://github.com/terraform-aws-modules/terraform-aws-eks/tree/eaa4d2c6/examples/basic))。
またその動作テストが簡単に行えるようterratestやkitchen-terraformなどで自動化します。
以下はterratestのコード例です。

```test/examples_basic_test.go
package test

import (
	"testing"
	"github.com/gruntwork-io/terratest/modules/terraform"
)

func TestExamplesBasic(t *testing.T) {
	terraformOptions := &terraform.Options{
		TerraformDir: "../examples/basic",
		NoColor: true,
	}

	defer terraform.Destroy(t, terraformOptions)
	terraform.InitAndApply(t, terraformOptions)
}
```

```test/go.mod
module placeholder

go 1.14

require (
	github.com/gruntwork-io/terratest v0.26.1
	github.com/stretchr/testify v1.5.1 // indirect
)
```

```test/run-test.sh
#!/usr/bin/env bash
go test -v -timeout 60m
```

## outputもvariableも必要になったときに必要なものだけ追加する

outputの追加は既存のモジュール利用を壊しません。必要になったときに必要な分だけ追加しましょう。
またvariableもdefaultを設定することで同じく既存の利用を壊さず追加することができます。

これはいわゆるYAGNIの法則に沿うためのプラクティスです。

## variable / outputのdescriptionにはリソースのargument / outputへのリンクを積極的に使う

variable / outputは単に1つのリソースのargument / outputとのみ紐付いているケースが結構あります。
そのような場合でもdescriptionを省略するのではなく以下のように紐付いている項目へのリンクを書いてあればモジュールがとても使いやすくなります。

```outputs.tf
output "vpc_id" {
  value       = vpc.this.id
  description = "https://www.terraform.io/docs/providers/aws/r/vpc.html#id"
}
```

## `terraform fmt` を使う

手動で実行するのは不毛なのでエディタで自動実行しましょう。
僕は[hashivim/vim\-terraform](https://github.com/hashivim/vim-terraform)を使っていますがEmacs、vscodeにも同様のものがあると思います。

## 定数を宣言したい場合はlocalを使う

Terraformには定数がありませんが、その用途には `local` を使うことが一般的です([EKSモジュールの例](https://github.com/terraform-aws-modules/terraform-aws-eks/blob/eaa4d2c6/examples/irsa/locals.tf#L1-L5))。
定数代替に `variables` を使うとモジュール利用者が間違って上書きしてしまう可能性があるため `local` を使いましょう。

## outputだけのmoduleも結構有用

ステージング環境へのアクセスを社内のIPアドレスからのみに限定することがよくあります。
ただ、そのIPアドレスを直接tfファイルへ記述しているとシステム数が増えるにつれ管理が大変になります。例えばIPアドレスが変更された場合、たくさんのファイルを書き換える必要が出てきます。
そこでoutputs.tfにオフィスゲートウェイのIPアドレスリストのみ書いたモジュールを定義してそこを参照するようにしたのですがなかなかよかったです。variablesもmain.tfも空っぽなのですが十分有用でした。
このようなモジュールの活用方法もありだと思います。

## 既存のプログラミングパラダイムのベストプラクティス・原則を活用する

terraformのモジュールと関数・メソッド・クラスはかなり似ています。
例えばvariablesを引数、outputsを返り値と考えればモジュールと関数は同一視することができます。

こういったこともあり、良いモジュールの設計には良い関数/メソッド/クラスの設計プラクティスをかなり流用できます。
例えばSOLID則のS・Oはterraformのモジュールへ以下のように適用することができるでしょう[^solid]。

[^solid]: SO以外のLIDについてもモジュールへ適用できないか考えたのですが、この3つはどれもインターフェイス型といったものがないと実現が難しいためそれがないterraformでは適用は困難と判断しました

* SRP：単一責任の原則
    * 良いモジュールは1つの目的のみ持ち複数の目的を持たない
        * Good: EKSとその周辺リソースのみ用意するモジュール
        * Bad: EKSとVPCとDBリソースすべてを用意するモジュール
* OCP：オープン・クローズドの原則
    * 機能を追加・拡張したいときにモジュール内へ手を入れるのではなくモジュールの外側で機能を拡張できるようにしておく
        * Good: ネットワークモジュールのoutputへvpcの情報を適切に設定してモジュール外でもvpc上へリソースを作れるようにしておく
        * Bad: ネットワークモジュールにoutputがなくvpcへリソースを追加する場合ネットワークモジュール内へそれを書く

その他、以下のような基本的なプラクティスもterraformで活用できると思います。

* YAGNIの法則
* DRYの法則
* KISSの法則
* 粗結合なモジュール設計
* 依存関係の単純化・一方向化
* 定数の活用
* 大きすぎるモジュールは分割する(目安として4種類以上がシグナル)