Terraformの運用Tips

# 目的
Terraformを使いこなす！を目標に、学びとTipsを整理します。

# 環境
``` sh
$ terraform -version
Terraform v1.9.6
on linux_amd64
```

# Tips
## Terraformで変数を扱う
環境変数に`TF_VAR_`プレフィックスをつけた変数を登録することで、`variable`に値を渡すことができます。
``` sh
$ printenv |grep TF_VAR
TF_VAR_your_secret=yyyyyy
```
``` variables.tf
# 環境変数から`your_secret`に値を渡すことができる。
variable "your_secret" {
  type        = string
}
```

## terraform_remote_stateとその管理を考える
terraform_remote_state を使うと、他のモジュールの状態ファイルから情報を参照できます。

https://developer.hashicorp.com/terraform/language/state/remote-state-data

モジュールが多くなり依存関係の管理が複雑になる場合、ディレクトリ構成を工夫することで煩雑さを解消できそうです。

`module3`が他のモジュールの出力値を参照したい場合の例で考えてみます。
`remote_state`で各モジュールの出力値を一元管理します。すると、参照する状態ファイルは固定できるため、依存関係が整理しやすくなります。

``` sh
$ tree
.
├── module1
│   ├── backend.tf
│   └── data.tf
├── module2
│   ├── backend.tf
│   └── data.tf
├── module3
│   ├── backend.tf
│   └── data.tf
└── remote_state
    ├── backend.tf
    ├── data.tf
    └── outputs.tf
```

`module1`で`remote_state`に出力値を登録します。
``` module/module1/data.tf
# module1配下のtfstateで出力値を管理する
variable "bucket" {
  type = string
}
data "terraform_remote_state" "module1" {
  backend = "gcs"
  config = {
    bucket = var.bucket
    prefix = "module1"
  }
}
output "module1_value" {
  value = "module1_value"
}
```
remote_stateフォルダで各モジュールからの出力値を受け取ります。
``` module/remote_state/data.tf
# module1とmodule2の出力値にアクセス

variable "bucket" {
  type = string
}

data "terraform_remote_state" "module1" {
  backend = "gcs"
  config = {
    prefix = "module1"
    bucket = var.bucket
  }
}

data "terraform_remote_state" "module2" {
  backend = "gcs"
  config = {
    prefix = "module2"
    bucket = var.bucket
  }
}

```
remote_stateフォルダ配下の`outputs.tf`で、再度各モジュールから受け取った値を出力します。
``` module/remote_state/outputs.tf
# 各モジュールの出力値を一元管理する
output "terraform_remote_state_output_all" {
  value = {
    # module1の出力値
    module_1_output = data.terraform_remote_state.module1.outputs.module1_value
    # module1の出力値
    module_2_output = data.terraform_remote_state.module2.outputs.module2_value
  }
}
```
出力値を受け取りたい`module3`は`remote_state`で管理されている値を参照します。
``` module/module3/data.tf
variable "bucket" {
  type = string
}

# remote_stateで管理されている状態ファイルにアクセス
data "terraform_remote_state" "env" {
  backend = "gcs"
  config = {
    prefix = "remote_state"
    bucket = var.bucket
  }
}
output "module1_value_used_in_module3" {
  value = data.terraform_remote_state.env.outputs.terraform_remote_state_output_all.module_1_output
}
```

## Custom Validation Rules
> 特定の変数のカスタム検証ルールを指定できます
:::note warn 
この機能は、Terraform CLI v0.13.0 で導入されました。
:::

インフラリソースが特定の値の範囲や形式を必要とする場合に、事前検証する上で役立ちます。
値に`dev`か`prd`が含まれないとエラーを出力するケースを例にとります。
シンプルなコーディング規約のように使えそうです。

