Terraformで複数環境を管理するのに良い構成＋良い書き方とは？【ざっくりと】

# はじめに
こんにちは！masa-asaです。

突然ですが、以前このような記事を投稿しました。

https://qiita.com/masa-asa/items/8edf4c5650c942db948c

この記事は1つの環境にインフラを構築することを想定していますが、dev/stg/prd環境など複数の環境に合わせて名前などを変えたい場合どのような構成にするべきか気になったので、ここにまとめようと思います。

また、その他のベストプラクティスについても、よく使いそうな部分を抜粋して述べようと思います。
今回の内容は主にterraformのスタイルガイドを参考に記述しました。

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


# 複数環境を持つ際のフォルダ構成
以下のような構成で実現できそうです。これはスタイルガイドの「Multiple environments」を参考にしています。

```
├── modules
│   ├── cloud_run
│   │   └── main.tf
│   ├── cloud_storage
│   │   └── main.tf
│   └── variables.tf
├── dev
│   ├── backend.tf
│   ├── main.tf
│   └── variables.tf
│   └── terraform.tfvars
├── prd
│   ├── backend.tf
│   ├── main.tf
│   └── variables.tf
└── stg
    ├── backend.tf
    ├── main.tf
    └── variables.tf
```


### devフォルダについて

構成についてそれぞれの働きを見ていきます。devフォルダの「main.tf」を見てみます。
「main.tf」にはこの環境で実際に作成するリソースが記述されています。ここではmodulesフォルダで定義したリソースをインポートしてその環境（dev）で作成する定義です。
「main.tf」では`env = var.env`という記述をしています。これは、このモジュールに対して「env」という変数を設定するという内容です。

```terraform
module "cloud_storage" {
    source = "../modules/cloud_storage"
    env = var.env
}

module "cloud_run" {
    source = "../modules/cloud_run"
    env = var.env
}
```

devフォルダ内の「variables.tf」には以下のような記述がされています。ここでは「env」という変数を宣言しています。ここで変数宣言することで、「main.tf」でもその変数が使えるようになります。
```terraform
variable "env" {
  description = "Environment name"
  type        = string
  
}
```

「terraform.tfvars」では、具体的な変数の値を設定しています。例のように「env」という変数に「dev」という具体的な値を設定しています。


```terraform
env              = "dev"
```

### modulesフォルダについて

modulesフォルダの中にあるcloud_runの「main.tf」を見てみます。
`name     = "crun-sandbox-${var.env}"` というように、「env」という変数を設定しています。

```terraform
resource "google_cloud_run_v2_service" "default" {
    name     = "crun-sandbox-${var.env}"
    location = "asia-northeast1"
    deletion_protection = false
    ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"

    template {
        max_instance_request_concurrency = 20
        scaling {
        max_instance_count = 1
        }
        containers {
        image = "us-docker.pkg.dev/cloudrun/container/hello"
        resources {
            limits = {
              cpu = "1"
              memory = "512Mi"
                }
            }
        }
    }
}

```
cloud_runの下にある「variables.tf」でも「env」という変数を宣言しています。
これは、cloud runフォルダの中で有効なenvという変数の宣言です。

```terraform
variable "env" {
  description = "Environment name"
  type        = string
  
}
```

### 変数の動作
上記の構成のように、devフォルダとmoduleフォルダそれぞれで変数の宣言をしています。
この際、実際にリソースを作る場合はdevフォルダ内にある「main.tf」に対して`terraform plan`や`terraform apply`コマンドを実行します。
「main.tf」はモジュールとしてmoduleフォルダ内で定義した各リソースを読み込みます。`source`では読み込むフォルダのパス（ここでは相対パス）を示しています。
「env」は、このモジュールで利用する変数です。これは自身で決めた変数を記述します（例えば、「name」や「location」などなど、自身で宣言した変数名をここで用います）。

「env」という変数の値は`var.env`から取得されます。これは、devフォルダ内の「variables.tf」で宣言された変数を示しており、その具体的な値は「terraform.tfvars」で記述された値です。

```terraform
module "cloud_storage" {
    source = "../modules/cloud_storage"
    env = var.env
}

module "cloud_run" {
    source = "../modules/cloud_run"
    env = var.env
}
```

