Terraformまとめ

クラウドインフラの構成管理をするデファクトスタンダードとしての地位を確立しているTerraform。
この記事は、以下についてまとめたものである。

- Terraformの概要
    - 動作イメージ
    - 利用フロー
- Terraformの利用
    - ローカル環境の構築方法
    - Terraformのディレクトリ・ファイル構成
    - .tfファイルの記述方法
    - terraformコマンド(主要なもの)
    - 公式ドキュメント(参照することが多そうなもの)

(TODOになっているところも今後更新していく予定です)

# 1. Terraformの概要
- ``Terraform``は、HashiCorp社（Vagrant, Vault)によってGo言語で開発されたOSSで、Infratructure as Codeを実現するツール。
- **AWS,GCP,Azureの他、DatadogやHerokuなど様々なクラウドインフラの構成管理ができる**。
    - 類似ツールに`CloudFormation`があるが、こちらはAWSのインフラリソースのみが対象
    - ちなみに、クラウドインフラ以外にローカルのDocker環境の構成管理もできる
- **インフラ構成の定義は、.tfという拡張子のファイルに、各インフラのリソースの定義をコードで記述する**ことによって行う。
    - 具体的には、各インフラの初期化、更新、削除といった内容を記述する。
    - .tfファイルに記述されたコードに基づいてTerraformが自動でインフラを構築してくれる。
        - 利用者は、インフラリソースの構築・設定順を気にする必要はあまりなく、インフラリソース間の依存関係は基本的にTerraform側でいい感じに考慮して制御してくれる。
        - 冪等性の担保もしてくれて、コードとして記述している内容が**対象のインフラリソースですでに実現されている場合、その構築処理をスキップしてくれる。**
- Terraformの主な構成要素は、``TERRAFORM CORE`` と ``TERRAFORM PROVIDER``である。

## 1.1. 動作イメージ
```
[.tfファイル]
↓
↓
↓
---------------- Terraform ------------------
[TERRAFORM CORE]
↓
↓(gRPC)
↓
[TERRAFORM PROVIDER]
↓
↓(Golang)
↓
[各社クラウドインフラが提供するSDK(API)]
---------------------------------------------
↓
↓(HTTP(s))
↓
[各社クラウドインフラのリソース]
```

