TerraformにおけるDRYなディレクトリ構成
背景
元祖ゴリゴリ系エンジニアのpageoです。
Terraformを使用するPJに参加することが増えてきたので、備忘録的にTerraformディレクトリ構成のpageo的ベストプラクティをまとめたいと思います。
前提
ディレクトリ構成を考える上で想定するPJイメージを示します。
FEはSPAでS3 Web Hosting、BEはDockerizedでECS Fargateによるホストをするような一般的な構成を想定しています。
※マルチアカウントやデータ基盤などを考慮に入れ出すと個別性が高くなるので、かなり単純なPJを想定しています。
構成
以下がディレクトリ構成です。
+- pj-root/
|
+---- shared/
| +---- main.tf
| +---- provider.tf
| +---- variables.tf
| +---- modules/
| | +---- alb/
| | +----- alb.tf
| | +----- listener.tf
| | +----- target.tf
| | +----- outputs.tf
| | +----- variables.tf
| | +---- cloudfront/
| | +---- ecs/
| | +---- iam/
| | +---- network/
| | +---- oai/
| | +---- rds/
| | +---- route53/
| | +---- s3/
| | +---- security_group/
+---- env/
| +---- dev/
| | +----- backend.tf
| | +----- locals.tf
| | +----- terraform.tfvars
| | +----- main.tf Symbolic link to /shared/main.tf
| | +----- provider.tf Symbolic link to /shared/provider.tf
| | +----- variables.tf Symbolic link to /shared/variables.tf
| +---- stg/
| +---- prd/
+---- templates/
| +---- ecs/
| +----- sample.json.tpl
+---- backend/
+---- terraform_backend.yaml
Github上にサンプルRepositoryを置いています
説明
各ディレクトリの説明
-
shared/
- 各環境で共通で使用するファイル、moduleを定義する
-
main.tf
,provider.tf
,variables.tf
は各環境毎にSymbolic Linkでリンクさせる -
modules/
に各moduleを定義する
-
env/
- 各環境(Dev/Stg/Prd)構築用のtfファイルを管理する
-
main.tf
,provider.tf
,variables.tf
はSymbolic Linkでshared配下のファルとリンクされている - 各環境毎に修正が必要なファイルは
backend.tf
,locals.tf
,terrform.tfvars
のみ
-
templates/
- ECSのコンテナ定義ファイルや、Lambdaのソースコード、CloudWatch AgentなどのConfigファイルを格納する
-
backend/
- TerraformのBackend向けのS3/DynamoDB構築用のCFnテンプレートを格納する
Symbolic Link
DRYを実現する肝がSymbolic Linkです
※Symbolic Linkについてはこちら参考
各環境のディレクトリにmain.tf
, provider.tf
, variables.tf
をコピペするのではなく、Symbolic Linkを貼ることで一元管理することができます。
shared/
配下のmain.tfにおいて、以下のようにmoduleを実装する必要があることには注意です
正しい書き方
module "sample" {
# シンボリックリンクを貼るディレクトリ(ex: `env/dev`)視点での相対パスとして記述する
source = "../../shared/modules/sample"
sample = local.sample
}
誤った書き方
module "sample" {
# シンボリックリンク元視点での相対パスではエラーになる
source = "./modules/sample"
sample = local.sample
}
tfstateの管理
tfstate管理はremote Backendを使用するのが定石なので、必ず使用しましょう。
この際、AWS管理コンソール or AWS CLIからS3/DynamoDBを作成することもできますが、そこもIaC化するのがベストなのでCloudFormation Templateを使用します。
modulesの注意点
moduleの定義もDRYになるように心がけましょう。
せっかくmodule化しているにも関わらず以下のように実装されているのをよく目にします。
アンチパターン
resource "aws_rds_cluster_instance" "writer" {
identifier = "sample_writer"
cluster_identifier = aws_rds_cluster.main.id
~
}
resource "aws_rds_cluster_instance" "reader" {
identifier = "sample_reader"
cluster_identifier = aws_rds_cluster.main.id
~
}
上記のように実装してしまうと、今後readerインスタンスがもう一つ必要になった場合にsharered/modules/rds/cluster.tf
を修正しなければならなくなります。
また、readerインスタンスの値をコピペして一部の設定値だけ編集するというDRYに違反した運用をすることになりかねません。
上記の問題を回避するため、以下のようにcount, for_each, elementをうまく利用した実装を心がけるのがベストです
正しい実装
resource "aws_rds_cluster_instance" "main" {
count = length(var.cluster.instance_classes)
identifier = "${var.cluster.identifier}-${count.index + 1}"
cluster_identifier = aws_rds_cluster.main.id
instance_class = element(var.cluster.instance_classes, count.index)
~
}
最後に
ここまで色々説明してきましたが、結局は「DRYの原則に従っているか?」を問い続けながらPJ毎にディレクトリ構成を柔軟にブラッシュアップしていくことが大事なのかなと思います。
マルチサイトやBackend/Frontend/DataPipelineなどの要素を考慮する場合は、上記のディレクトリ構成をより拡張する必要がありますし、PJによっては環境(Dev/Stg/Prd)間で共通のAWSリソースを使用する場合もあるかと思いますが、その際も独自にディレクトリ構成を考える必要があります。
自分もまだディレクトリ構成のベストプレクティスを模索中なので、コメント欄よりご指摘やご教授お願いいたします
Discussion