Mobile Factory Tech Blog

技術好きな方へ!モバイルファクトリーのエンジニアたちが楽しい技術話をお届けします!

Terraform を管理するリポジトリのディレクトリ構成とその思想

こんにちは。ブロックチェーンチームのソフトウェアエンジニアの id:odan3240 です。

tech.mobilefactory.jp

上記の記事で紹介した通りユニマ/ガレージのインフラは Terraform で管理されています。

この記事では Terraform を管理するリポジトリのディレクトリ構成とその思想について紹介します。

前提

Terraform を管理するリポジトリは2020年の1月頃に開発されたものです。

当時の最新版の Terraform のバージョンは 0.12 でした。

当時の Terraform のバージョンでのディレクトリ構成の紹介であり、現在の最新版のベストプラクティスに沿わない可能性があります。

ディレクトリ構成

リポジトリルートのディレクトリ構造は次の通りです。以降で紹介するディレクトリ構造は説明のために一部簡略化しています。

$ tree -L 1 .                               
.
├── README.md
├── environments
└── modules

environments のディレクトリ構成は次の通りです。

$ tree -L 3 environments
environments
├── production
│   ├── components
│   │   ├── app
│   │   ├── base
│   │   ├── log
│   │   └── rds
│   └── tfbackend.tfvars
└── staging
    ├── components
    │   ├── app
    │   ├── base
    │   ├── log
    │   └── rds
    └── tfbackend.tfvars

環境の名前でディレクトリを作成し、その中に components というディレクトリを作成しています。各 components の中のディレクトリについては後述します。

次に modules のディレクトリ構成は次の通りです。

$ tree -L 2 modules
modules
├── iam_role
│   ├── main.tf
│   └── variables.tf
├── security_group
│   ├── README.md
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── ssm_placeholder_parameter
│   ├── README.md
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
└── components
    ├── app
    ├── base
    ├── log
    └── rds

iam_role/security_group/ssm_placeholder_parameter は汎用的なモジュールです。これらの説明は今回の記事の趣旨とはずれるので、これ以上は説明しません。

components 以下のディレクトリは environments/production/componentsenvironments/staging/components と同じ構造です。

以上がリポジトリのディレクトリ構成です。ここからはなぜこの構成にしたかを紹介していきます。

思想

当時このディレクトリ構成を考えるのにあたって大事にしていた思想について紹介します。

Web フロントエンドの思想にある Presentational/Container Components パターンを意識する

ディレクトリ構造の紹介で説明したとおり、modules/componentsenvironments/$ENVIRONMENT/components には対応関係があります。これは Web フロントエンドの設計の思想にある Presentational/Container Components を意識してこの形になりました。

Presentational and Container Components | by Dan Abramov | Medium

Presentational/Container Components パターンをざっくり説明すると Web フロントエンドを作るコンポーネントを、ボタンやレイアウトなどの UI の形を決める Presentational Components と、API でのデータの取得を行う Container Components に2つに分類する、というものです。このパターンは提唱されてからしばらく時間が経っており、コンポーネントをさらに細分化するパターンが知られています。しかし、この「汎用的な見た目を提供するグループ」と「具体的にデータを流し込むグループ」に分けて整理する思想は、後に考えられたパターンにも影響を及ぼしています。

Terraform の環境分離の方法の1つに Module 方式が知られています。自分は Web フロントエンドに土地勘があったので、Presentational/Container Components パターンに当てはめながら設計を考えることにしました。

modules/components は Presentational Components に対応してます。このレイヤーではアプリケーションサーバが動作する Fargate のサブネットや RDS のパラメーターグループなどが定義されています。RDS のスペックやアプリケーションサーバに付与する IAM ロールに指定する S3 バケットの ARN などは、tf ファイルにハードコーディングしないようにしています。

environments/$ENVIRONMENT/components は Container Components に対応してます。このレイヤーでは modules/components で定義したインフラの構造に対して具体的な値を流し込んでいます。ALB/Fargate/RDS に付与するセキュリティグループや各種必要な ARN をこのレイヤーから variable 経由で指定できるようになっています。

