実際に運用して得られたTerraformのノウハウをまとめる

# Terraformのノウハウをまとめる

以下の公式のドキュメントが充実しているため、基本的にはこれに従うようにし、個人的に得たノウハウを記載します。
Terraformの使い方やコードの書き方など、基礎的な内容は省略します。
https://developer.hashicorp.com/terraform/language

なお、Terraform Cloudは使ったことがないため、これTerraform Cloud使った方がいいよというものがあるかもしれません。

## 基本的なディレクトリ構成

構築するシステム構成に合わせて好きに考えてよいですが、再利用性や可読性を考えてモジュール化は行った方がよいです。
ただし、モジュールが不必要に増えすぎると構成が複雑になるため、適度に意味のある単位でモジュール化を行うのが望ましいです。
だいたいの場合は、以下のような構成にするのが無難だと考えます。

```text
/
├─ doc/
├─ environment/
│  ├─ prod/
│  │  └─ network/
│  │      └─ main.tf
│  ├─ staging/
│  └─ develop/
└─ modules/
　 ├─ network/
　 │  ├─ Readme.md
　 │  └─ main.tf
　 └─ user/
```

## モジュール関連
### モジュールのファイル構成

参考：https://developer.hashicorp.com/terraform/language/modules/develop/structure

コードの他に、モジュールを説明するReadme、モジュールへのインプット（変数定義）、モジュールのアウトプットの3ファイルがほぼ必須です。
公式ではLICENSEファイルも置いておくことを推奨しています。
また、必ず作成する`terraform`ブロックも別のファイルに外出ししておいた方が管理しやすいです。
モジュールによってはmain.tfの1ファイルにすべてを記載すると可読性が悪くなってしまう場合もあるため、これは適宜分割するのもよいと思います。
以上より、基本的なモジュールのファイル構成は以下のようになります。

```text
modules/
└ network/
　 ├─ Readme.md
　 ├─ variables.tf
　 ├─ outputs.tf
　 ├─ versions.tf  # terraformブロックを記載する
　 ├─ main.tf
　 ├─ subnet.tf  # main.tfだけではコードの見通しが悪くなる場合は適度に分割
　 └─ LICENSE
```

### モジュールの変数定義

参考：https://developer.hashicorp.com/terraform/language/values/variables

`variables.tf`にモジュールへのインプットとして与えられる変数を定義します。
必須ではありませんが、descriptionとtypeは省略せずに適切に指定しておくことが望ましいです。
基本的に変更することを想定していない変数やオプションとしたい変数にはdefaultを指定しておくとよいです。
なお、可読性などのためにモジュール内でのみ使用したい変数はvariableではなく、localsを使います。

### モジュールの変数に接頭辞のパラメータを用意する

複数人で並行して作業する場合にオススメの方法です。
S3バケットなど、リソースによっては重複した名前で作成できないものがあるため、複数人で同時に作業しようとすると競合してしまう問題が発生します。
そこで、重複した名前が許されないリソースには任意の接頭辞を付けるように実装しておきます。
各々で任意の文字列を`name_prefix`に指定しておけば、他の作業者のことを気にせずに作業を行えるようになります。
ブルーグリーンデプロイメントのように、一時的にリソースを2セット作成したい場合などにも有効です。

`variables.tf`に以下を定義

```hcl
variable "name_prefix" {
  description = "作成するリソースの名前に付与する接頭辞"
  type        = string
}
```

`main.tf`に記載するリソースでは以下のようにパラメータを指定しておく。

```hcl
resource "aws_s3_bucket" "foo" {
  bucket        = "${var.name_prefix}-foo-files"
}
```

### モジュールのアウトプット

参考：https://developer.hashicorp.com/terraform/language/values/outputs

主に他のモジュールから参照したいパラメータや、実行後に値を確認したいパラメータをアウトプットに記載します。
必須ではありませんが、変数と同様にdescriptionは適切に指定しておくことが望ましいです。

なお、モジュールを使用する側の方のtfファイル（本記事ではenvironment以下に作成するtfファイル）で以下のように記載するとモジュールで定義しているアウトプットをまとめてすべてアウトプットできます。

```hcl
output "network" {
  description = "networkモジュールのOutput"
  value       = module.network
}
```

### モジュール内のterraformブロック

古いバージョンのTerraformやプロバイダでは実行できない可能性があるため、現在使用しているバージョンより新しいバージョンでのみ実行を許可するように定義しておくのがよいです。
モジュールごとにバラバラでも問題はないと思いますが、なるべく統一した方が混乱しにくいです。

```hcl
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.38.0"
    }
  }
  required_version = ">= 1.7.4"
}
```

### モジュールのReadme