**TERRAFORM PROVIDER**
- HassiCorp社とTerraformコミュニティによってGolangで開発されているプラグインである。以下単にプロバイダと呼ぶ。
    - [Terraform Registy](https://registry.terraform.io/)で公開されている
- .tfファイルの設定に基づき、各社クラウドから提供されているAPI, SDKを利用していい感じに各社クラウドインフラリソースを操作してくれるものである。
- TERRAFORM PROVIDERは、ローカルでgRPCサーバとして動作する。

## 1.2. 利用フロー
Terraformを利用して、インフラリソースの構築・設定反映をする工程は、以下の通り。

- **①インフラリソースの設定ファイル(.tf)を記述**
- **②設定ファイルの検証**
    - ``terraform validate``コマンドを実行する
- **③作業用ディレクトリの初期化**
    - ``terraform init``コマンドを実行する
    →設定ファイルのインフラリソース定義の反映に必要な分のプロバイダがDLされる
- **④インフラリソースへ設定を適用するための具体的な操作を確認**
    - ``terraform plan``コマンドを実行する
    →「クラウドリソースに対して、Terraformがこれからどんな操作をするのか？」の内容が表示されるので、問題ないかを確認する
- **⑤インフラリソースへ設定を適用**
    - ``teffaform apply``コマンドを実行する

# 2. Terraformの利用
## 2.1. ローカル環境構築
[公式ドキュメント](https://developer.hashicorp.com/terraform/tutorials/gcp-get-started/install-cli#install-terraform)を参考に、以下をインストール。
- Terraform(※)
- terraformコマンド補完機能

※terraformのバージョンを管理しながら使いたいならば、[tfenv](https://github.com/tfutils/tfenv)経由でインストールして利用する。

```
# tfenvをインストール
brew install tfenv

# インストール可能なterraformのバージョンを確認
tfenv list-remote

# terraformをバージョンを指定してインストール
tfenv install <バージョン>

# インストール済みのterraformのバージョンを一覧表示(*付きが現在使用しているバージョン)
tfenv list

# 使用するterraformのバージョンを切り替え
tfenv use <バージョン>
```

## 2.2. Terraformのディレクトリ・ファイル構成
**設定ファイル(``.tf`` or ``.tf.json``)群を同じディレクトリにまとめたもの**を、**モジュール**という。

- モジュールは、基本的にディレクトリ直下の設定ファイル群のみで構成される。
- サブディレクトリは完全に別のモジュールとして扱われる
    - **モジュール呼び出し**という仕組みを使わない限り、自動的に読み込まれたりすることはない。

![サブディレクトリの扱い.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59542/afef3af5-9e67-10c7-4d76-c704f507f8bd.png)

- Terraformはモジュールのディレクトリ直下の設定ファイル内の記述すべてを評価し、それが1つの設定ファイルであるかのように動作する。
    - ブロックを別ファイルにわけるのは、純粋に読む人・メンテナンスする人にとってわかりやすくするためだけで、動作には全く影響しない。
- Terraformは単一の**ルートモジュール**を起点として動作する。
    - Terraform CLIでは、Terraformを起動する作業用ディレクトリ = ルートモジュール。
        - コマンドライン引数で、別ディレクトリをルートモジュールにすることもできるが、実際にはほぼやらないらしい。


## 2.3. .tfファイルの記述方法
- 文字コード: UTF-8
- 改行コード: 慣習的にLF(CRLFでも可)
- コメント: `#`で一行コメントを記述

### 基本的な構造、構文と用語

```example.tf
# --- ブロック ----
<ブロックタイプ> "<ラベル>" "<ラベル>" {
  # --- ボディ ---
  ...
  # 引数
  <引数名> = <値> or <式>
  # --- ボディここまで ---
}
# --- ブロックここまで ---

```

- ブロック: 設定内容のコンテナであり、ブロックタイプ, 任意の数のラベル, ボディ(`{}`で囲まれる部分。引数、他のブロックを含む)から構成される。 
- 引数: 引数名に「値」もしくは「式」を紐付ける形で記述する。
- 式: 他の値を参照したり、組み合わせたりするもの。

例えば、以下のような形になる。
```example.tf
resource "aws_vpc" "main" {
  cidr_block = var.base_cidr_block
}
```

### 2.3.1. ブロックタイプ
#### `terraform`ブロック
Terraform自体の基本的な設定をするブロック。
モジュールに必要なプロバイダ(`required_providers`ブロック)や、tfstateの置き場(`backend`ブロック)をボディに記述する。

例. 
```example.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 1.0.4"
    }
  }
  backend "gcs" {
    bucket  = "tf-state-prod"
    prefix  = "terraform/state"
  }
}
```
##### `required_providers`ブロック
モジュールで利用する全てのプロバイダを列挙するブロック。
```sample.tf
required_providers {
  <プロバイダのローカル名> = {
    source = "<プロバイダのソースアドレス>",
    version = "<プロバイダのバージョン>"
  }
}
```
プロバイダのローカル名は`provider`ブロックで使われる。

##### `backend`ブロック
ステートファイル(.tfstate)の保存方法(バックエンド)を設定するブロック。
```
backend "<バックエンドタイプ>" {
  <バックエンドタイプ固有の設定>
}
```
ステートファイルは、その名の通りTerraformの管理対象であるインフラ構成の状態を保存するもの。
`backend`ブロックで指定できるバックエンドタイプや、その固有の設定については、使用するTerraformバージョンの[リファレンス](https://developer.hashicorp.com/terraform/language/settings/backends/configuration)を参照する必要がある。例えば`gcs`バックエンドタイプでは、以下のような設定をする。
```
terraform {
  backend "gcs" {
    bucket  = "tfstate" # 保存先のGCSバケット名
    prefix  = "terraform/production" # <保存先のバケット名以下のディレクトリ名>/<tfstateファイル名>
  }
}
```

#### valiable
TODO

#### `provider`ブロック
プロバイダを使って、各種インフラリソースの詳細設定をするにあたっての、前提条件を設定するブロック。
```sample.tf
provider <プロバイダのローカル名> {
  <プロバイダ固有の設定>
}
```

ボディにはプロバイダ固有の設定を記述する。
例えば`google`プロバイダだと`project, region`という固有の設定がある。
```sample.tf
provider "google" {
  # resourceブロックで設定するインフラリソースが所属するGoogle Cloudプロジェクトのデフォルト値
  project = "acme-app" 
  # resourceブロックで設定するインフラリソースが所属するGoogle Cloudリージョンのデフォルト値
  region  = "us-central1" 
}
```
ボディで設定できる項目は、各プロバイダのリファレンスを参照する必要がある。

#### `resource`ブロック
仮想ネットワークなどのインフラリソースの設定をするブロック。
```sample.tf
resource "<リソースタイプ>" "<リソースのローカル名>" { # リソースタイプとローカル名の組み合わせはモジュール内で一意
    <リソースタイプ固有の設定>
}

```
対象のリソース(1番目のラベル)に対する詳細設定を記述(ボディ)し、任意のローカル名(2番目のラベル)を与える。
指定できるリソースタイプは、`required_providers`ブロックで設定したプロバイダから提供されるもの。
ローカル名はモジュール内で一意である必要があり、そのモジュール内だけで有効である。
ボディで設定できる項目は、`provider`ブロック同様、各プロバイダのリファレンスを参照する必要がある。

##### lifecycle.ignore_changes
resource ブロック内の特定の項目について、差分を無視する設定ができる。
ここを設定した状態で、terraform plan や terraform apply を実行すると、実際には対象項目について差分が発生する場合も no changes と表示されて実行が終了する。が、tfstateには変更後の状態がちゃんと記録される。
「差分は出ないでほしいが、実際には変わっていてほしい」というケースが稀にあるが、そんな場合は terraform show で確認。

不安になったらば


#### `module`ブロック
「複数のインフラリソースを束ねるコンテナ(=module)」の設定をするブロック。
moduleは、

#### data
TODO

### 2.3.2. データ型

大区分|小区分|型名|概要
---|---|---|---
プリミティブ|-|string|文字列型
プリミティブ|-|number|数値型。整数も小数も扱える。
プリミティブ|-|bool|ブーリアン型
複合|Collection|list| 配列の型。<br>配列の全ての要素が同じ型でなければならない。<br>明示的に宣言する場合は `list(<値の型>)`とする。
複合|Collection|set|ユニークな値の集合。<br>全ての要素が同じ型でなければならない。<br>インデックスやキーを持たず、並び順の概念がない。
複合型|Collection|map|キーバリューを伴う型。<br>全てのバリューが同じ型でなければならない。<br>`{ "<キー>": <値>, ... }`<br>or<br>`{ <キー> = <値>, ... }`<br>という形で生成できる。<br>ただし、数値から始まるキーの場合は、前者の引用符でくくる方でなければならない。
複合|Structural|tuple|配列の型。<br>各要素の型は違っていてよい。<br>明示的に型を宣言する場合は、<br>`tuple([<値の型>, <値の型>, ...])`<br>という形で、要素数・要素の型の並び順ともにきっちり決まっていなければならない。
複合|Structural|object|キーとバリューを伴う型。<br>値の型はそれぞれ別でよい。<br>明示的に型を宣言する場合は、<br>`object( { <キー>=<値の型>, <キー>=<値の型> } )`<br>という形で行う。


**Collection型とStructural型の違い**
Collection型は全ての値が同じ型出ないとダメ、Structural型はそうでなくてよい。

#### 複合型の選択(所感)
以下のようなフローで判断すればよさそう。
- 計算量の観点から、利用する際に制約を満たすことができるのであれば、計算量の観点から`set`を選択する
- setの制約を満たさないのであれば配列型はlistとtupleを、 キーとバリューを伴う型はmapとobjectを「値の型が全て同じであるかないか」で選択する

**参考**
[Types and Values(公式ドキュメント)](https://developer.hashicorp.com/terraform/language/expressions/types)
[Type Constraints(公式ドキュメント)](https://developer.hashicorp.com/terraform/language/expressions/type-constraints)


### 2.3.3. 繰り返し処理
#### 2.3.3.1. for_each
類似の内容の`resource` or `data` or `module`ブロックを別々に記述したくないときに使う。
`for_each` に`map` or `set(string)`型のデータを与えて繰り返し処理する。

**例. mapの場合**

```
resource "<リソースタイプ>" "<リソースのローカル名>" {
  for_each = tomap({
    <キー1>: <値1>
    <キー2>: <値2>
    ...
  })
  <引数1> = each.key
  <引数2> = each.value
}
```

**例. set(string)の場合**

```
resource "<リソースタイプ>" "<リソースのローカル名>" {
  for_each = toset(["<要素1>", "<要素2>",...})
  <引数1> = each.key
}
```

`each.key`, `each.value`ともに繰り返し時に`set`の要素値が使われるため、`<引数1> = each.value`でも可。
for_eachによって複製された各リソースへアクセスするためには、
`<リソースタイプ>.<リソースのローカル名>["<mapのキー> or <set(string)の要素>"]` 
とすればよい。


参考: [The for_each Meta-Argument](https://developer.hashicorp.com/terraform/language/meta-arguments/for_each)

#### 2.3.3.2. count
`for_each`と同様に類似の内容の`resource` or `data` or `module`ブロックを別々に記述したくないときに使う。

こちらは、`number`型(かつ整数)のデータを与えて繰り返し処理させるもの。

**例.**

```
resource "<リソースタイプ>" "<リソースのローカル名>" {
  count = 3
  <引数1> = "xxx-${count.index}"
  <引数2> = count.index
}
```

複製された各リソースへアクセスするためには、
`<リソースタイプ>.<リソースのローカル名>[<インデックス(0始まり)>]`
とすればよい。

参考: [The count Meta-Argument](https://developer.hashicorp.com/terraform/language/meta-arguments/count)

#### 2.3.3.3. for
`resource` or `data` or `module`ブロック単位の繰り返し処理ではなく、単純に 「複合型(`list, set, tuple, map, object`)を、式を設定して別の複合型(tuple, object)に変換する」際に使う。

##### `tuple`へ変換する場合

```
# 複合型の値のみ使ってtupleへ変換する
[for <複合型の値を格納する変数> in <複合型> : <変換後tupleの値の式>]

# 複合型のキー, バリューともに使ってtupleへ変換する
[for <複合型のキー or インデックスを格納する変数>, <複合型の値を格納する変数> in <複合型> : <変換後tupleの値の式>]
```

**具体例.**
```
variable sample_set {
    type set(string)
    default = ["a", "b"]
}

resource "<リソースタイプ>" "<リソースのローカル名>" {
    <引数1> = [for sample in var.sample_set : upper(sample)]
}

variable sample_map {
    type map(string)
    default = {
        "a": "A",
        "b": "B"
    }
}

resource "<リソースタイプ>" "<リソースのローカル名>" {
    <引数1> = [for k, v in var.sample_map : "${k}, ${v}"]
}
```


##### objectへ変換する場合

```
# 複合型の値のみ使ってobjectへ変換する
{for <複合型の値を格納する変数> in <複合型> : <変換後objectのキーの式> => <変換後objectの値の式>}

# 複合型のキー, バリューともに使ってobjectへ変換する
{for <複合型のキー or インデックスを格納する変数>, <複合型の値を格納する変数> in <複合型> : <変換後objectのキーの式> => <変換後objectの値の式>}
```

つまり、括弧が `[]` か`{}`かの違いで変換後の型が`tuple`になるのか`object`になるのか決まる。


**具体例.**
```
variable sample_set {
    type set(string)
    default = ["a", "b"]
}

resource "<リソースタイプ>" "<リソースのローカル名>" {
    <引数1> = {for sample in var.sample_set : sample => upper(sample)}
}

variable sample_map {
    type map(string)
    default = {
        "a": "A",
        "b": "B"
    }
}

resource "<リソースタイプ>" "<リソースのローカル名>" {
    <引数1> = {for k, v in var.sample_map : ${v} => ${k}"}
}
```

参考: [for Expressions](https://developer.hashicorp.com/terraform/language/expressions/for)


## 2.4. terraform コマンド
### 主要なコマンド
主要なものをTerraformを使用する際の工程順に並べると以下の通り。

#### ``terraform validate``
.tfファイルを検証する

#### ``terraform init``
.tfファイルに基づくクラウドインフラへの設定反映に必要なPROVIDERのダウンロード

#### ``terraform plan``
クラウドリソースに対する現設定からの変化点の確認

「実際の環境の状態」、「過去にTerraformで適用した際の状態(tfstate)」、「これから適用しようとしている内容(.tf)」を加味し、結果どういう変更がなされるのか？の結果が出力される。

**tfstate**
Terraformで適用したインフラリソースの状態を保存しておくファイル。
最後に terraform apply した後で、Terraformを介さず手動でリソースの設定を変更してしまった場合、以下のようなメッセージで指摘される。

```
Terraform detected the following changes made outside of Terraform since the last "terraform apply":
```

#### ``terraform apply``
クラウドリソースに.tfファイルの設定を反映

### その他コマンド
#### ``terraform destory``
.tfファイルで定義したクラウドリソースを削除

#### ``terraform fmt``
.tfファイル内の標準の記述形式に則ってフォーマット

#### ``terraform show``
tfstate ファイルに記録されている、「最後に terraform apply が実行された際のリソースの状態」を表示

### ``plan,apply,destroy`` について
#### 差分に現れる記号の意味

- `+`: 追加
- `-`: 削除
- `-/+`: 置換 (削除からの追加)
- `~`: 更新 (削除からの追加じゃなくて、単純な更新)
- `<=`: 読み込み? (データリソースへの適用時のみ発生しうるらしい)

#### 特定のリソースに対してだけインフラ設定を反映させたい場合
以下のようにする。
```
terraform plan -target='<対象のリソース1>' -target='<対象のリソース2>' ...
```

リソースは、`'module.<モジュール名>.<リソースタイプ>.<リソースのローカル名>'`という形で指定するのが無難。
for_eachで暗黙的に生成されるresourceオブジェクトを指定する場合、
`'module.<モジュール名>.<リソースタイプ>.<リソースのローカル名>["キー名"]'`
みたいな形になるが、`'`でくくらずに実行して以下のエラーに遭遇したことがあった

```
Error: Invalid target "module.<モジュール名>.<リソースタイプ>.<リソースのローカル名>[<キー名>]"
│
│ Index brackets must contain either a literal number or a literal string.
╵
```

## 2.5. 公式ドキュメント
以下、特に有用と思われるドキュメント。

- [設定ファイル関連](https://developer.hashicorp.com/terraform/language)
- [terraform コマンド関連](https://developer.hashicorp.com/terraform/cli)
- [PROVIDER関連](https://developer.hashicorp.com/terraform/language/providers)
 
# 3. 参考
https://developer.hashicorp.com/terraform/intro#how-does-terraform-work

https://developer.hashicorp.com/terraform/plugin

https://developer.hashicorp.com/terraform/plugin/framework/provider-servers

https://eagletmt.hateblo.jp/entry/2020/11/30/142114

https://udemy.benesse.co.jp/development/system/what-is-terraform.html

https://hirocks.jp/iac%E3%81%AE%E6%8A%80%E8%A1%93%E9%81%B8%E5%AE%9A/

https://y-ohgi.com/introduction-terraform/first/about/

https://www.lac.co.jp/lacwatch/service/20200903_002270.html

https://blog.dcs.co.jp/aws/20210401-terraformaws.html

https://www.redhat.com/ja/topics/automation/what-is-infrastructure-as-code-iac

https://github.com/hashicorp/terraform/issues/14379

https://zoo200.net/terraform-module-target/

# 4. しばらく実務で使ってみての感想
GCP周りで使う機会があったが、以下のような問題があった。利用するうえではなかなかストレスがたまるところ・・・
- 依存関係を明示的に記述すべき箇所があったりする。
- 各リソースで、意味のない「etag」の差分が一生表示される問題が残っている模様。プロバイダ管理外の情報のため、ignoreすることもできないっぽい。こちらはGitHubプロバイダのものだが事象は同じっぽい(https://github.com/integrations/terraform-provider-github/issues/796)