tfstate の分離はライフサイクルを意識する

サービス全体のインフラを1つの tfstate にまとめると、tfstate が巨大化して plan/apply が遅くなるという問題が知られています。これに対応するためには、いくつかの tfstate に細分化する必要があります。

今回紹介したディレクトリ構造だと components/appcomponents/rds などのコンポーネントごとに tfstate が生成されるようになっています。これはアプリケーションサーバのインフラ構成の刷新の可能性は RDS の刷新の可能性と比べて高い、というライフサイクルの違いに着目しています。アプリケーションサーバのインフラ構成の刷新があって components/app2 というコンポーネントが生えても RDS やログに関する tfstate には干渉しない仕組みになっています。

これは同じ IaC のライブラリである、CloudFormation のスタック分割のベストプラクティスでも触れられている内容です。

docs.aws.amazon.com

Terraform Workspaces は環境を分けるのに使用しない

本番環境、stg 環境など環境を分離する手法で採用される手法として Terraform Workspaces が知られています。しかし今回のリポジトリではこれまでに紹介した通り Modules 方式で各環境を分けています。

この理由は最新版のドキュメントでは記述が削除されていますが、次の由来によるものです。

In the 0.9 line of Terraform releases, this concept was known as "environment". It was renamed in 0.10 based on feedback about confusion caused by the overloading of the word "environment" both within Terraform itself and within organizations that use Terraform. https://developer.hashicorp.com/terraform/language/v1.2.x/state/workspaces

元々 Workspaces は environment という名前で知られていました。しかし本番環境、stg 環境などを意味する環境との混同を避けるために名前を変更した経緯があります。これは Workspaces は環境を分離するために用意された機能ではないことを指していると考えて環境を分離するためには使用しない判断を下しました。

運用してみての感想

terraform import コマンドとの相性が良い

Terraform には既存の AWS のリソースを tfstate に読み込む terraform import というコマンドがあります。.tf ファイルの作成にはこのコマンドを使用して次の流れで行うようにしていました。

  • stg 環境用の AWS アカウントでリソースを作成
  • 対応するコンポーネントが存在しないならコンポーネントを作成 (hoge とする)
  • terraform import で stg 環境のコンポーネントの tfstate に読み込む
  • environments/staging/components/hoge で plan の diff がなくなるまで modules/components/hoge 以下の .tf ファイルを編集
  • 本番環境への反映は environments/production/components/hoge で apply することによって行う

この流れで作業すると modules/components/hoge をベースに本番環境が作成されるため、環境ごとの差異を極力抑えることができていました。

components が数がどんどん増えていく

リポジトリを作成した当初のコンポーネントの数は4つか5つでした。しかし2年以上の月日によってアプリケーションに必要なインフラが増えた結果、コンポーネントの数が16個まで増えました。

16個に増えると新しい環境を作るときに16回 apply する必要があるため、一時的な手間は増えてしまいました。

app コンポーネントの肥大化

当初の app コンポーネントは API 用のサーバを Fargate で用意する定義だけが書かれておりシンプルな内容でした。しかし開発が進むにつれて、batch 用や worker 用のサーバの構成の定義が増えたりした結果、app コンポーネントが肥大化しました。

これは plan/apply の時間の増加に繋がりつらいです。

app コンポーネントでは ALB やフロントエンドで使用する S3 + CloudFront も管理しています。app という名前だと役割が大きいので fargate コンポーネントという命名にして、ALB などの設定が別のコンポーネントに生えるようにするのが良かったかもしれません。

まとめ

Terraform のリポジトリのディレクトリ構成とその思想を紹介しました。

その中でも特に、Web フロントエンドのコンポーネント分割のパターンである Presentational/Container Components パターンをベースに Modules 形式で各環境を分離する手法を紹介しました。

この記事が Terraform のディレクトリ構造に悩む人に1つのアイディアとして受け取っていただけると幸いです。