Terraform Best Practices in 2017

--- 
### 2019/12/23 更新
本記事はベストプラクティスかもしれないけど複雑だったので、2020年度版（0.12対応版)として [Terraform 0.12 のコードを黒魔術にしないために心がけたこと ~ 自分への戒めを込めて ~](https://qiita.com/shogomuranushi/items/266f5ef342fb81a7a5cd) を書きました。こちらも参照ください。

---

# Terraform Best Practices in 2017
以下のブログをベースにver0.10の新機能のworkspaceや、backend、remote stateを活用してベストプラクティスを考えた。
細かい話は以下のブログを参照いただき、ver0.10に対応した内容だけ記載します。
[Terraformにおけるディレクトリ構造のベストプラクティス ｜ Developers.IO](http://dev.classmethod.jp/devops/directory-layout-bestpractice-in-terraform/)

# サンプルコード
サンプルコードを置きましたので、イメージが付かない場合は以下を見てみて下さい。
（適当に作ったので間違えてたらプルリクください）
https://github.com/shogomuranushi/oreno-terraform

# ディレクトリ構造
```
├── environments
│   ├── not_immutable
│   │   ├── provider.tf
│   │   ├── backend.tf
│   │   ├── variable.tf
│   │   ├── main.tf
│   │   └── output.tf
│   └── immutable
│       ├── provider.tf
│       ├── backend.tf
│       ├── variable.tf
│       ├── main.tf
│       └── output.tf
└── modules
    ├── compute
    │   ├── ec2.tf
    │   ├── elb.tf
    │   ├── output.tf
    │   ├── userdata.sh
    │   └── variable.tf
    ├── db
    │   ├── main.tf
    │   ├── output.tf
    │   └── variable.tf
    └── vpc
        ├── main.tf
        ├── output.tf
        └── variable.tf
```

# ディレクトリ構造のポイント
## 1. environments配下の分け方
- tfstateファイルで管理する範囲が大きいと問題があった際の影響範囲が大きくなるため実行単位を小さくする
- 今回の場合は、not_immutableとimmutableで分けて、それぞれの配下でterraformを実行する
- terraformの実行単位を分けるとterraform間での値の受け渡しが通常とは異なり、 `remote state` 機能を利用する必要がある
    - 以下のようにdataを定義することで、remote側のoutputを参照できるようになる
        - remote側のoutputを簡素化させる方法はこちらを参照 -> [Terraformのoutputでmapを利用する方法 - Qiita](http://qiita.com/shogomuranushi/items/cc387a1233cc7f4e6e60)
    - 注意点
        - 制約としてremote先のmoduleの先のoutputは読み取れないのでmodule直下（root）でoutputを定義する必要がある
        - tfstateの管理方法をs3にした状態でstate environmentsを使った時のs3のprefixは `env:/<environment>/` なるため以下のように記述する

```:remote_state参照方法（backend.tf内に記述）
data "terraform_remote_state" "not_immutable" {
  backend = "s3"
  config {
    bucket = "< backetname >"
    key    = "env:/${terraform.workspace}/not_immutable/terraform.tfstate"
    region = "< region >"
  }
}
```

```:moduleへの渡し方
module "compute" {
    source         = "../../modules/compute"

    vpc            = "${data.terraform_remote_state.not_immutable.vpc}"
}
```

## 2. dev/stg/prodなどの環境の分け方
- ver0.9以前はdev,stg,prodなどはディレクトリを分けることで、tfstateを競合させないようにしていたが、ver0.10で追加された `workspace` を利用して環境を分ける
- `terraform workspace new dev` を打つことでdevの環境が作られる
    - デフォルトでは直下に `terraform.state.d` というディレクトリができ、その配下に環境毎にtfstateが管理される

- `terraform workspace list` を打つことで現在のenviromentを参照可能

```:workspaceの実行方法
$ terraform workspace new dev

$ terraform workspace list
  default
* dev
  stg
```

- その前にtfstateはs3に置いたほうが良いと思うので、以下の記述も入れて `terraform init` を実行することでtfstateをs3で管理出来る状態になる。その後に `terraform apply` を実行することでtfstateが生成される
    - なお、s3をbackendにすると `/env:/< environment >` が補完され `< backetname >/env:/< environment >/immutable/terraform.tfstate` のように管理される

```:backend.tf
terraform {
  backend "s3" {
    bucket = "< backet name >"
    key    = "immutable/terraform.tfstate"
    region = "us-west-2"
  }
}
```

```:initの実行方法
$ terraform init
```

# workspaceの活用方法
今まではdevやstg、prodを別のディレクトリで管理していたため、それぞれのディレクトリにvariableを置くような形だったが、workspaceの登場により1つのディレクトリで複数の環境を扱えるようになった。
そこで如何に効率的に複数の環境を扱えるか考えた結果、以下になった。

- map関数をガンガン使う
    - environments配下のvariable.tfには以下のようにmapで定義する
- map関数のkeyの部分をドット区切りでenv情報を入れる
    - 環境毎に値を定義出来る
        - env毎の切り替え方法は、値取得時に `"vpc-${lookup(var.common, "${terraform.workspace}.region", var.common["default.region"])}"
    }` のように `${terraform.workspace}` にdevやstgが入りvalueとして参照可能になる
    - envの値が無ければdefaultを参照するように定義する方法は以下
        - defaultの指定方法は `"vpc-${lookup(var.common, "${terraform.workspace}.region", var.common["default.region"])}"
    }`のように `${lookup(key, value, default)` で指定可能

それらを踏まえたコードは以下

```:variable側
variable "common" {
    default = {
        default.region     = "us-west-2"
        default.project    = "oreno-project"

        dev.region         = "us-west-2"
        stg.region         = "us-west-2"
        prd.region         = "ap-northeast-1"
  }
}

# VPC
variable "vpc" {
    type = "map"
    default = {
        default.cidr       = "10.0.0.0/16"
        default.public-a   = "10.0.0.0/24"
        default.public-c   = "10.0.1.0/24"
        default.private-a  = "10.0.2.0/24"
        default.private-c  = "10.0.3.0/24"
    }
}
```

```:module呼び出し時
module "vpc" {
    source       = "../../modules/vpc"
    common       = "${var.common}"
    vpc          = "${var.vpc}"
}
```

```:module内からvariableの値を取得する時
resource "aws_vpc" "vpc" {
    cidr_block                  = "${lookup(var.vpc, "${terraform.workspace}.cidr", var.vpc["default.cidr"])}"
    enable_dns_support          = "true"
    enable_dns_hostnames        = "true"
    tags {
        Name                    = "vpc-${lookup(var.common, "${terraform.workspace}.project", var.common["default.project"])}"
    }
}
```

```:参考：こんな感じでproviderでもlookup可能
provider "aws" {
    region = "${lookup(var.common, "${terraform.workspace}.region", var.common["default.region"])}"
}
```

# まとめ
1. 影響範囲を小さくするため、terraformの実行単位は小さくしましょう
2. terraform間の受け渡しは remote state を使いましょう
3. workspace を使って環境を分けましょう
4. map関数を使ってmodule等に渡す時などのコードを簡素化しましょう
5. map関数 & workspace & default定義を使ってvariableを効率化させましょう

# 以上