Terraformのディレクトリ構成について考えてみた

# 0. はじめに
* Terraformのディレクトリ構成について改めて考えてみたときのメモです。
* 今回久しぶりにTerraformを触るに際し、そろそろ『正解のディレクトリ構成』があるのかと思って調べて見ましたが、どうやら全ての状況に当てはまる正解というものはなく、構築するシステム構成に応じてベストなものを模索していかなければならない認識です。
* 前提として、中規模くらいのシステム・環境ごとの差異は多少ある（特にprod/stgとdev間）、を想定しています。

# 1. ディレクトリ構成
## 採用したディレクトリ構成案
**最初に結論**

```txt:ディレクトリ構成
.
├── env
│   ├── prod
│   │   ├── datastore
│   │   ├── network
│   │   │   ├── main.tf
│   │   │   └── output.tf
│   │   ├── opsserver
│   │   ├── routing
│   │   ├── service
│   │   └── storage
│   ├── stg01
│   └── dev01
└── modules
    ├── datastore
    ├── network
    │   ├── main.tf
    │   ├── output.tf
    │   └── variable.tf
    ├── opsserver
    ├── routing
    ├── service
    └── storage
```

```terraform:modules/network/main.tf
data "aws_availability_zones" "available" {
  state = "available"
}

#----------
# vpc
#----------
# vpc
resource "aws_vpc" "vpc" {
  cidr_block           = var.vpc_cidr
  instance_tenancy     = "default"
  enable_dns_hostnames = true
  tags = {
    Name = "${var.system}-${var.env}-vpc"
  }
}

# flow_log
resource "aws_flow_log" "flow_log" {
  log_destination = var.flow_log_destination
  log_destination_type = "s3"
  traffic_type = "ALL"
  vpc_id = var.vpc_cidr
}

#----------
# subnet public
#----------
# subnet_public
resource "aws_subnet" "public" {
  count             = length(var.subnet_public_cidr)
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = element(var.subnet_public_cidr, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]
  tags = {
    Name = "${var.system}-${var.env}-subnet-public-${count.index + 1}"
  }
}
```

```terraform:modules/network/variable.tf
variable "system" {}
variable "env" {}
variable "vpc_cidr" {}
variable "flow_log_destination" {}
variable "subnet_public_cidr" {}
```

```terraform:modules/network/output.tf
output "vpc_id" {
  value = aws_vpc.vpc.*.id[0]
}

output "subnet_public_id" {
  value = aws_subnet.public.*.id[0]
}
```

```terraform:env/prod/network/main.tf
#----------
# Terraform
#----------
terraform {
  required_version = "0.13.2"
  backend "s3" {
    bucket  = "system-prod-deploy-tf-999999999"
    region  = "ap-northeast-1"
    key     = "tfstate"
    encrypt = true
  }
}


#----------
# Provider
#----------
provider "aws" {
  region = "ap-northeast-1"
}


#----------
# Remote State
#----------
data "terraform_remote_state" "XXX" {
  backend = "s3"

  config = {
    region = "ap-northeast-1"
    bucket = "system-prod-deploy-tf-999999999"
    key    = "tfstate.XXX"
  }
}


#----------
# Resource - Network
#----------
module "network" {
  source = "../../modules/network"

  # common
  system = "systemA"
  env    = "prod"
  # vpc
  vpc_cidr = "192.168.10.0/24"
  # flow_log
  flow_log_destination = data.terraform_remote_state.storage.outputs.s3_log_arn
  # subnet_public
  subnet_public_cidr = ["192.168.10.0/26", "192.168.10.64/26"]
```

```terraform:env/prod/network/output.tf
output "vpc_id" {
  value = module.network.vpc_id
}

output "subnet_public_id" {
  value = module.network.subnet_public_id
}
```

## 採用した理由
* 選定基準は、①より安全な改修・デプロイができること・②環境ごとの差異を比較しやすいこと
* [How to Create Terraform Multiple Environments](https://getbetterdevops.io/terraform-create-infrastructure-in-multiple-environments/)のSeparated Directoriesパターンを採用
    * モジュール切り出し+環境分離+コンポーネント分割
* 公式通り、中規模なのでmodulesを利用する・環境の切り替えにWorkSpaceは利用しない
    * [Standard Module Structure](https://www.terraform.io/language/modules/develop/structure)
    * [Workspaces](https://www.terraform.io/language/state/workspaces)
* 変更頻度・影響範囲・可読性・terraformコマンドの処理時間を考慮してstateファイルを分割するため、env配下もさらにmodulesに合わせてディレクトリを分割する
    * コンポーネント分割の粒度は[Terraformはどの単位で分割すべきか](https://eng.iridge.jp/post/2017/about-think-terraform-unit-of-execution/)を参考にした
    * ここは統合しても良いかもしれない（公式のcomplete-moduleに寄せる）。悩み中

## （参考）ディレクトリ構成案
```txt:minimal-module
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
```

```txt:complete-module
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
├── ...
├── modules/
│   ├── nestedA/
│   │   ├── README.md
│   │   ├── variables.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   ├── nestedB/
│   ├── .../
├── examples/
│   ├── exampleA/
│   │   ├── main.tf
│   ├── exampleB/
│   ├── .../
```

# 2. 開発規約
* modules配下は、公式通りmain.tf/variable.tf/output.tfに分ける
* env配下は、環境ごとのdiffを取るためmain.tfに寄せる。output.tfのみmodulesに合わせて切り出す
* 変数名などは、 `サービス名_役割_項目` 、でつける。重複はしないようにしつつ、シンプルにする。
    * ex. `subnet_public_cidr`
* リソース名の命名規則は[弊社で使っているAWSリソースの命名規則を紹介します](https://dev.classmethod.jp/articles/aws-name-rule/)参照
    * ex. `{sysname}-{env}-{nlayer}-subnetXX`
* コメントの粒度をmodules側とenv側で合わせる
* 極力各リソースタイプに対応するData Sourceを使って参照する

```terraform
data "aws_vpc" "vpc" {
  tags = {
    service = var.service
    env     = var.env
  }
}
```

* 積極的に使う関数
    * 繰り返し作成: for_each(countよりこちら), count, element
    * 文字列結合: format
* プロバイダバージョン固定のため、 `.terraform.lock.hcl` ファイルをリポジトリ管理対象とする
* コミット前に `$terraform fmt -recursive` と `terraform validate` は各自実行する
* CIで実行するコマンド: terraform plan, terraform validate, tflint, tfsec

# 参考
* [[Terraform]Moduleを作ると環境毎のデプロイが便利](https://dev.classmethod.jp/articles/terraform-deploy-module/)