実際にこのterraformが実行されると、記述されたモジュールのパスが読み込まれ、そのフォルダ内のterraformファイルが実行されます。
cloud_runフォルダを例にみると、リソースの定義が「main.tf」で行われおり、そこで使う変数が「variables.tf」で宣言されています。devフォルダの「main.tf」でcloud_runのモジュールが呼ばれると、そこで使う変数がcloud_run側にも渡されて利用されるというイメージです。
そのため、devなどの環境側とモジュール側それぞれで利用する変数を宣言する必要があります。

# そのほかの書き方ベストプラクティス（一部）

## リソース

### リソース名「名詞」を用いる必要があり、リソース名にリソースタイプを用いるべきではない

良い例だとリソース名が「frontend_lb_ip」で悪い例だと「global_address_lb_ip」です。良い例だとこのリソースがどんな役割を持つのか明確でわかりやすい一方、悪い例だと、リソースタイプとリソース名が重複し、このリソースの役割が一目でわかりにくいです。


良い例
```terraform
resource "google_compute_global_address" "frontend_lb_ip" {
  address_type = "EXTERNAL"
  description  = "フロントエンドのCloud Runに対するLBに付与する静的IP"
  name         = "lb_static_ip_sandbox"
}
```

悪い例
```terraform

resource "google_compute_global_address" "global_address_lb_ip" {
  address_type = "EXTERNAL"
  description  = "フロントエンドのCloud Runに対するLBに付与する静的IP"
  name         = "lb_static_ip_sandbox"
}
```

これらの書き方の違いは、リソースの値をほかのリソースで参照する際の記述に着目するとよくわかります。
この例は上記のリソースのidを示すコードですが、悪い例だと「global_address」という文字が連続し長くなってしまううえにこのリソースがどんな役割のものかわかりにくくなってしまっています。

良い例
```terraform
google_compute_global_address.frontend_lb_ip.id
```
悪い例
```terraform
google_compute_global_address.global_address_lb_ip.id
```


### 単語はアンダースコア（_）で区切る・リソースタイプとリソース名はダブルクオーテーション（"）で囲う
リソース名の例「frontend_lb_ip」のように単語はアンダースコアで区切ります（スネークケース）。
また「"google_compute_global_address"」や「"frontend_lb_ip"」のようにリソースタイプとリソース名はダブルクオーテーションで囲むように記述します。

```terraform
resource "google_compute_global_address" "frontend_lb_ip" {
  address_type = "EXTERNAL"
  description  = "フロントエンドのCloud Runに対するLBに付与する静的IP"
  name         = "lb_static_ip_sandbox"
}
```


### 参照するリソースの後に、依存リソースを定義する
複数のリソースがあり、それぞれに依存関係がある場合、初めに「依存される側」のリソースを記述し、後に依存するリソースを書くようにします。
例ではVPCが何にも依存していないリソースであり、これを先に書いています。
サブネットとファイアウォールはそれぞれVPCに依存するので、VPCのリソースの定義の後に記述しています。

```terraform
# VPCネットワークの定義

resource "google_compute_network" "example" {
  name = "example-network"
}

# サブネットの定義（VPCネットワークに依存）
resource "google_compute_subnetwork" "example" {
  name          = "example-subnetwork"
  network       = google_compute_network.example.name

  ip_cidr_range = "10.0.1.0/24"
  region        = "us-central1"
}

# ファイアウォールルールの定義（VPCネットワークに依存）
resource "google_compute_firewall" "example" {
  name    = "example-firewall"
  network = google_compute_network.example.name


  allow {
    protocol = "tcp"
    ports    = ["22"]
  }

  source_ranges = ["0.0.0.0/0"]
}
```

### リソースの順序
リソース記述の順序は以下のように記述するようにします。

1. 存在する場合、`count`と`for_each`のメタ引数を記述
1. リソース固有の非ブロックパラメータ
1. リソース固有のブロックパラメータ
1. ライフサイクルブロック
1. `depends_on`パラメータ


```terraform
resource "google_compute_instance" "example" {
  count        = 2  # 複数のインスタンスを作成

  name         = "example-instance-${count.index}"
  machine_type = "e2-medium"
  zone         = "us-central1-a"

  # 非ブロックパラメータ（例：インスタンスのマシンタイプとゾーン）
  boot_disk {
    initialize_params {
      image = "projects/debian-cloud/global/images/family/debian-10"
    }
  }

  # ブロックパラメータ（例：ネットワークインターフェース設定）
  network_interface {
    subnetwork = google_compute_subnetwork.example.name


    access_config {
      # 外部IPを設定

    }
  }

  # lifecycleブロック（必要に応じて使用）
  lifecycle {
    create_before_destroy = true

  }

  # depends_onパラメータ（必要に応じて使用）
  depends_on = [google_compute_firewall.example]
}
```