``` variables.tf
variable "environment" {
  validation {
    condition     = contains(["dev", "prod"], var.environment)
    error_message = "The environment must be dev or prod."
  }
}
```
``` sh
$ terraform apply
var.environment
  Enter a value: a
╷
│ Error: Invalid value for variable
│ 
│   on variables.tf line 1:
│    1: variable "environment" {
│     ├────────────────
│     │ var.environment is "a"
│ 
│ The environment must be dev or prod. 
│ 
│ This was checked by the validation rule at variables.tf:2,3-13.
```

## TFLint
TFlintとはTerraformのためのLinterで、構文やパラメータがルールに違反していないかをチェックしてくれるツールです。
開発ルールを明確化し適用することで、潜在的な問題の早期発見や、ルールに則った規則正しいコードの品質を担保できるメリットがあります。

### インストール
``` sh
curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
```

### .tflint.hclファイル
```.tflint.hcl
# Terraformプロジェクトルートに本ファイルを作成する
config {
}

plugin "google" {
  enabled = true
  version = "0.27.1"
  source  = "github.com/terraform-linters/tflint-ruleset-google"
}

# variableブロックやoutputブロックはvariables.tfやoutputs.tfに定義する
rule "terraform_standard_module_structure" {
  enabled = true
}

# プロバイダのバージョン記載を必須とする
rule "terraform_required_version" {
  enabled = true
}
```
上記のテストを次の構成のTerraformプロジェクトについて実行してみます。
```sh
$ tree
.
└── storage
    ├── main.tf
    ├── provider.tf
    └── variables.tf
```

### 結果

すると2件のワーニングから次のことがわかります。
- `provider "random"`が`terraform_required_providers`制約に抵触している
- `outputs.tf` fileが存在しないことにより、`terraform_standard_module_structure`制約に抵触している

```sh
$ tflint
2 issue(s) found:

Warning: Missing version constraint for provider "random" in `required_providers` (terraform_required_providers)

  on main.tf line 1:
   1: resource "random_id" "default" {

Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.10.0/docs/rules/terraform_required_providers.md

Warning: Module should include an empty outputs.tf file (terraform_standard_module_structure)

  on outputs.tf line 1:
   (source code not available)

Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.10.0/docs/rules/terraform_standard_module_structure.md
```

### 修正

outputs.tfファイルの追加とprovides.tfファイルを修正することでワーニングは消えます。
```sh
$ tree
.
└── storage
    ├── main.tf
    ├── provider.tf
    ├── outputs.tf # add
    └── variables.tf
```

``` provider.tf
terraform {
  required_version = "1.7.5"
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "5.24.0"
    }
  }
  # add
  required_providers {
    random = {
      source  = "hashicorp/random"
      version = "3.6.3"
    }
  }
}
```

## Terraform test
> Terraform テストを使用すると、作成者はモジュール構成の更新によって重大な変更が発生しないことを検証できます。
:::note warn 
このテスト フレームワークは、Terraform v1.6.0 以降で使用できます。
:::

### 構文
> Terraform は、ファイル拡張子: `.tftest.hcl`または`.tftest.json`に基づいてテストファイルを検出します.
> 各テストファイルには次の属性とブロックが含まれます
> - 1 つ以上の run ブロック 
> - variables ブロック (省略可)
> - provider ブロック (省略可)

### 早速やってみる!
``` sh
$ tree
.
├── storage.tf
└── storage.tftest.hcl
```

``` storage.tf

resource "google_storage_bucket" "test_bucket" {
  name     = "example-bucket-2"
  location = "ASIA"
  project  = var.project

  storage_class = "MULTI_REGIONAL"
  versioning {
    enabled = true
  }
  uniform_bucket_level_access = true
  public_access_prevention    = "enforced"
}

```

``` storage.tftest.hcl
run "check_gcs_object_name" {
  command = plan

  assert {
    condition     = google_storage_bucket_object.test_bucket.name == "example-bucket"
    error_message = "Object name is not example-bucket"
  }
}

run "check_bucket_location" {
  command = plan

  assert {
        condition     = google_storage_bucket.main.location == "ASIA-NORTHEAST1"
        error_message = "Bucket location is not ASIA-NORTHEAST1"
  }
}
```

