Terraform - リポジトリ構造と活用範囲を考える

## はじめに

Terraform に入門すると、最初に簡単な VPC を作成するまでは早いですが、実運用を見越して `terraform.tfstate` の管理方法について考えたり、効率的なディレクトリ構成について考えたりすると手が止まってしまいます。

入門時期を終えて、[書籍『Pragmatic Terraform on AWS』](https://booth.pm/ja/items/1318735) を読んで Terraform のお作法について学び直したところなので、これまで得た知見を整理するために記事を書いてみます。

### 書くこと

- Terraform の使い所
- Terraform で実装したリポジトリの例とサンプルコード（一部）

### 書かないこと

- Terraform の使い方・インストール方法

## Terraform の運用方法

Terraform でクラウド環境を構築するだけなら直ぐですが、**「構築した環境の上で動作するアプリケーションのライフサイクルについて」**や、**「AWSアカウント直後から、対象の環境で Terraform を初めて実行するまで」**のことを考えると、色々と考えることが多くなります。

### 実際の構築順序

今回の記事は**「Terraformで全ては構築しない」**という前提で記載しています。
これを踏まえて、「AWSアカウントを取得して基本設定を終わらせた状態」から「アプリケーションをデプロイする」までをまとめると次の３つのステップが必要になります。

1. アカウントの基本設定と`tfstate`保存領域の確保
2. Terraform によるインフラの構築
3. Terraform 対応範囲外のインフラ構築（GUI、CUI）

このうち、2) のみが Terraform の実行によって環境を構築するフェーズです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79222/6b2f2525-997b-8c6f-1b2e-98457c9fb81a.png)

#### 1. アカウントの基本設定と`tfstate`保存領域の確保

AWSアカウントを取得して、本当の直後は次のようなサイトを参考にして設定を行います。

