モジュールとループを駆使して可読性の高いterraformを書きたい

モジュールとループを駆使して可読性の高いterraformを目指したメモ

以前の記事 の拡張エキスパンション的な記事。
上記を読んでる前提で書くので、先に読んでおくとわかりやすい、かも。

これはなんの記事か

最近terraformを書くときに、意識的に同じようなリソース定義を繰り返さない実装をしている。
具体的には、

  • 子モジュールの作成
  • ループを使ったリソース定義

の2つを積極的に取り入れている。

たぶん半年くらいすると自分で意図がわからなくなっているので、未来の自分に向けて記録を残す。

そもそも解決したいこと

リポジトリ、もしくはserviceモジュール直下のtfファイルは、そこで管理されているterraform全体で定義したいものを読み解けることが必要だと考えている。
ところが、リソースを律儀に定義しているとtfファイルがどんどん肥大化して可読性が落ちてしまう。これをなんとかしたい。

より具体的にすると、

  • main.tfの中身とリポジトリ/serviceモジュール直下のls結果でなにを定義したいのかがなんとなくわかる
  • ファイルを開いたらスクロールしなくてもなんとなく定義されているものがわかる
  • terraform graphがある程度機能する

あたりを目指す。

実装と意図

依存関係を閉じる目的でのモジュールの作成

基本的にterraformはモジュールを使わなくても使える。
モジュールは再利用したい=複数回定義したいリソース群をまとめるのが一般的な使い方だと思うが、最近はあえて一度きりしか定義しないものもモジュール化している。

これは依存関係をわかりやすくするため。
terraformは、同一ディレクトリ内で定義されたリソースやデータに対して自由に参照できる。
便利な一方で、「このリソースってどこで定義しているんだっけ?」「このリソースそこからも参照してるの?」みたいなことが時々起きる。

モジュールを使うことで、これを防ぐ。
モジュールで定義されたリソースやデータには直接参照できず、またモジュール側もモジュールの外のリソースを直接参照できない。
モジュール-モジュールの外部の入出力はそれぞれvariable, outputで明示的に定義する必要があり、これを安全装置として使おうという試み。

  • variable, outputをいちいち定義するのはめんどうなので、周辺の依存関係を自然とモジュール化するようになる
  • どうしても必要な外部依存はモジュールのREADMEやvariable, outputのdescriptionでドキュメント化される
    また、呼び出し元(main.tfを想定)を見ればだいたいどんなリソースが定義されるのか俯瞰できるようになる、はず

の2点をねらっている。

また、適度にモジュール化されていると、terraform graphの出力がある程度綺麗になってセイシンテキがとてもいい。

ループを使ったリソース定義

まず、このようにlocalsを使ってリソースのリストを作る。

locals {
cname_records = {
"blog" = { value = "cname.vercel-dns.com.", comment = "blog on Vercel", proxied = false }
"k16em.net" = { value = "k16em-net.pages.dev", comment = "LP on Cloudflare Pages / using CNAME flattening", proxied = true }
"www" = { value = "k16em-net.pages.dev", comment = "LP on Cloudflare Pages", proxied = true }
}
txt_records = {
"k16em.net" = { value = "keybase-site-verification=tpgM8Hmp9bkePa9j-qMD7G_Hy1KoCkpEgPi_OQVqzW8", comment = "for keybase" }
"blog" = { value = "keybase-site-verification=udbQAxp-I1L3AHezcTnOAXhSPpq_CbLz1P9KCj9lkfo", comment = "for keybase" }
}
}
それをもとに、for_eachを使って実体を定義する。

# CNAME Records
resource "cloudflare_record" "cname" {
for_each = local.cname_records

zone_id = var.cloudflare_zone_id
name = each.key
comment = each.value.comment
type = "CNAME"
value = each.value.value
proxied = each.value.proxied
allow_overwrite = false
}

# TXT Records
resource "cloudflare_record" "txt" {
for_each = local.txt_records

zone_id = var.cloudflare_zone_id
name = each.key
comment = each.value.comment
type = "TXT"
value = each.value.value
allow_overwrite = false
}
やっていることは単純で、事前に定義したいパラメータのリストを作り、それをループで回しているだけ。
この書き方にはメリットがあり、

  1. localsを見ればそのtfファイルで何を定義しているかがすぐにわかる
    ファイル先頭で定義しておくとよりヨシ
  2. リソース名で悩まなくなる

とくに、リソース名で悩まなくなったのが大きい。
これまでは、cloudflare_record.txt_blog , cloudflare_record.cname_blog のような形でリソースを定義していた。
あれ、これってサフィックスだっけ? プレフィックスだっけ? DNS名ってどこまで書くんだっけ? と、意外と命名には脳ミソを使っていた。
今回から、cloudflare_record.DNSの種別['ドメイン名']の形にしたので、なにも悩まなくなった。

例ではcloudflare_recordを挙げたが、この他にもaws_ecr_repositoryaws_ecs_clusterなどもこの形で定義しており、今後は基本的にこの形で書いていくつもり。
aws_lb_listener_rule のような、複数のblockが定義できるリソースをどうするかは要検討。