DIGGLE ではインフラのコード管理(IaC: Infrastructure as Code)のツールとして Terraform を利用しています。
私たちの開発組織では「自動化・効率化できるものはなるべくして、人的ミスや工数の無駄を無くすこと」を大切にしており、そのための手段として Terraform が有力であると考えているためです。
Terraform のコードを書く上で悩ましいポイントの一つにディレクトリ構成があると思います。 DIGGLE ではつい最近、メンテナンス性の向上を目的にディレクトリ構成の見直しを行いました。 本記事では、その見直しの内容について紹介します。
前提
はじめに前提情報として、DIGGLE のインフラ環境を共有します。
- 運用してるプロダクトは一つ
- AWS を用いてインフラ環境を構築
- AWS以外のサービスはTerraformで管理していません
- 本番環境の他に検証環境(ステージング環境)が複数存在
- 全ての環境が Terraform による管理の対象
上記となっています。
従来のディレクトリ構成
見直し前のディレクトリ構成は以下のようになっていました。
aws ├ production # 本番環境用のリソース定義 │ ├ main.tf │ ├ variables.tf │ ├ <resource>.tf │ └ ... ├ staging-1 # 検証環境用のリソース定義 │ └ ... └ ...
( <resource>.tf
は AWS リソースの種類ごとにリソース定義を分割して格納しているファイルです。例えば ECS に関連する ECR や ECS サービスのリソース定義は ecs.tf
というファイルにまとめています。)
構成としては、環境ごとにディレクトリを分けてその中にリソース定義を各々書くというシンプルな形でした。
事業の立ち上げ期で本番環境しかないような状態でコードが書かれて、後から検証環境が必要になり本番環境用のコードを複製して... のようなあるあるパターンで生まれがちな構成だと思います。
環境が少ないうちは大して問題はありませんでしたが、組織が大きくなり検証環境の数も増えていく中で以下のような問題が出てきました。
- ファイルやリソース定義の数が多いため、環境間の差分が分かりにくく比較が難しい
- 各リソースのパラメータ
- 特定の環境にしか存在しないリソース
- リソース追加などの構成変更作業の手間が大きい
- 同じリソース定義を環境の数分書く必要がある
これらの問題を放置しておくと環境が増えるごとにインフラの構成変更の作業コストが増えてしまうことが目に見えていたため、ディレクトリ構成の見直しを行うことにしました。
見直しの方針
今回の構成見直しにおける大方針は「環境間(本番環境、検証環境)でのリソース定義の重複をなくす(= DRY にする)」です。
各環境のディレクトリにある大半のコードが重複しているということが問題の根本原因のため、重複部分を共通化していく仕組みを取り入れました。
Modules vs Workspace
Terraform でのリソース定義の共通化の手段としては Modules と Workspace が候補にあがります。
Modules はリソース定義の一群を再利用可能なテンプレートとしてパッケージ化する仕組みです。
Workspace では workspace という環境の分離単位を作成し、その workspace を切り替えることで同じリソース定義から複数の環境のリソースを作成することが可能です。
Workspace は便利な一方で以下のような懸念点がありました。
- 特定の環境にしか存在しないリソースがある場合に工夫が必要
- 具体的には count を利用する方法 があるが、環境とリソースの対応を網羅的に把握することが難しい(各リソース定義のcountの有無を確認していく必要があるため)
- workspace の切り替え忘れによる誤操作
- Terraform Cloud を使う分には無視できるかもしれない
- 環境の存在自体をコード化できない
- コードからはどのような環境が存在しているか分からない
そのため、DIGGLE では Modules を採用して構成を行っています。
見直し後のディレクトリ構成
見直し後の Terraform のディレクトリ構成は以下の通りです。
aws ├ envs # 環境ごとのリソース定義 │ ├ production │ │ ├ main.tf │ │ ├ variables.tf │ │ ├ local.tf │ │ └ ... │ ├ staging-1 │ └ ... └ modules # 環境間で共通するリソース定義をmoduleとしてまとめる ├ base # サービス間で共通利用されるリソース群 │ ├ outputs.tf │ ├ variables.tf │ ├ <resource>.tf │ └ ... └ services # サービス単位のリソース定義 ├ service-A │ ├ outputs.tf │ ├ variables.tf │ ├ <resource>.tf │ └ ... ├ service-B └ ...
解説
従来からの差分として、ディレクトリを大きく envs
と modules
の二つに分割しています。
envs
は環境単位でリソースをまとめる場所であり、環境差異となる部分は全て envs
下にまとまります。
そして、State ファイルも envs
下のディレクトリ単位で別れることになります。
すなわち Terraform の plan や apply を実行する単位での分割ともいえます。
modules
ディレクトリには Terraform modules を利用して分割・再利用されるリソース定義を配置しています。
services
配下にはプロダクトを構成するサービスの単位でリソース定義を配置します。
この場合、サービスごとに同じような AWS リソースの定義が生まれ得ますが、この重複は許容することにしています。AWS リソースのまとまりごとにさらに module を作成して module から module を使うネスト構造にすることで回避することもできますが、必要以上に構成が複雑化し module 間の依存関係も考慮しなければならないため採用していません。
base
配下にはサービス間で共通して利用されるリソース群の定義を配置しています。
分かりやすいものだと VPC 等のネットワーク系リソースや Route53 のリソースが該当します。
modules
内のリソース定義は env
内の main.tf
から参照する形で利用されます。
# main.tf module "base" { source = "../../modules/base" vpc_cidr = "10.0.0.0" ... } # -> VPC を作成 module "service_a" { source = "../../modules/services/service-A" vpc_id = module.base.vpc_id ... } # -> base module で作成された VPC の ID を受け取り、サービス固有のリソースを作成
環境間での差異は main.tf
で各 module に与えているパラメータに表れます。
そのため、各環境ディレクトリ配下にある main.tf
を確認することで環境ごとの設定を容易に比較することができます。
また、AWS リソースの構成変更を行う場合は modules
内の変更だけで、variables の変更がない限りは envs
内の各環境側の変更は不要なため、従来構成よりも作業コストは削減できます。
補足: 構成変更適用時の運用フローについて
実際に構成変更を行う際には、まず検証環境に変更を適用し動作確認を行い、その後に本番環境へ変更を適用するという形をとると思います。
その場合の構成変更適用には下記の運用フローを想定しています。
- staging ブランチにおいて
module
下のリソース定義を変更 - 検証環境(staging 環境)に
terraform apply
を実行して変更を適用 - staging 環境での動作確認が問題なければ、staging ブランチを main ブランチへマージ
- main ブランチで本番環境に
terraform apply
を実行して変更を適用
本番環境の構成は常に main ブランチの内容と一致することとした上で、検証環境の構成は柔軟に変更ができるという方針による運用となっています。
おわりに
今回はメンテナンス性向上のための Terraform ディレクトリ構成の改善について紹介しました。
「terraform ディレクトリ構成」でネット検索すると多くのプラクティスがヒットしますが、あらゆる組織にフィットするような決定版といえるものはないと考えています。 本記事で紹介したものも2023年時点の DIGGLE に最適だと考える構成であり絶対的な正解というわけではないため、数ある事例の一つとして参考にしていただけると幸いです。
We're hiring!
DIGGLE では共にプロダクトを開発してくれるエンジニアを大募集中です。
少しでも興味があれば、ぜひ下記採用サイトからエントリーください。