## 変数（Variables）

変数の記述については、以下のような順序の構造で記述されることが推奨されます。以下でそれぞれについて説明します。

1. タイプ（Type）
1. 説明（Description）
1. デフォルト値（Default）（オプション） 
1. センシティブ設定（Sensitive） (オプション)
1. バリデーション設定（Validation blocks）


### 変数宣言には、タイプと説明を含む
例のように、変数宣言にはタイプ（type）と説明（description）を含めます。

```terraform
variable "web_instance_count" {
  type        = number
  description = "Number of web instances to deploy. This application requires at least two instances."
}
```

### 必要に応じてデフォルト値を設定する
`default`を設定することで、変数のデフォルト値を定義可能です。

```terraform
variable "web_instance_count" {
  type        = number
  description = "Number of web instances to deploy. This application requires at least two instances."
  default     = 2
}
```

### センシティブな値を扱う際には`sensitive`属性を`true`にする
`sensitive`属性を`true`にするとその変数の値は、`terraform plan`や`terraform apply`に表示されなくなります。ただしterraformファイルには平文で値が記述されていることには注意が必要です。
値を秘匿した管理は別途「secrets management」という機能を使う必要があります。

```terraform
variable "web_instance_count" {
  type        = number
  description = "Number of web instances to deploy. This application requires at least two instances."
  default     = 2
  sensitive   = true
}
```

### 入力バリデーションを使う
`validation`句を用いることで、入力値のバリデーションチェックを行うことができます。`condition`句でバリデーションの具体的な条件を記述し、`error_message`句で違反時のエラーメッセージを記述します。

```terraform
variable "web_instance_count" {
  type        = number
  description = "Number of web instances to deploy. This application requires at least two instances."
  default     = 2
  sensitive   = true
  validation {
    condition = var.web_instance_count >= 1
    error_message = "The number of web instances must be at least 1."
  }
}
```

## アウトプット（Output）
アウトプットは定義したリソースの情報をterraformのほかの部分で参照するように公開する機能です。
アウトプットは以下のような順序の構造で記述されることが推奨されます。

1. 説明（Description）
1. 値（Value）
1. センシティブ設定（Sensitive） (オプション)

### アウトプットには説明と値を含む
`description`句で対象のアウトプットの説明を記述し、`value`句で具体的なアウトプットの値を記述します。

```terraform
resource "google_compute_global_address" "frontend_lb_ip" {
  address_type = "EXTERNAL"
  description  = "フロントエンドのCloud Runに対するLBに付与する静的IP"
  name         = "lb_static_ip_sandbox"
}

output "frontend_lb_ip" {
  description = "value of the global address"
  value = google_compute_global_address.frontend_lb_ip.id
}
```

### センシティブな値を扱う際には`sensitive`属性を`true`にする
`sensitive`属性を`true`にするとその変数の値は、`terraform plan`や`terraform apply`に表示されなくなります。

```terraform
resource "google_compute_global_address" "frontend_lb_ip" {
  address_type = "EXTERNAL"
  description  = "フロントエンドのCloud Runに対するLBに付与する静的IP"
  name         = "lb_static_ip_sandbox"
}

output "frontend_lb_ip" {
  description = "value of the global address"
  value = google_compute_global_address.frontend_lb_ip.id
  sensitive = true
}
```

## ローカル値（Local values）
ローカル値は「main.tf」などのファイル内で共通する値などを持ち再利用するためのローカル変数のような働きをします。設定したローカル値は`${local.location}`のような記述で利用できます。

```terraform
locals {
  location = "asia-northeast1"
}


resource "google_artifact_registry_repository" "docker" {
  location      = "${local.location}"
  repository_id = "ar-repo-poc"
  description   = "example docker repository"
  format        = "DOCKER"


  docker_config {
    immutable_tags = true
  }
}
```

また、ローカル値は変数を用いることもできます。例としては以下のようになります。

```terraform
locals {
  name_suffix = "${var.project_id}_${var.env}"
}
```

