terraform開発時の現実的な使い方

terraformは結構癖があるので慣れるまでは使いにくいと思っています。
最近割と慣れてきた気がするので、個人的に使っているterraformの使い方の具体例を紹介します。

あくまで個人的にいいと思っているやり方であって、最適でもベストプラクティスでもありません。
主にAWSを触っているので例はAWSが多いですが、GCPでも基本的に同じようにできるはずです。

### バックエンドを作る
`.tfstate`を置くバックエンドはlocalに置かないことが[推奨されています](https://www.terraform.io/docs/backends/)。
これは同時編集によるコンフリクトを防いだり、stateに書き込まれた機密情報を一箇所にまとめられるなどの利点のためです。

バックエンドをs3にしようと思えば、当然そのバケットはterraform使用開始前に作成しておく必要があります。
これを手動で作成してはせっかくの*Infrastructure as Code*が不完全になってしまうので避けたいところです。

terraformで完結させるためには

```tf
resource "aws_s3_bucket" "hogehoge_backend" {
  bucket = "hogehoge-backend"
}

terraform {
  required_version = "= 0.12.20"
}
```

としてlocal backendのまま `terraform apply`し、

```diff
resource "aws_s3_bucket" "hogehoge_backend" {
  bucket = "hogehoge-backend"
}

terraform {
  required_version = "= 0.12.20"
+  backend "s3" {
+    bucket = "hogehoge-backend"
+    key    = "hogehoge/terraform"
+    region = "ap-northeast-1"
+  }
}
```
を追記して `terraform init` することでバックエンドを変更できます。
ローカルの`.tfstate` のファイルが残る気もしますが不要なので消して大丈夫です。

> s3バックエンドの場合はバージョニングの有効化などが[推奨されてます](https://www.terraform.io/docs/backends/types/s3.html)
> 上の例は最小構成なので、実際に使うときはちゃんとバケットの設定しましょう。
> あと、このバケット設定はモジュール化しておくと再利用できて良いです。[既製品](https://github.com/cloudposse/terraform-aws-tfstate-backend)もあります。

### モジュールをgithub repoに分ける
バックエンドのところでも触れましたが積極的にモジュール化したほうがいいと思います。
既存のモジュールを使ってもいいですが、自分がよく使う構成は自分のリポジトリにまとめておくとより便利に使えます。

```tf
module "backend" {
  source = "git::ssh://git@github.com/hoge/my-terraform-s3-backend.git"
}

# リポジトリのサブディレクトリも指定できる
# ひとつのリポジトリに複数モジュールをまとめても良さそう
module "backend" {
  source = "git::ssh://git@github.com/hoge/my-terraform-modules.git//s3-backend"
}

# ブランチやタグも指定できる
module "backend" {
  source = "git::ssh://git@github.com/hoge/my-terraform-modules.git//s3-backend?ref=master"
}
```

### リポジトリ作るまでもない場合はlocal moduleを使う
現在のプロジェクトでしか使わないモジュールをわざわざ別repositoryにする必要はありません。
例えばhogeというアプリケーションのリソースを定義する場合

```
.
|-- backend.tf 
|-- common.tf // 共通リソース(s3/iamなど)
|-- hoge.dev.tf // dev環境
|-- hoge.prod.tf // prod環境
|-- modules
|   |-- hoge
|   |   |-- cat.tf
|   |   |-- dog.tf
|   |   |-- cow.tf
|   |   `-- variables.tf
|   `-- fuga
|       |-- fuga.tf
|       `-- variables.tf
`-- terraform.tf
```
こんな感じのディレクトリ構成にして、

```tf:hoge.prod.tf
module "hoge_prod" {
  source             = "./modules/hoge"
  env                = "prod"
  common_resource_id = common_resource.common.id # common.tfで定義したリソース
}
```

などとするといい感じになります。


hogeモジュールからfugaモジュールを参照することも可能です。


### エイリアスを張る
`terraform plan`って長いので・・・

```bash

alias tp='terraform plan'
alias ta='terraform apply'
```

ちょっと変更するたびにこまめにplanしています。
planに時間がかかる場合は

```bash
tp --target aws_s3_bucket.hogehoge // 特定のリソースだけplan
tp --target module.hoge // hogeモジュールだけplan
```

が便利です。

### 認証情報を管理する
AWSの場合`$AWS_ACCESS_KEY_ID`や`$AWS_SECRET_ACCESS_KEY` 環境変数を `.env`などでexportするのがいい気がします。
`.tfvars` に書くという手もありますが、認証情報は環境変数に書いたほうが楽です。

こうすると`aws-cli` の設定を兼ねられます。

GCPの場合デフォルトの認証情報を勝手に読みに行ってくれるので何も気にしなくても大体動きます。ありがたいですね。

### 開発環境
VScodeの[いい感じのplugin](https://marketplace.visualstudio.com/items?itemName=mauve.terraform)があります。
code format、補完、syntax highlightなどをしてくれます。使いましょう。

### tfenvを使う
[複数のterraformのバージョンを切り替えながら使えるツール](https://github.com/tfutils/tfenv)です。いろんな言語でよくあるやつです。
terraformは[バージョン互換性がなかったり](https://www.terraform.io/upgrade-guides/0-12.html)するので使っておいたほうが幸せになれると思います。

`.terraform-version`というファイルを作っておくと[自動的にバージョンを切り替えてくれます](https://github.com/tfutils/tfenv#terraform-version-file)

ちなみにインストール可能なバージョンの列挙が

```bash
tfenv list-remote
```

となっていて他の同系列のツールとちょっと違う気がします。

### 手動で作ってからimportする
「terraformより手動でやったほうが早いから手動でやる・・・]
ということを言う人がいますが、**実際そのとおりだと思います。**

最初からterraformでリソース定義して`apply`するのは効率が悪いので、 `aws-cli`や`gcloud` コマンドやwebコンソールで手動で作成してから `terraform import`するのがオススメです。

`terraform import`はterraform管理外のリソースを、terraform内の未applyのリソース定義と紐付けて取り込むコマンドです。
RDSとかECSとかはこっちのほうがかなり早い気がします。

例えば ECSタスク定義の場合は以下のような手順です。

1 (まずwebコンソールでポチポチとタスク定義を作ります)

2 適当にリソース定義を書きます

```tf

# ひとまず最小構成でOK
resource "aws_ecs_task_definition" "service" {
  family                = "service"
  container_definitions = jsonencode([{
  }])
}
```
 3 importします
importするリソースの指定方法はリソースによって違うので[ドキュメント](https://www.terraform.io/docs/providers/aws/r/ecs_task_definition.html#import)を見ましょう


```bash
terraform import aws_ecs_task_definition.service arn:aws:ecs:ap-northeast-1:123456789:task-definition/service:1

# providerを指定してる場合は明示する必要がある。
terraform import --provider aws.us-east-1 aws_ecs_task_definition.service arn:aws:ecs:ap-northeast-1:123456789:task-definition/service:1
```


4 `terraform plan`して差分を確認し、差分がなくなるように定義を修正していきます。
terraformの差分表示は変更の前後がひと目で分かるので、簡単に直せます。

以下は別の例ですが、`gcloud` コマンドで作ったcloud functionsをimportした場合。自動でつく `label`が、import後には差分表示されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116979/e9fa6b4a-5908-c425-8832-274ca051957e.png)

terraform側は以下のように修正します

```diff
resource "google_cloudfunctions_function" "hoge" {
   ...略...
+  labels = {
+    deployment-tool = "cli-gcloud"
+  }
}
```

再度 `terraform plan` してみると差分が消えています。
これで最初からterraformで作ったのと同じ状態になりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116979/096d2ffe-e64c-a9ab-f385-b2275b2d91b9.png)

ちなみにimportしたresourceを管理外に外したいときは

```bash
terraform state rm google_cloudfunctions_function.hoge
```
で外せます。
リソースを差し替えたいときなどには一度 `state rm` してから`import`する合せ技が有効です。


### 機能単位にファイルを分ける
terraformでは以下のようにサービス単位でファイルを分けることがよく行われている気がします。

```
.
|-- s3.tf
|-- iam.tf
|-- vpc.tf
|-- rds.tf
|-- cloud_watch.tf
`-- ecs.tf
```

しかし、この分け方だと特定の機能の構成を変更するとき複数ファイルにまたがった変更になりますし、どのリソースがどこで使われているのかがひと目でわかりません。

以下のように機能ごとにファイルを分けることを個人的には推奨します。

```
.
|-- common.tf             // vpc,iam,s3など
|-- webserver.tf          // ecs,ecr,security groupなど
|-- error_notification.tf // cloud watch, lambdaなど
`-- database.tf           // rds,security groupなど
```
こうするとかなり読みやすくなると思います。(注: 機能の分け方は適当な例です)

もし１ファイルに収まらなくなってきたら、`./modules` にモジュールとして分割しましょう。
機能ごとの結合部分が明確になり、疎結合性を保ちやすくなります。

逆にサービスごとの定義が分散して見にくいという意見もありますが、 terraformではresource名はproviderに定められた固定の文字列(`aws_s3_bucket`など)を使うので、文字列検索で容易に一覧できます。


### variableにobjectを使うときはデフォルト値を設定できない

変数にはデフォルト値を設定できますが、object型の変数に対して指定した場合、

```tf
variable "ports" {
  type = object({
    webserver = number
    db        = number
  })
  default = {
      webserver = 8080
      db        = 3306
  }
}
```

以下のようにbdの値だけ指定したいと思っても

```tf
ports = {
    db = 3000
}
```

`attribute "webserver" is required.` というエラーが起きます。
object型のデフォルト値はobject全体の値をまとめて指定することしかできないからです。(できるようにしてほしい・・・)

デフォルト値を一部だけ上書きすることが想定される場合、object型ではなく個別の変数にするべきでしょう。

```tf
variable "port_webserver" {
  type    = number
  default = 8080
}

variable "port_db" {
  type    = number
  default = 3306
}
```