Terraform設計・運用のノウハウ 

# 概要

[Terraform](https://www.terraform.io/)を使い始めて一年ほど経ったので、設計や運用上のノウハウを公開していこうと思います。
基本的にAWSを使うことが多いので、AWS寄りの話になります。

# 設計編

## 利用可能なプロバイダを調べる

TerraformはAWSやGCPといったクラウドサービスをコード化できることで有名ですが、[オフィシャルだけでも様々なプロバイダに対応](https://www.terraform.io/docs/providers/index.html)しています。
私はAWSのほか、GitHubやDatadogをコード化して運用してます。

## ネットワーク設計

サービスを構築する上で本番環境のほかに開発やステージング環境を作りますが、環境の分け方は大きく分けて次のいずれかになることが多いと思います。

1. 1つのVPCに全ての環境を入れる
2. 環境ごとにVPCを分ける
3. 環境ごとにAWSアカウントを分ける

2のパターンで設計する場合、Terraformで構成を組む際はVPCを`development`、`staging`、`production`に分離し、更に各VPCで共通となるサービスを配置するための`management`を作成します。`management`には踏み台であったり、CI/CDを管理するインスタンスを配置しています。
ネットワーク上は`management`から各環境にVPCペアリングで接続できますが、環境間(例えば`development`から`staging`、`staging`から`production`)の接続はルートテーブル上で制限しています。

![開発基盤の概要-Page-3 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27454/bc6032f6-64d0-d707-9455-c423a1a87f5e.png)

## ディレクトリ設計

Terraformでリソースを管理する場合、悩むことになるのがディレクトリ構成かと思います。私は [Code structure](https://www.terraform-best-practices.com/code-structure) で紹介されている方法を元に次のような構成にしてます。

```
# 構築する対象サービス。EC2、Security Groupといった単位
{service}
  # resourceやmodule
  main.tf

  # main.tfやbackend.tfで参照する変数のリスト
  variables.tf

  # 出力パラメータのリスト
  outputs.tf

  # プロバイダ情報
  providers.tf

  # backendやdataのリスト
  backend.tf
```

インフラは複数のサービスから構成されるため、実際には次のようなディレクトリ構成となります。

```
providers
  aws
    # AWS運用上必須となる基盤サービス
    base
      # リージョンに依存しないサービス
      global
        iam
        route53
        ...
      # 特定のリージョンを構築するサービス
      ap-northeast-1
        cognito
        cloudtrail
        ...
    # アプリケーションを構成する上で必要となるサービス
    {APP NAME}
      # ワークスペースに依存しないサービス
      base
        route53
        ...
      # ワークスペースで構成されるサービス
      workspace
        ap-northeast-1
          global
            vpc
            ecs
            ...
```

## Terraform Best Practiceを読む

[Terraform Best Practice](https://www.terraform-best-practices.com/)を読みましょう。中でも[Naming conventions](https://www.terraform-best-practices.com/naming)は非常に有用です。いくつか気になるポイントをまとめてみました。

* Resource and data source arguments
    * リソース名や変数、出力名の区切り文字には`-`の代わりに`_`を使う
    * リソース名にはリソースタイプを含めない (例えば、`aws_route_table`のリソース名に`route_table`などのタイプを含めない)
    * 単一リソースや妥当な名前がない場合、リソース名は`this`とする
    * `tags`、`depends_on`、`lifecycle`はリソースの最後に記述する
* Variables
    * `default = []`がある場合、`type = "list"`は省略する
    * `default = {}`がある場合、`type = "map"`は省略する
    * `list`や`map`の変数名は複数形を使用する
* Outputs
    * 名前は`{name}_{type}_{attribute}`形式が望ましい

## リソース名の変更

`terraform state mv`コマンドを使います。

```
# リソース名をfooからbarに変更
$ terraform state mv aws_kms_key.foo aws_kms_key.bar
```

Workspaceを利用してる場合はWorkspace単位で実行が必要となります。

## リソースをモジュール化

基本的にはリソース名の変更と同じです。接頭辞として`module.{MODULE_NAME}`を付けるだけです。

```
# リソースfooをbarモジュール内のaws_kms_key.bazに変更
$ terraform state mv aws_kms_key.foo module.bar.aws_kms_key.baz
```

# 実装編

## Docker版Terraformを使う

[Docker版のTerraform](https://hub.docker.com/r/hashicorp/terraform/)を利用しましょう。

## バージョンを指定する

利用するバージョンを明確化することで、無用なトラブルを回避することができます。

### Terraform

```
terraform {
  required_version = "= 0.12.4"
}
```

Docker版Terraformを利用する場合、イメージのバージョンは`latest`ではなく、`0.12.4`のように番号で指定することをお勧めします。

### プロバイダバージョン

```
provider "aws" {
  version = "= 2.19.0"
}
```

### モジュールバージョン

```
module "foo" {
  source  = "terraform-aws-modules/security-group/aws"
  version = "3.0.1"
}
```

尚、`source`がGitHubの場合は`version`が使えないので、代わりに`ref`でバージョンを指定する必要がある。

```
module "foo" {
  source = "github.com/azavea/terraform-aws-acm-certificate?ref=1.1.0"
}
```

## tfstateファイルの管理方法

リソースの状態を管理するtfstateファイルはローカルやGitで管理するべきではありません。TerraformのBackend機能を使うことで、tfstateをDynamoDBやS3で管理することができます。
AWSを利用しているのであれば、S3がお手軽かと思います。

```
terraform {
  backend "s3" {
    bucket = "mybucket"
    key    = "path/to/my/key"
    region = "us-east-1"
  }
}
```

尚、S3を使う場合、対象バケットのバージョニングの有効化を強くお勧めします。tfstateを誤って削除したり、Terraformのバージョンアップでファイル構成が書き換わっても、簡単にステートファイルを復元することができます。

## Terraform Module Registryを使う

[Terraform Module Registry](https://registry.terraform.io/)では、汎用的に使えるモジュールが公開されてます。AWSで言えばEC2やSecurity Groupなど、いくつかの変数を渡すだけでリソースを作成してくれます。
Registryには大きく分けて、Terraformコミュニティが作ったものと、HashiCorpによって検証されたVerifiedモジュールがあります。
運用で使う場合は、検索時に`Verified`にチェックを付けることをお勧めします。

<img width="461" alt="Screen_Shot_2019-07-15_at_23_21_12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27454/83e40a1a-73c3-584c-0595-787364cdc798.png">

## フォーマットを整形する

`fmt`コマンドでディレクトリ下のファイルに対しまとめて整形してくれます。

```
# 0.11まで
$ terraform fmt

# 0.12以降
$ terraform fmt -recursive
```

## デバッグ

変数や関数の結果を確認したい場合は`console`コマンドが便利です。

```variables.tf
variable "foo" {
  default = "bar"
}
```

```
# 変数の確認
$ terraform console
> var.foo
bar

# 関数の実行結果の確認
> lookup({foo="bar"}, "foo", "baz")
bar
```

## 環境ごとに異なる値を読み込む方法

変数名はWorkspace名を含めて定義します。

```
variable "rds_cluster" {
  default = {
    "default.engine_version"    = "5.7.12"
    "production.engine_version" = "5.7.22"
  }
}
```

リソース作成時に`lookup`関数を使うことで、現在有効なWorkspaceが`production`であれば`5.7.22`、それ以外であれば`5.7.11`の値が設定されます。

```
resource "aws_rds_cluster" "this" {
  engine_version = lookup(
    var.rds_cluster,
    "${terraform.workspace}.engine_version",
    var.rds_cluster["default.engine_version"],
  )
  ...
```

## lifecycleでリソースを保護する

リソースに`lifecycle`ブロックを指定することで、リソース作成・更新時の動作を変更することができます。`lifecycle`にはいくつかのタイプがありますが、`prevent_destroy`を指定することで、強制的なリソース再作成が発生したときにエラーを発生するようになります。

```main.tf
lifecycle {
  prevent_destroy = true
}
```

`destroy`を実行した結果。

```
$ terraform destroy
...
Error: Instance cannot be destroyed

  on main.tf line 55:
  55: resource "aws_eip" "this" {

Resource aws_eip.this has lifecycle.prevent_destroy set, but the plan calls
for this resource to be destroyed. To avoid this error and continue with the
plan, either disable lifecycle.prevent_destroy or reduce the scope of the plan
using the -target flag
```

リソースを強制的に削除したい場合は、`prevent_destroy`を`false`にするか、ブロックごと削除して、`apply`を実行します。

※`prevent_destroy`はあくまでTerraform上で削除を保護するためのもので、AWSコンソールやCLIからは削除することが可能です。EC2やRDSなどいくつかのサービスには削除保護のオプションがあるので、別途有効化しておくことをお勧めします。

## IAM PolicyのJSONはaws_iam_policy_documentで定義する

以下のリソースはIAMポリシーがヒアドキュメント形式で書かれており、可読性が低いです。

```main.tf
resource "aws_iam_policy" "example" {
  # ... other configuration ...
  policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Action": "*",
    "Resource": "*"
  }
}
POLICY
}
```

ヒアドキュメントはこのように置き換えることができます。HCLで書けるのでだいぶスッキリします。

```main.tf
resource "aws_iam_policy" "example" {
  # ... other configuration ...
  policy = data.aws_iam_policy_document.example.json
}

data "aws_iam_policy_document" "example" {
  version = "2012-10-17"
  statement {
　　 effect = "Allow"
    actions = ["*"]
    resources = ["*"
  }
}
```

## デバッグログを出力する

環境変数`TF_LOG`にログレベルを渡すことで、ログを出力することができます。ファイルに出力したい場合は`TF_LOG_PATH`も指定すると良いでしょう。

* [Debugging Terraform](https://www.terraform.io/docs/internals/debugging.html)

## 実行速度を改善する

Terraformのコマンドに`--parallelism`を渡すことで、APIの同時呼び出し数を変更することができます。

```
$ terraform plan --parallelism=20
```

デフォルト値は`10`です。パフォーマンスを見て数値を変えて見ると良さそうです。
尚、環境変数で指定する場合は`TF_CLI_ARGS_plan`、`TF_CLI_ARGS_apply`、`TF_CLI_ARGS_destroy`を指定します。

## 拡張ライブラリ

* [GoogleCloudPlatform/terraformer](https://github.com/GoogleCloudPlatform/terraformer)
    * 既存のリソースからtfファイルとtfstateファイルを生成してくれる
* [gruntwork-io/terragrunt](https://github.com/gruntwork-io/terragrunt)
    * Terraformのラッパー。stateファイルのロックや、モジュール管理をDRYにする仕組みを提供
* [hashivim/vim-terraform](https://github.com/hashivim/vim-terraform)
    * Vimmer向け。保存時にフォーマット整形、コード検証
* [vim-terraform-completion](https://github.com/juliosueiras/vim-terraform-completion)
    * Vimmer向け。入力補完やコード検証