📖

Terraformのディレクトリ構成に関する学び

2023/03/30に公開

よく言えば柔軟、悪く言えば

Terraformは terraform applyを実行したディレクトリ直下に存在する拡張子がtfのファイルをすべて読みこんでくれます。
そしてリソースごとの依存関係はTerraform側が勝手に判断してよしなにやってくれるので、tfファイルがどんなファイル名でどんな順番で書かれていようとも動作に影響はありません。
これがものすごく便利な一方で、縛られてないからこそファイルの分割自由度が高く、ベストプラクティスの模索が大変でもあります。

個人的ベストプラクティス

弊社(というか私)は基本的にはリソースの種類ごとにファイル分割するという手法を用いていました。
具体的には以下のような構成です。

terraform/
├── common/
│   ├── acm.tf
│   ├── apigateway.tf
│   ├── cloudfront.tf
│   ├── dynamodb.tf
│   └── lambda.tf
└── envs/
    ├── dev/
    │   └── main.tf
    ├── stg/
    │   └── main.tf
    └── prd/
        └── main.tf

この構成だと同じ種類のリソースが大量に生まれた場合に1ファイルの情報量が膨大になってしまうことに気が付きました。
特に、dynamodb.tflambda.tfは同列リソースが多くなるのでこの構成だとコンフリクトもするし、可読性も低いしでいいことがありません。

そこで次の発想としては、以下のようにすべてのリソースタイプをディレクトリに変えて、それらをモジュールとして読み込むという方法です。lambda.tf内でlambdasディレクトリ以下をモジュールとして読み込んでいます。

terraform/
├── common/
│   ├── acm.tf
│   ├── apigateway.tf
│   ├── cloudfront.tf
│   ├── dynamodb.tf
│   ├── dynamodbs/
│   │   ├── table1.tf
│   │   └── table2.tf
│   ├── lambda.tf
│   └── lambdas/
│       ├── function1.tf
│       └── function2.tf
└── envs/
    ├── dev/
    │   └── main.tf
    ├── stg/
    │   └── main.tf
    └── prd/
        └── main.tf

注意しなければならないのは、同モジュール内ではリソースのoutput値は簡単に参照できますが、モジュールをまたぐと変数化してモジュールに渡すという記述が必要になります。
なので、モジュールを分割すればするほど記述内容は増加していってしまいます。
これの前段でcloudfront等のほぼ1つしかリソースがないであろうものまですべてモジュール化してしまうということも考えましたが、上記の理由により管理が煩雑になってしまうのでやめました。

リファクタリングに注意

さて、ここまでをまとめると、

  • モジュール化すると変数の渡し合いが起きて管理が煩雑
  • モジュールは少ない方が基本的にはいい
  • 1ファイルが大きくなりすぎるならファイルは分割した方がいい
  • ファイル群をグルーピングするためにモジュール化する

ということがわかりました。
ここで思いつくのは、

ファイルが大きくなってきたときにモジュール分割するようにリファクタリングすればいいじゃん

というリファクタリングに任せようという考えです。

しかし、これには罠があります。
Terraformはリソースのことリソースの種類とリソース名で個体識別しています。
例えば、

resource "aws_dynamodb_table" "atomic_counters" {
  name           = "atomic_counters"
  billing_mode   = "PROVISIONED"
  read_capacity  = local.capacity_units_mid
  write_capacity = local.capacity_units_mid
  hash_key       = "key"

  attribute {
    name = "key"
    type = "S"
  }
}

というリソースであれば、
aws_dynamodb_table.atomic_countersという個体識別名です。
ですが、モジュールを介した場合、モジュール名も識別名に含まれるようになります。
モジュールを呼び出すコードは以下のようなコードです。

module "dynamodbs" {
  source                     = "./dynamodbs"
}

こうなるとこのリソースの識別名は
module.dynamodbs.aws_dynamodb_table.atomic_countersとなります。
識別名が変わるとTerraformにとってはそれは識別名が変わったとは判定されず、

  • aws_dynamodb_table.atomic_countersは削除
  • module.dynamodbs.aws_dynamodb_table.atomic_countersは新規作成

という判定になります。
Lambdaの場合、リソースが再作成されたところで何の影響もないので構わないのですが、DynamoDBの場合は再作成されてしまうとDBの中身が消えてしまうので注意が必要です。

terraform state mvというコマンドがあるにはあるのですが、ひとつひとつ置き換えていくのは現実的ではありませんし、何よりCDによる自動デプロイから遠ざかってしまいます。

以上のことから、ファクタリングに任せようはかなり危険な考えです。
あらかじめシステムの大きさを予測し、適切なディレクトリ構成で作り始めることがベストプラクティスです。

エックスポイントワン技術ブログ

Discussion