### 結果
期待通りのエラーメッセージが発生していますね！
- "Object name is not example-bucket"
- "Bucket location is not ASIA-NORTHEAST1"



```
$ terraform test
storage.tftest.hcl... in progress
  run "check_gcs_object_name"... fail
╷
│ Error: Test assertion failed
│ 
│   on storage.tftest.hcl line 5, in run "check_gcs_object_name":
│    5:     condition     = google_storage_bucket.test_bucket.name == "example-bucket"
│     ├────────────────
│     │ google_storage_bucket.test_bucket.name is "example-bucket-2"
│ 
│ Object name is not example-bucket
╵
  run "check_bucket_location"... fail
╷
│ Error: Test assertion failed
│ 
│   on storage.tftest.hcl line 14, in run "check_bucket_location":
│   14:     condition     = google_storage_bucket.test_bucket.location == "ASIA-NORTHEAST1"
│     ├────────────────
│     │ google_storage_bucket.test_bucket.location is "ASIA"
│ 
│ Bucket location is not ASIA-NORTHEAST1
╵
storage.tftest.hcl... tearing down
storage.tftest.hcl... fail

Failure! 0 passed, 2 failed.
```

### TFlintとTerraform testの整理

| 特徴              | TFLint                                                                                   | Terraform Test                                                               |
|-------------------|-----------------------------------------------------------------------------------------|----------------------------------------------------------------------------|
| **目的**          | コードの静的解析（リソース作成前にコード品質を確認）                                            | モジュールの動作確認やリソース作成後のテスト                                             |
| **タイミング**     | 開発中やコードレビュー時の静的解析                                                        | リソース適用後や CI/CD のテストプロセスで使用                                      |
| **チェック対象**   | - Terraform の構文エラー<br>- ベストプラクティス違反<br>- プロバイダー固有のエラー                    | - モジュールやリソースの期待される出力のテスト<br>- 実際に作成されたリソースの動作確認                  |
| **主な利用シーン** | - コードの初期開発<br>- プルリクエストレビュー<br>- チーム標準の設定適用                             | - 新規モジュールの動作検証<br>- リソース変更の影響確認<br>- CI/CD のリリーステスト前確認 |

#### 利用シーンをもう少し深堀して考えてみる
個人的な整理ですが、次のような観点で整理してみると、それぞれが効果を発揮してくれる利用シーンが見えてきそうです。
- TFLint
    - **規模の大きなチーム開発**
        - コードスタイルを統一し、レビュー負担を減らす
    - **チーム内で議論して規約を決定**
        - 強制力のある規約は、チーム全体で議論し合意を得たものにする
    -  **要不要を整理**
        -  規約を段階的に分け、必須事項のみエラーとして扱い、推奨・任意は警告または情報として表示するなど対応を分ける

- Terraform test
    - **モジュールの再利用性が高い場合**
        - 頻繁に変更が加えられる部分を対象とする
        - 変更が既存の挙動を壊さないことをテストする
    - **リスクの高いリソース変更がある場合（クリティカルパス）**
        - VPCやセキュリティグループのように、ミスがシステム全体に影響を及ぼすリソースについて重点的にテストする


## 最後に
Terraformの運用についてはまだまだ勉強中です。
今後も実践で得られた学びを発信していけたらと思います。



## リファレンスとスペシャルサンクス
執筆にあたり数多くのサイトを参考にさせてもらいました。ありがとうございました。

https://developer.hashicorp.com/terraform/language/state/remote-state-data

https://developer.hashicorp.com/terraform/language/tests

https://github.com/terraform-linters/tflint

https://zenn.dev/erueru_tech/articles/a32fead5669031

https://dev.classmethod.jp/articles/integrate-terraform-remote-state-hub/