複数のファイルからローカル値を参照する場合は、「locals.tf」という名前のファイルにローカル値をまとめることが推奨されます。
リソースなど同一のファイルにローカル値を書く場合は、ローカル値を一番先頭で書くようにします。

## `for_each`と`count`
### `for_each`
`for_each`を用いることで、リスト型やマップ型の変数に対して繰り返し処理を行うことができます。

リソースの定義の初めに`for_each`の値として、マップ型の変数を指定しています。マップの場合は`for_each`に直接、変数を指定できます。
マップ型の変数では、キー/値のペアによる表現がなされており、`for_each`を用いたリソースの中でそれぞれ`each.key`と`each.value`を指定することができます。
この例ではkeyが左辺（repo1やrepo2）に相当し、valueが右辺（Example Docker repository 1 ...）に当たります。
このterraformを実行すると、マップの要素数の回数リソースが作成され、結果的にArtifact Repositoryが4つできます。

```terraform
# マップ型変数の`for_each`例
variable "artifact_repositories" {
  type = map(string)
  default = {
    "repo1" = "Example Docker repository 1"
    "repo2" = "Example Docker repository 2"
    "repo3" = "Example Docker repository 3"
    "repo4" = "Example Docker repository 4"
  }
}

resource "google_artifact_registry_repository" "docker_repos" {
  for_each     = var.artifact_repositories


  location      = "asia-northeast1"
  repository_id = each.key

  description   = each.value

  format        = "DOCKER"

  docker_config {
    immutable_tags = true

  }
}

output "repository_descriptions" {
  value = { for repo in google_artifact_registry_repository.docker_repos : repo.repository_id => repo.description }
}

output "repo1_description" {
  value = google_artifact_registry_repository.docker_repos["repo1"].description

}

```

リスト型の場合、`for_each`に渡す値は「toset()」でリスト型をセット型に変換する必要があります。
セットは重複を削除したユニークな値のみを許す型です。
セット型の場合`each.key`と`each.value`の結果はどちらも要素の値という同じ結果を返します。

```terraform
variable "repository_names" {
  type    = list(string)
  default = ["repo1", "repo2", "repo3", "repo4"]
}

resource "google_artifact_registry_repository" "docker_repos" {
  for_each     = toset(var.repository_names)

  location      = "asia-northeast1"
  repository_id = each.value  # リストの各要素


  description   = "Example Docker repository for ${each.value}"

  format        = "DOCKER"

  docker_config {
    immutable_tags = true

  }
}
```

### count
`count`を使うことで、例のように指定した数だけリソースを作ることが可能になります。number型の変数を用いる場合、`count`の引数には直接変数を指定することができます。あるいは、`count = 3`のように数字を指定することも可能です。
`${count.index}`には繰り返しのインデックス番号が格納されます。0からカウントアップしていく形式です。

```terraform

variable "instance_count" {
  type    = number

  default = 3

}

resource "google_compute_instance" "example" {
  count        = var.instance_count

  name         = "example-instance-${count.index}"
  machine_type = "e2-medium"
  zone         = "us-central1-a"

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-9"
    }
  }

  network_interface {
    network = "default"

    access_config {
      // エフェメラルIPを設定

    }
  }
}
```

リスト型の変数を用いる場合は、`count`の引数で`length()`関数を用いてリストの長さを渡してあげる必要があります。
```terraform
variable "instance_names" {
  type    = list(string)
  default = ["instance-1", "instance-2", "instance-3"]
}

resource "google_compute_instance" "example" {
  count        = length(var.instance_names)  # リストの長さを指定

  name         = var.instance_names[count.index]  # リストからインスタンス名を取得

  machine_type = "e2-medium"
  zone         = "us-central1-a"

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-9"
    }
  }

  network_interface {
    network = "default"

    access_config {
      // エフェメラルIPを設定

    }
  }
}
```

# 終わりに
IaCを実現するためにterraformを書くことが重要になります。今までなんとなくで書いていましたが、環境別に分けて、可読性・保守性の高いコードを書く場合にはしっかりとしたベースの知識が必要になると実感します。
terraformのスタイルガイドは分量が多く、ここですべてを網羅することはできていませんが、コードを書く上で何度も参照することになる重要なものと感じたので、今回書ききれなかった部分もいつか述べることができればと思います。
スタイルガイドのほかにも、公式サイトにはそれぞれの機能の詳細なドキュメントがあるので、それを適宜読み込むことも必要になりそうです！