Terraform 0.12 のコードを黒魔術にしないために心がけたこと ~ 自分への戒めを込めて ~

[ABEJA Advent Calendar 2019](https://qiita.com/advent-calendar/2019/abeja) の4日目です。

こんにちわ。 最近 [@rwle1212](https://twitter.com/rwle1212) で呟き始めました。Twitterの良い使い方がわからん。

# 本記事のキッカケ
## 過去のお話

さて、過去にこんな投稿しました。
193いいねが付いているので割と好評だったのかもしれません。しかし、
[<img width="700" alt="スクリーンショット 2019-12-01 12.34.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75573/9ceaf332-3c16-5a58-b8bf-49353b6abdcd.png">](https://qiita.com/shogomuranushi/items/e2f3ff3cfdcacdd17f99)

その1年半後に次のような話を JAWS Days 2019 で話しました。

[<img width="700" alt="スクリーンショット 2019-12-01 12.36.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75573/d13a6f98-9a89-2cf4-e94f-4bcc79d9c426.png">](https://speakerdeck.com/shogomuranushi/infrastructure-as-code-is-very-tired)

それらを要約するとすると以下になります。

- 2017年11月: Terraform のベストプラクティスな使い方考えたよ！
- 2019年2月 : ごめん。疲れたわ

と、疲れて終わっていたので

## 0.12 になり再入信

色々反省し、Terraform も 0.12 になり大きく変わったこともあり、黒魔術にしないように心がけた設計を共有します。たぶん楽になってるはず。たぶん

# 本題： Terraform 0.12 で心がけていること

`シンプルさ` を重視しています。
過去に誰かが「複雑な仕組みを作ることは誰でも出来るが、シンプルな仕組みを作るのは難しい」と言ってました。
確かに、上のベストプラクティス云々の記事の記事は複雑です。なので、今度こそシンプルな仕組みを心がけたいと思います。

最終的なコードはこちらに上がっています。
https://github.com/shogomuranushi/oreno-terraform-v0.12

## 可読性

やっぱり、人が書いたゴチャゴチャしたコードとか読めないよね。 ~~読む気にならないよね~~
「人にやられて嫌なことはするな」ってばっちゃが言ってたので、人から受け取った時に嫌な読めないコードは書かないようにしようと思いました。

ということで「可読性」で心がけていることは

- `moduleは最終手段`
  - moduleを使うと複雑になりがちなので、基本は使わない
  - 他の方法で繰り返し実行することができないか考える
  - その方法とmoduleがどちらがシンプルか考える

- `全てを変数化しない`
  - 読みやすさを重視しハードコードも厭わず、全環境固定で変わりそうにないパラメータはハードコードする
- あちこち `参照しないとコードを追えないようにはしない`
  - せいぜい数カ所、2段階位追っ掛ければ追えるようにする
      - 分かりやすい => resource → variable
      - 分かりにくい => resource → module → output → backend resource → module → variable …
  - module間をoutputで受け渡したり、terraform間をdata sourceで受け渡したりとか、何の値が入っているのかをさらにわかりにくくなる
     - terraform間、module間の受け渡しはせずにハードコードする、受け渡しした方がミスは少なくなるが、アチコチ参照しないといけないので、どこがどう連動しているのか分かりづらい
     - 取り回し性は低くなるが、可読性は上がり、（クラウドの知識があれば）何をすれば良いのかわかる
- チーム内のTerraformスキルセットを踏まえて難解になる記述や、 `読みにくい関数は利用しない`
    - 難解になる位なら冗長的なコードにする。まだ分かりやすい
    - チーム内でTerraformにそこまでコミットする必要がないなら、難解なコードより冗長的になったとしてもTerraform初心者でもわかるようなコードの書き方が望ましい
      - [workspaceの活用方法](https://qiita.com/shogomuranushi/items/e2f3ff3cfdcacdd17f99#workspace%E3%81%AE%E6%B4%BB%E7%94%A8%E6%96%B9%E6%B3%95) は悪い例で、「workspace」「variable」「map」「lookup」「module」の動作を知ってないと読みづらい。他にも色々 data source とか組み合わせていたりする。
      - もはや、以下のようなシンプルな感じで良いじゃん。これならAWS知ってる人ならすぐにイメージが湧く

```
resource "aws_vpc" "vpc" {
    cidr_block                  = "10.0.0.0/16"
    enable_dns_support          = "true"
    enable_dns_hostnames        = "true"
    tags {
        Name                    = "oreno-vpc"
    }
}
```

## 冗長性
シンプルとは言え、ハードコードし過ぎたり、冗長なコードが増えるとメンテナンスコストが増える。
dev, stg, prod はほぼ同じ構成でパラメータだけ違うというのはよくあるので、共通化するためにもシンプルにまとめることを心がける

- Workspaceは使う
    - 環境・変数を分け、コードを使い回すようにする
    - variableのmap(map())を利用することでvariableを複数書く冗長性を防ぎ、次のように環境追加時の変更箇所を明確にすることで保守性を上げる

```
variable "vpc" {
  type = map(map(string))
  default = {
    dev = {
      name          = "vpc-dev"
      ip_cidr_range = "10.10.0.0/16"
    }
    stg = {
      name          = "vpc-stg"
      ip_cidr_range = "10.10.0.0/16"
    }
    prod = {
      name          = "vpc-prod"
      ip_cidr_range = "10.10.0.0/16"
    }
  }
}
```

- さらにlocalsを一枚間に抽象化層を挟むことで、冗長的な呼び出し方をシンプルにする
    - 以下は、 var.vpc の中の workspace で選択された dev, stg, prod のどれかの値を locals.vpc に入れる
    - 呼び出す時は local.vpc.<key>で valueを取ることができる

```
locals {
  vpc = var.vpc[terraform.workspace]
}
    
resource "google_compute_network" "main" {
  name                    = local.vpc.name
  auto_create_subnetworks = false
}
```

なお、一枚間に挟まない場合はこのようになり

```
    name = var.vpc[terraform.workspace].name
    name = var.vpc[terraform.workspace].name
    name = var.vpc[terraform.workspace].name
    name = var.vpc[terraform.workspace].name
    name = var.vpc[terraform.workspace].name
    name = var.vpc[terraform.workspace].name
```

一枚挟むと、以下のようになる。普通に変数から取得しているように見えるのでわかりやすい（と思っている）

```    
    name = local.vpc.name
    name = local.vpc.name
    name = local.vpc.name
    name = local.vpc.name
    name = local.vpc.name
```

そしてmoduleを使わないので、実際のコードは以下のようになる。

```:vpc.tf
variable "vpc" {
  type = map(map(string))
  default = {
    dev = {
      name          = "vpc-dev"
      ip_cidr_range = "10.10.0.0/16"
    }
    stg = {
      name          = "vpc-stg"
      ip_cidr_range = "10.10.0.0/16"
    }
    prod = {
      name          = "vpc-prod"
      ip_cidr_range = "10.10.0.0/16"
    }
  }
}

locals {
  vpc = var.vpc[terraform.workspace]
}

resource "google_compute_network" "main" {
  name                    = local.vpc.name
  auto_create_subnetworks = false
}

resource "google_compute_subnetwork" "main" {
  name          = local.vpc.name
  ip_cidr_range = local.vpc.ip_cidr_range
  region        = local.common.region
  network       = google_compute_network.main.self_link
}
```

少し、variable周りが複雑になっているが

- resourceを見れば、 local.vpc から name を取ってるだな。とかが分かる
- local.vpc ってなんやねんってのは、同じファイルの上段を見れば分かる

# ディレクトリの分け方
- 好みで

- 僕は消したら困る statefull と最悪消えても良い stateless をトップディレクトリで切り
    - statefull はミスしても困るので目的毎に小さい塊で
    - stateless はミスしても作り直せるので目的毎に大きい塊で管理
- 実際の構成は以下の感じです
    - ref: https://github.com/shogomuranushi/oreno-terraform-v0.12
    - module使っていますが、あまりに冗長化になりそうだったので、最終手段で使いました

```:tree
$ tree
.
├── README.md
├── statefull
│   ├── 01_vpc
│   │   ├── 01_setting.tf
│   │   ├── 02_firewall.tf
│   │   └── 02_vpc.tf
│   └── 02_gke
│       ├── 01_setting.tf
│       └── 02_gke.tf
└── stateless
    └── node_pools
        ├── 01_setting.tf
        ├── 02_node-pool_cpu.tf
        ├── 02_node-pool_k80.tf
        ├── 02_node-pool_v100.tf
        └── modules
            ├── node_pool_cpu
            │   └── main.tf
            └── node_pool_gpu
                └── main.tf
```

# 最後に
インフラチームがありコードで管理したりメンテナンスしたりするのがお仕事の場合はコードの高度化を行い効率を求めれば良いですが、そこまで体制が整っているところが多くなかったり、本来価値ある開発に集中したい場合があります。
その際は、過度に効率を求めるのではなく、シンプルな書き方を重視するのも良いかと思います。

もし、可読性上げながら冗長性上げながらシンプルさを追求するなら汎用言語で書ける AWS CDK などがオススメですが、 DSL の良さは消えます。最近の Infrastructure as Code 流行りは汎用言語な感じがするので、 DSL と 汎用言語 を行ったり来たりして infrastructure as code も進化していくのかなと思っています。