- [AWSアカウントを取得したら速攻でやっておくべき初期設定まとめ - Qiita](https://qiita.com/tmknom/items/303db2d1d928db720888)

この設定が終わったら、 Terraform で作業を行うために次のような設定を行います。

1. 作業ユーザの作成
2. `access_key`, `secret_key` の発行
3. backend 用の S3 Bucket の作成

また、状況に応じて Terraform 実行前にやることとして、以下のようなこともあるかもしれません。

- Elastic IP の発行（外部IF として固定が必要なものなど）
- ドメインの取得 と Route 53 の設定
- ACM を使用した SSL証明書の登録

cf. [Multi-account AWS Architecture - Terraform by HashiCorp](https://www.terraform.io/docs/backends/types/s3.html#multi-account-aws-architecture)

>  ideally the infrastructure that is used by Terraform should exist outside of the infrastructure that Terraform manages.

#### 2. Terraform によるインフラの構築

詳細については、「Module のお作法を整理する」以降の節で記載します。

#### 3. Terraform 対応範囲外のインフラ構築（GUI、CUI）

アプリケーションのデプロイなど、Terraform が構築したシステム・インフラの上にのるリソースのデプロイを行います。

- Elastic Beanstalk アプリケーション
- AWS Lambda アプリケーション
- API Gateway アプリケーション

ただし、この 3) については、自分の中でも悩みの多いところであり、どのように役割分担してゆくか戸惑っている箇所です。

## Module のお作法を整理する

まず最初に、Terraform でインフラを構築するにあたって避けては通ることの出来ない module について整理します。

### Standard Module Structure

Terraform の公式サイトでは **Standard Module Structure ( 標準モジュール構造 )**　として次の２つの構成が紹介されています。
cf. [Creating Modules - Terraform by HashiCorp](https://www.terraform.io/docs/modules/index.html)

- minimal recommended module
-  complete example of a module

#### 構造のルール

構造のルールとして次のようなことが記載されています。

- **Root module** : リポジトリのルートディレクトリに「Terraform files」を配置しなければならない。<br>これがモジュールの primary entrypoint となる。
- **README** : Root module と ネストされた moduleは README を配置した方が良い。<br>input や output はツールで自動生成できるため、記載する必要はない。
- **LICENSE** : Public にモジュール公開するなら、あった方が良い。
- **main.tf**, **variables.tf**, **outputs.tf** : 最小モジュールとして推奨されるファイル名。空でもあったほうが良い。
- **Variables and outputs should have descriptions.** : 全ての variable と output に１～２行の説明を記載しましょう。
- **Nested modules.** : module は `modules/`配下に配置しましょう。module は可能な限り複雑さを排除した振る舞いを記載し、README に用法を記載しましょう。<br>**Root module** が **Nested modules** を呼ぶ場合、個別のリポジトリにせずに全て１つのリポジトリで管理するようにしましょう。
- **Examples.** : module の使用方法を記載した example は `examples/` 配下に配置しましょう。<br>example には README を配置し、用法のゴールを記載しましょう。

#### A minimal recommended module

```bash:例）最小構成
$ tree minimal-module/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
```

#### A complete example of a module

```bash:例）全体構成
$ tree 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/
│   ├── .../
```

## Terraform リポジトリの構造を検討する

Terraform によって システムのインフラを構築する際に実装するリポジトリの構造について検討します。

### 基本コンセプト

検討にあたっての基本コンセプトは次の通りです。

1. コンポーネント分割
2. 環境分割
3. ローカル・モジュール
4. Backend は AWS S3 で管理

#### 1. コンポーネント分割

[書籍『Pragmatic Terraform on AWS』](https://booth.pm/ja/items/1318735)に**「17.4 コンポーネント分割」**という節があります。

ここには次のような記載記載があります。

> 環境は分かりやすい境界です。ひとつの環境につき、ひとつのtfstate ファイルというのは
素直な考え⽅です。しかし、この考え⽅にはデメリットがあります。ひとつのtfstate ファイ
ルでその環境のすべてのリソースを管理すると、ひとつのミスが全体に影響を与えてしまいま
す。また、Terraform の実⾏にも時間がかかります。そこで、環境を複数のコンポーネントに
分割しましょう。
引用：「[KOS-MOS（2019）.『Pragmatic Terraform on AWS』v1.0.0, 146-149](https://booth.pm/ja/items/1318735)」

また、具体的な例として次のような指針が示されています。

- 安定度が高いコンポーネントとそれ以外の分離
- ステートフルなリソース（ストレージやデータストア）の隔離
- （エンドユーザへの）影響範囲が異なるものの分割
- 組織のライフサイクルに関わるリソースの分離
- 関心事の分離

これらの記載を踏まえると、例えば「VPC」「RDS」「EC2」「IAM」といった異なる役割を持つリソースを１つの tfstateファイル で保持することが好ましくないことがわかります。

そのため、今回は記事「[terraformはどの単位で分割すべきか - Qiita](https://qiita.com/yukkyun/items/0981a262a0df18f11121)」を参考にしてコンポーネントの分割を行うことにしました。

#### 2. 環境分割

ここで言う環境とは、Terraform によって構築する対象のサービスが「本番」「検証」「開発」のどの用途で使用されるといった意味で使用します。
この環境の定義の方法は次の２つの方法があります。

- ディレクトリ分割型
- workspace型

インターネット上には「ディレクトリ分割型」で環境を定義する例が多く、記事「[Terraform 運用ベストプラクティス 2019 ~workspace をやめてみた等諸々~ - 長生村本郷Engineers'Blog](http://kenzo0107.hatenablog.com/entry/2019/04/17/103558)」では「workspace型」のデメリットによって「ディレクトリ分割型」に戻したという旨の記載があります。

また、[書籍『Pragmatic Terraform on AWS』](https://booth.pm/ja/items/1318735)には、2018 年9 ⽉に⾏われたHashiCorpJapan Meetup において、workspace型の利用者は少数派で、多くのユーザがディレクトリ分割を行っていたとの記載があります。

一方で、workspace型の活用例も確かに存在し、そちらの実例の方が魅力に感じたため、今回は workspace型 を採用することにします。

#### 3. ローカル・モジュール

「[Module Sources - Terraform by HashiCorp](https://www.terraform.io/docs/modules/index.html)」によると、module block が `source` として指定出来るものとして次の方法が紹介されています。

- [Local paths](https://www.terraform.io/docs/modules/sources.html#local-paths)
- [Terraform Registry](https://www.terraform.io/docs/modules/sources.html#terraform-registry)
- [GitHub](https://www.terraform.io/docs/modules/sources.html#github)
- [Bitbucket](https://www.terraform.io/docs/modules/sources.html#bitbucket)
- Generic [Git](https://www.terraform.io/docs/modules/sources.html#generic-git-repository), [Mercurial](https://www.terraform.io/docs/modules/sources.html#generic-mercurial-repository) repositories
- [HTTP URLs](https://www.terraform.io/docs/modules/sources.html#http-urls)
- [S3 buckets](https://www.terraform.io/docs/modules/sources.html#s3-bucket)
- [GCS buckets](https://www.terraform.io/docs/modules/sources.html#gcs-bucket)

今回、この中で使用するのは「[Local paths](https://www.terraform.io/docs/modules/sources.html#local-paths)」です。

#### 4. Backend は AWS S3 で管理

これはもう当然の選択ですが、Backend に AWS S3 を指定して `terraform.tfsate` を管理する方式を採用します。

## プロジェクト構成

**Standard Module Structure** をベースに、前述の基本コンセプトを考慮して設計したディレクトリ構成が以下です。

```bash:構成例
$ tree sample-project/
.
├── README.md
├── LICENSE
├── .secret
├── .gitignore
├── ...
├── bin             <--- リポジトリの初期化 
│   ├── init_components.sh
│   ├── init_s3.sh
│   ├── init_s3.bat
│   ├── config/
├── environments/   <--- 環境ごとの変数を定義
│   ├── common
│   │   ├── terraform.tfvars
│   ├── product
│   │   ├── terraform.tfvars
│   ├── staging
│   ├── develop
│   ├── default
├── modules/   <--- Local Module
│   ├── nestedA/
│   │   ├── README.md
│   │   ├── variables.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   ├── nestedB/
│   ├── .../
├── components/   <--- コンポーネント分割した設定群
│   ├── network/
│   │   ├── README.md
│   │   ├── main.tf        <--- 基本となる処理を記載
│   │   ├── variables.tf   <--- variables を記載
│   │   ├── outputs.tf     <--- output ブロックを記載
│   │   ├── backend.tf     <--- リモートステートを記載（terraformブロック、dataブロック）
│   │   ├── provider.tf    <--- provider を記載
│   │   ├── .terraform
│   ├── firewall/
│   ├── iam/
│   ├── s3/
│   ├── datastore/
│   ├── application/
│   ├── operation/
│   ├── .../
```

Terraform を実際に実行するのは、　`components`配下にある任意のコンポーネント・ディレクトリの下で実施します。

大雑把な構成図は以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79222/092ed17b-dd6c-f3d2-df23-12c7384cd62e.png)


### Sample Code

`firewall`コンポーネントを例に、コンポーネント配下のコード例を記載します。

#### backend

リモートステート情報を定義する `components/firewall/backend.tf` の例は次の通りです。

```json:components/firewall/backend.tf
terraform {
  required_version = "0.12.3"
  backend "s3" {
    region  = "ap-northeast-1"
    encrypt = true

    bucket = "<unique-bucket-name>"
    key    = "firewall/terraform.tfstate"

    profile = "profile-name"
  }
}

data "terraform_remote_state" "network" {
  backend = "s3"

  config = {
    bucket = "<unique-bucket-name>"
    key    = "env:/${terraform.workspace}/network/terraform.tfstate"
    region = "ap-northeast-1"

    profile = "profile-name"
  }
}
```

terraformブロックの backend を指定すると、以下のような PATH で remote state が作成されます。

unique-bucket-name/env:/${terraform.workspace}/firewall/terraform.tfstate

#### variables

変数情報を定義する`components/firewall/variables.tf` の例は次の通りです。

```json:components/firewall/variables.tf
variable "common" {
  type = map(string)

  default = {
    "default.region"  = "ap-northeast-1"
    "default.project" = "project-name"
  }
}
```

#### main

基本となる処理を定義する`components/firewall/main.tf` の例は次の通りです。

```json:components/firewall/main.tf
module "firewall" {
  source = "../../modules/nestedB"

  common = var.common
  vpc    = data.terraform_remote_state.network.outputs.vpc
}
```

#### provider

providerを定義する`components/firewall/provider.tf` の例は次の通りです。

```json:components/firewall/provider.tf
variable "aws_access_key" {
}

variable "aws_secret_key" {
}

provider "aws" {
  version    = "= 2.18.0"
  access_key = var.aws_access_key
  secret_key = var.aws_secret_key
  region     = "ap-northeast-1"
}
```

### terraform.tfvars

環境ごとに定義する変数ファイル `environments/common/terraform.tfvars` の例は次の通りです。

```json:environments/common/terraform.tfvars
region = "ap-northeast-1"
cidrs = [ "10.0.0.0/16", "10.1.0.0/16" ]
amis = {
  "ap-northeast-1a" = "ami-abc123"
  "ap-northeast-1c" = "ami-def456"
}
```

### workspace

各コンポーネント・ディレクトリ配下に次の４つの workspace を作成します。

```bash:command
terraform workspace list
* default
  develop
  product
  staging
```

### AWS CLI profile

`backend` で指定する AWS S3 にアクセスするための profile 情報を設定します。

```bash:.aws/credentials
[profile-name]
aws_access_key_id = AKIAXXXXXXXXXXXXXXXXXX
aws_secret_access_key = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```

### Backend用 AWS S3 Bucket 作成スクリプト

AWS CLI を使用して S3 Bucket を作成します。

```bash:bin/init_s3.bat
@echo off

set profile_name=%1
set bucket_name=%2

aws s3 mb s3://%bucket_name% ^
  --profile %profile_name%

aws s3api put-bucket-versioning ^
  --profile %profile_name% ^
  --bucket %bucket_name% ^
  --versioning-configuration Status=Enabled

aws s3api put-bucket-encryption ^
  --profile %profile_name% ^
  --bucket  %bucket_name%  ^
  --server-side-encryption-configuration file://config/config-public-access-block.json

aws s3api put-public-access-block ^
  --profile %profile_name% ^
  --bucket %bucket_name% ^
  --public-access-block-configuration file://config/config-public-access-block.json
```

```json:bin/config/config-public-access-block.json
{
    "Rules": [{
        "ApplyServerSideEncryptionByDefault": {
            "SSEAlgorithm": "AES256"
        }
    }]
}
```
```json:bin/config/config-bucket-encryption.json
{
    "BlockPublicAcls": true,
    "IgnorePublicAcls": true,
    "BlockPublicPolicy": true,
    "RestrictPublicBuckets": true
}
```

### 実行

`terraform`コマンドを実行する際は以下のようにします。
コマンドを実装するフォルダに注意してください。

```bash:command_bash
// 環境変数の設定を確認
export TF_VAR_aws_access_key="AKIAXXXXXXXXXXXXXXXXXX"
export TF_VAR_aws_secret_key="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
echo $TF_VAR_aws_access_key
echo $TF_VAR_aws_secret_key

// workspace の選択
terraform workspace select [ default / product / staging / develop ]

// 初期化
terraform init

// Dry run
terraform plan \
-var-file="../../environments/common/terraform.tfvars" \
-var-file="../../environments/$(terraform workspace show)/terraform.tfvars"
```
```powershell:command_Powershell
// 環境変数の設定を確認
$env:TF_VAR_aws_access_key="AKIAXXXXXXXXXXXXXXXXXX"
$env:TF_VAR_aws_secret_key="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
$env:TF_VAR_public_key_path=".\.secret\public"
$env:TF_VAR_aws_access_key;
$env:TF_VAR_aws_secret_key;
$env:TF_VAR_public_key_path;
Get-ChildItem env:

// workspace の選択
terraform workspace select [ default / product / staging / develop ]

// 初期化
terraform init

// Dry run
terraform plan `
-var-file="../../environments/common/terraform.tfvars" `
-var-file="../../environments/$(terraform workspace show)/terraform.tfvars"
```

## まとめ

この記事のまとめです。

### 実現したこと

1. コンポーネント分割によって、tfstateファイルで管理するリソースを分割
2. Workspaces を使用して環境（本番、検証、開発）分割
3. ローカル・モジュールしてリソースを定義
4. AWS S3 を使用した `terraform.tfstate` の管理とバージョニング

### 課題

1. 一つのリポジトリに複数のコンポーネントを管理する形になっており、`terraform init`の度に、実行ディレクトリに `provider`ツールがインストールされる。
2. Module Repogitory が良くわからない。
3. 全体的に手探り

### 対策

1. コンポーネントごとにリポジトリ設計した方が良いのかもしれない。
2. これは使ってみるしかない。
3. （悩ましい）

## おわりに

記事を書いている途中に `v0.11.x` から `v0.12.x` に Terraform のバージョンを上げたら、書いていたコードが全然動かなくなってかなり焦りました。`terraform 0.12upgrade`コマンドを実行しましたが、万能ではないようです。

さすがにメジャーバージョンがまだ 0系だけあって、破壊的な変更というのがあるのですね。

Terraform が持つコアの機能は素晴らしくイメージも付きやすいものですが、実際に触ってみると HCL の記法や各構築対象サービスのお作法を押さえておく必要があり、最初の壁を乗り越えるまでが大変だという印象を受けます。
また、まだツール自体が成長段階ということもあり、次々の新しい記法やお作法が生まれており、そういった情報を把握して追従することにも一手間が生まれそうです。

ただ、そのコスト以上に、Terraform から得られるメリットの方が大きいため、もっと Terraform について知って行きたいと思います。

## 参考

今回の記事を作成するにあたって参考にした情報です。

### ディレクトリ構造

- [Terraform Best Practices in 2017 - Qiita](https://qiita.com/shogomuranushi/items/e2f3ff3cfdcacdd17f99)
- [Terraformにおけるディレクトリ構造のベストプラクティス - Developers.IO](https://dev.classmethod.jp/devops/directory-layout-bestpractice-in-terraform/)
- [shogomuranushi/oreno-terraform - GitHub](https://github.com/shogomuranushi/oreno-terraform)
- [DevOpsを支える今話題のHashiCorpツール群についてに登壇してきました - てっくぼっと！](https://blog.applibot.co.jp/2018/09/27/hashicorp-meetup-180911/)
- [Terraform workspaceを利用する。環境ごとのリソース名の分岐など - Goldstine研究所](https://blog.mosuke.tech/entry/2018/06/16/terraform-workspaces/)
- [TerraformのModuleソースとしてTerraform Enterprise’s Private Module Registryを利用する - GMOメディア エンジニアブログ](https://tech.gmo-media.jp/post/172877318502/tfe-private-repo)

### Terraform の分割単位

- [terraformはどの単位で分割すべきか - Qiita](https://qiita.com/yukkyun/items/0981a262a0df18f11121)
- [Terraformと変数（variable）の話 - CUPSULE CLOUD](https://capsulecloud.io/blog/aws/5078)
- [Feature: Conditionally load tfvars/tf file based on Workspace #15966 - GitHub](https://github.com/hashicorp/terraform/issues/15966)
- [Variable Definitions (.tfvars) Files - Input Variables - Configuration Language - Terraform by HashiCorp](https://www.terraform.io/docs/configuration/variables.html#variable-definitions-tfvars-files)

### State の管理

- [Backend の S3 や DynamoDB 自体を terraform で管理するセットアップ方法 - Qiita](https://qiita.com/saiya_moebius/items/a8f8aa3683c2347d607c)

### サンプル

- [cloudposse/terraform-aws-elastic-beanstalk-application - GitHub](https://github.com/cloudposse/terraform-aws-elastic-beanstalk-application)
- [terraform-aws-modules/terraform-aws-vpc - GitHub](https://github.com/terraform-aws-modules/terraform-aws-vpc/blob/master/variables.tf)

### Terraform v0.12.x

- [terraform v0.12 アップデート terraform 0.12upgrade,terraform 0.12checklistサブコマンド実行結果と、ファイルの変更例 - Qiita](https://qiita.com/miyazawa214/items/4cc69cf67ac17f7b418d)

### AWS S3 for terraform.tfstate

- [独り Terraform 研究所 (1) 〜 Backend についてドキュメントを読んだり, チュートリアルしたり 〜 - ようへいの日々精進XP](https://inokara.hateblo.jp/entry/2018/09/08/151804)
- [S3の暗号化についてまとめてみた(2018年6月版) - 本日も乙](https://blog.jicoman.info/2018/06/s3-encryption/)