こんにちは、SREグループの水戸 (@y_310)です。今回はSREグループが管理しているTerraformのディレクトリ構成について紹介します。MoTのSREグループでは数十のマイクロサービスを運用しており、その全てのサービスのTerraformテンプレートファイルを1つのリポジトリで管理しています。様々なサービスを構築していく中でディレクトリ構成の共通化や共有モジュールの切り出し方などの方針が固まってきたので設計の考え方について紹介したいと思います。
まず今回ご紹介するディレクトリ構造を採用した根幹となる考え方について説明します。そのために前提となるTerraformで管理される対象のマイクロサービスがどのように設計されているのかを最初に説明します。
マイクロサービス環境においてはあるサービスとそのサービスが使用する様々なインフラリソース (例えばAWSのS3やSQSなど)がサービスの数だけ大量に存在する状態になります。SREグループではマイクロサービスのアーキテクチャ設計において、この各サービスとそれに依存するリソースのオーナーシップを重視し、リソースの共有、つまり1つのリソースに複数のオーナーがいる状態を発生させないようにサービスを設計しています。
また当然ですが開発環境と本番環境といった環境をまたいでリソースを共有するといったこともしません。
Terraformのディレクトリ設計においてはこのサービス設計を前提とし、あるサービスとそのサービスに依存するリソースをまとめて一つの単位として管理することを基本思想としています。そのため、まずサービスのディレクトリがあり、その中に環境のディレクトリがあり、そこにstateを作ることでサービス×環境を1つの単位としてapplyできる構造としました。
ここから基本思想に基づいた具体的なディレクトリ構造についてご紹介します。
各サービスに関連するリソースは1つのサービス用ディレクトリの中で全て定義しているため、基本的に最上位のレベルにはサービスのディレクトリが並ぶ形になっています。
terraform
├ service1
├ service2
├ service3
...
次に各サービスは基本的に開発環境、QA環境、本番環境の3つの環境を持っており、加えて環境に依存しない共通リソース、各環境をまたいで共有しているモジュールを持つためサービス用ディレクトリの内部は以下のディレクトリ構成になっています。
service1
├ common
├ development
├ qa
├ production
└ modules
commonと各環境ごとのディレクトリがstateを管理する単位となっており、ここにエントリポイントとなるテンプレートファイルが配置されています。
development
├ main.tf
├ provider.tf
└ versions.tf
ここまでの構造をすべてまとめると以下のようになります。
terraform
├ service1
│ ├ common
│ ├ development
│ │ ├ main.tf
│ │ ├ provider.tf
│ │ └ versions.tf
│ ├ qa
│ ├ production
│ └ modules
├ service2
├ service3
...
インフラ構成の中にはVPCとその中のサブネットやEKSクラスタといった複数のマイクロサービスを横断して使用されるリソースが存在します。これらの横断リソースは特定のマイクロサービスにオーナーシップをもたせるのは不自然なため、VPCやEKSクラスタだけで1つのサービスとしてみなす形で整理しています。
そのためサービスと同じ最上位レベルのディレクトリを起点にテンプレートファイルを管理しています。
terraform
├ example-vpc
│ ├ development
│ │ ├ main.tf
│ │ ├ provider.tf
│ │ └ versions.tf
│ ├ qa
│ ├ production
│ └ modules
├ service1
├ service2
ただし、この例におけるexample-vpcはあくまで1つ (厳密には環境毎に1つ)のVPCを管理しているものであって、全てのVPCを集中管理するためのものではありません。特定のユースケースのために作られた1種類のVPCを1つのサービスのように見立てて管理しているものになります。このディレクトリの中ではVPCに依存するルートテーブルやInternet Gateway、サブネットを管理しています。
サブネットについては1つ1つは特定のマイクロサービスに依存するリソースともみなせるのですが、IPレンジを適切に割り当てるために集中管理する必要があるので、example-vpcがオーナーシップを持つ、と整理し、各マイクロサービスは割り当てられたサブネットをdataソースによって使用するだけ、という関係性にしています。
基本構造を紹介したところで定義の共通化をするためのmoduleの配置と使い方について解説します。SREグループではmoduleを大きく分けて以下の2つの目的で作成しています。
1つ目の環境間の差異を無くすためのmoduleはサービスディレクトリ内のmodulesディレクトリで管理します。moduleの単位は基本的にAWSサービスなどの単位となります。このmoduleを development/main.tf や production/main.tf から参照することで設定値 (インスタンスサイズなど)以外の差分がない状態を実現しています。なおこのmoduleは特定のservice専用moduleのため必要以上の汎用化はせず環境間の差分になる部分のみ変数化し、差分にならない部分はハードコードした抽象度の低いmoduleです。
service
├ modules
│ ├ s3
│ │ └ main.tf
│ ├ sqs
│ └ iam
├ development
2つ目のmoduleは1つ目のmoduleのパターンがある程度収束し、どのサービスでも共通して必要になる定義が見えてきた段階で全体共通moduleに昇格したものです。
shared module repository
├ aws-frontend
├ aws-s3-internal-bucket
│ ├ README.md
│ ├ main.tf
│ ├ outputs.tf
│ ├ variables.tf
│ └ versions.tf
├ aws-sqs
全体共通moduleは別のリポジトリに切り出した上でサービス内moduleより汎用化した構成で作成しています。またTerraform CloudのPrivate Registryでバージョン管理をしています。汎用的にはしていますがあくまでMoTにおいて必要最小限の機能を持つmoduleとして実装しており、全てのパラメータを変更可能にするといった過度な汎用化はしていません。むしろ変える必要がないと判断したパラメータやリソースを積極的に変更不可能とすることでサービス間での差分を最小化することを狙っています。
例えばS3に関するmoduleについて、サービスの設定ファイルやログファイルを配置するために内部的に使用するバケットもあれば、CloudFront経由でWebコンテンツを配信するためのバケットもあります。これらは一部共通となる設定もありますが、大きく異なる設定もあります。そういったものは無理に一つのS3 moduleで両方を実現するのではなく、ユースケース毎に別のmoduleとして定義することでdynamicや条件分岐を駆使した技巧的なmoduleになることを極力回避するようにしています。
なお、この全体共通moduleは以前はterraformディレクトリ直下にサービス用ディレクトリと並列にmodulesディレクトリを配置して管理していましたが、その場合moduleのバージョン管理ができず、ちょっとした変更でもそのmoduleに依存している全てのディレクトリでapplyが必要になるため心理的に変更が入れづらい状態になっていました。Private Registryを使うことでバージョン管理ができるようになり気軽にmoduleのアップデートができるようになりました。
最後にサービスのmain.tfがどういった構造になっているのか紹介します。サービスによって依存するリソースの種類は違いますが、どのサービスでも概ね以下のような構造になっています。
# --------------------
# Settings
# --------------------
locals {
name = "service1"
namespace = "example"
environment = "development"
}
# --------------------
# IAM
# --------------------
module "iam" {
source = "../modules/iam"
environment = local.environment
service_name = local.name
}
# --------------------
# SQS
# --------------------
module "sqs-payment-event" {
source = "app.terraform.io/organization_name/sqs/aws"
version = "2.1.0"
environment = local.environment
service_name = local.name
number = "001"
queue_name = "payment_event"
}
# 以下依存するリソースを定義するmoduleを同様に列挙していく
ファイル冒頭にvariableやlocalsといった変数定義を配置します。主にサービス名や環境などのパラメータを記述し、シンプルな構成であれば開発環境を構築した後にこのファイルを本番環境にコピーして冒頭の変数部分の記述だけ修正すれば本番環境も完成するようになっています。
変数定義以降は基本的に直接resourceを記述せずmodule経由でリソースを作成し環境間の差異が発生しないようにしています。
IAM moduleはサービス内moduleの例です。IAMの定義はサービス間で差異が大きいためサービス内moduleとしています。
SQS moduleは全体共通moduleの例です。SQSはパラメータの差異はあるものの作成するリソースはどのサービスでも大差ないため、不要な差分が生まれないように基本の型を定めて共通moduleにしています。
実際にはこれ以外にも監視ダッシュボードやアラート設定のmoduleなどいくつかの全てのサービスで共通に定義しているmoduleがあります。
マイクロサービス環境ではインフラを構成するリソースの数がサービス数に応じて肥大化していく傾向にありますが、一貫した設計規則を持つことで複雑さを抑えながらシンプルに管理することができています。サービス設計の段階からリソースのオーナーシップを明確にし、共有リソースを作らないことが一貫性を維持するための鍵だと考えています。
Terraformの管理に課題を感じている方のヒントになれば幸いです。
興味のある方は 採用ページ も見ていただけると嬉しいです。
Twitter @mot_techtalk のフォローもよろしくお願いします!