参考：[https://devops-blog.virtualtech.jp/entry/20230310/1678416395](https://devops-blog.virtualtech.jp/entry/20230310/1678416395)

[terraform-docs](https://terraform-docs.io/)を利用すると、コードを解析して自動的に「作成されるリソース」「変数」「アウトプット」などの情報を埋め込んだファイルを生成できます。
モジュールの目的や注意事項なども記述しておくのが望ましいですが、変数やアウトプットのdescriptionをしっかりと書いておけば、これだけでも最低限のReadmeとして使えると思います。

また、Terraformには`terraform graph`というコードの関係をグラフとして出力してくれる機能があるため、これもReadmeに載せると便利そうに思えますが、実際に出力してみるとかなり視認性の悪いグラフが出てきます。
これを改良したようなツールはいくつか公開されており、個人的には[terraform-graph-beautifier](https://github.com/pcasteran/terraform-graph-beautifier)がオススメです。
用途にもよると思いますが、個人的には以下のように`local`, `var`, `output`を除外したグラフがリソースの関連だけ見られてオススメです。

`terraform graph | terraform-graph-beautifier --output-type=graphviz --embed-modules=false --exclude="local\\." --exclude="var\\." --exclude="output\\." | dot -Tpng > ../../../doc/images/$(basename $(pwd)).png`

この2つのツールを使用するだけで、簡易的にかなりの情報量を含んだReadmeを生成できます。


## 運用関連
### パラメータを外出しする

通常はenvironment内の各ディレクトリの`main.tf`に各モジュールの変数へ与える値を直接記載しますが、多くの場合に複数の`main.tf`で共通にしたい値が存在すると思われるため、設定ファイルを外出しすると便利です。
`main.tf`内で`yamldecode`関数や`jsonencode`関数を使うと、これを実現できます。

```hcl
locals {
  config = yamldecode(file("../config.yaml"))
}

module "network" {
  source = "../../../modules/network"
  az     = local.config.az
  tags   = local.config.tags
}
```

```yaml
az: "ap-northeast-1c"
tags:
  Project: "hoge"
  TerraformVersion: "1.7.4"
  Environment: "staging"
```

※上記は、以下のようなディレクトリ構成を想定しています。

```text
/
├─ environment/
│  ├─ prod/
│  │  ├─ config.yaml
│  │  └─ network/
│  │      └─ main.tf  # 上記の内容のtfファイル
└─ modules/
　 ├─ network/
　 └─ user/
```

### セキュアなパラメータを扱う

コードをGit管理したいが、設定値にIPやパスワードなどのセキュアな情報が含まれていて、その部分はGitに含めたくないといった場合があります。
このような場合は以下のような方法が考えられます。

- 環境変数で定義する
- [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-parameter-store.html)のようなクラウド上で管理できる場所にパラメータを定義しておき、そこから取得した値を使用するコードにしておく
- [SOPSを利用する](https://kdotdev.com/kdotdev/how-to-use-sops)
  - TerraformでSOPSを利用する場合の参考記事: [https://zenn.dev/himekoh/articles/202209041050](https://zenn.dev/himekoh/articles/202209041050)

SOPSはPGPや各種クラウドのキーマネジメントサービスのキーなどを使用してファイルの暗号化/複合を行うツールです。
これを利用するとGitには暗号化したファイルが置かれるため、通常通りにGit管理を行うことができます。
パラメータの用途や運用によって適した方法は異なるため、適宜使い分けるとよいと思います。

### initを効率化する

参考：https://developer.hashicorp.com/terraform/cli/config/config-file

通常、`terraform init`を実行すると実行したディレクトリ毎にプロバイダがダウンロードされます。
たとえば、awsプロバイダを使用するモジュールを実行するディレクトリが3箇所存在する場合、3箇所に同じawsプロバイダがダウンロードされることになります。
意図的にこの仕様を利用したい場合を除いて、プロバイダは共通のディレクトリにダウンロードし、そこを参照して利用するように設定しておくのがオススメです。

### 効率的なバージョンアップ対応

environmentやmodules内にバージョンを記載しているため、すべての値を書き換える必要があります。
sedで置換してもいいですが、[tfupdate](https://github.com/minamijoyo/tfupdate)を使って以下のようなスクリプトを実行するのがわかりやすくて簡単です。
このために`terraform`ブロックを別のファイルに外出ししておくことを個人的にオススメします。

```bash
TERRAFORM_VERSION=1.7.4
AWS_PROVIDER_VERSION=5.38.0

cd environment
find -name versions.tf | while read line; do
    tfupdate terraform -v ${TERRAFORM_VERSION} ${line}
    tfupdate provider aws -v ${AWS_PROVIDER_VERSION} ${line}
done

cd modules
find -name versions.tf | while read line; do
    tfupdate terraform -v ">= ${TERRAFORM_VERSION}" ${line}
    tfupdate provider aws -v ">= ${AWS_PROVIDER_VERSION}" ${line}
done
```

### コードの静的解析

スペースの個数や僅かな書き方の違いで差分が出るのは煩わしいので、共通のルールを作っておくのがいいです。
可能であれば、GitHub Actionsなどで検知を自動化しておくと実行し忘れ防止や手間の削減になります。
後から導入すると修正対応が面倒になるため、なるべく初期から導入しておく方が楽です。

- シンプルなフォーマット整形
  - リポジトリのトップで`terraform fmt -recursive`を実行する
    - コードの見た目をキレイに整えてくれる
    - 動作への影響は無い
- 不正なパラメータの検知
  - [tflint](https://github.com/terraform-linters/tflint/releases)
    - EC2インスタンスで存在しないインスタンスタイプを指定しているなど、書き方としては問題ないが実行するとエラーになるパラメータを検知してくれる
- 非推奨な設定で記載されているリソースの検知
  - [tfsec](https://github.com/aquasecurity/tfsec/releases)
    - S3をパブリック公開する設定になっているなど、実行は可能だがセキュリティ的に懸念点のあるリソースを検知してくれる

### モジュール依存関係の可視化（Terragrunt）

参考：https://terragrunt.gruntwork.io/docs/features/execute-terraform-commands-on-multiple-modules-at-once/#dependencies-between-modules

モジュールを作成していくと、「networkモジュールでVPC周りを作成する→作成したネットワークにeksモジュールでEKS周りを作成する」のように依存関係のあるモジュールが増えていきます。
同一モジュール内でのリソース同士の依存は自動的に適切な順序で作成してくれますが、モジュールの場合は実行時に依存関係を考慮しながら実行しなければなりません。
順序を間違えると中途半端にリソースが作られてしまったり、古い状態のアウトプットを参照してしまうおそれがあります。
このような依存関係の問題は、[Terragrunt](https://terragrunt.gruntwork.io/)を使用するとだいぶ解消されます。
ただし、本気でTerragruntを使おうとすると仕組みやルールが複雑になってしまって危険な雰囲気がしたので、ここでは依存関係の可視化を行うだけのシンプルな使い方を紹介しておきます。
この使い方だけであれば、既存のTerraformのみでの環境にも気軽に導入できると思います。

```text
/
├─ environment/
│  ├─ prod/
│  │  ├─ config.yaml
│  │  ├─ terragrunt.hcl  # ①
│  │  ├─ network/
│  │  │   └─ 省略
│  │  └─ eks/  # networkモジュールに依存したモジュール
│  │      ├─ main.tf
│  │      ├─ terragrunt.hcl  # ②
│  │      └─ その他省略
└─ modules/
　 ├─ network/
　 └─ eks/
```

上記は、以下の構成を仮定したディレクトリ構成になっています。

- networkモジュールとeksモジュールの2つのモジュールが存在する
- eksモジュールはnetworkモジュールに依存しており、先にnetworkモジュールを実行する必要がある

`config.yaml`にはtfstateを保管するS3バケットの情報などを定義しておきます。

```yaml
backend_s3:
  bucket: "hoge-staging-terraform-tfstate"  # tfstateを格納するS3バケット
  region: "ap-northeast-1"                  # tfstateを格納するS3バケットのリージョン
  folder: "prod"                            # バケットに作成するフォルダ
```

①の`terragrunt.hcl`ファイルには、参照したいすべてのtfstateファイルを読み込むように以下のような記載をしておきます。
`key`の部分は環境に合わせて適切に書き換えてください。
ここでは`prod`という名前のフォルダに`{モジュールのディレクトリ名}.tfstate`という名前で各モジュールのtfstateを保管するルールになっていると仮定しています。

```hcl
# Terragrunt root file

locals {
  config = yamldecode(file("config.yaml"))
}

remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite"
  }
  config = {
    bucket  = local.config.backend_s3.bucket
    region  = local.config.backend_s3.region
    key     = "${local.config.backend_s3.folder}/${path_relative_to_include()}.tfstate"
    encrypt = true
  }
}
```

②の`terragrunt.hcl`ファイルには、依存しているモジュールのディレクトリのパスを`dependencies`に定義しておきます。

```hcl
include "root" {
  path = find_in_parent_folders()
}

dependencies {
  paths = ["../network"]
}
```

この状態で`prod`ディレクトリで`terragrunt graph-dependencies | dot -Tsvg > graph.svg`のようなコマンドを実行すると、以下の
モジュール依存関係のグラフが出力されます。

![dependency_eks_network.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116209/74b19948-f1d9-2f1c-110a-bf0996a7c912.png)

なお、依存しているモジュールの数が多くてすべて正確に記載すると複雑になりすぎてしまう場合は適宜間引いて定義することを検討してみるのもよいです。
例えば、aモジュールにbモジュールが依存、aとbモジュールにcモジュールが依存となっている場合、cがaに依存しているかどうかに関係なくa→b→cという依存関係になるため、c→aの部分は省略しても順序的には問題ありません。

![abc.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116209/122621ca-47cb-3b4b-2a58-f90401a11c74.jpeg)