🌍

ココナラ的ベストTerraformディレクトリ構造を考える

2023/04/03に公開

こんにちは。
株式会社ココナラのシステムプラットフォーム部でインフラ・SREチームのチームマネージャーをしているよしたくと申します。
前回はインフラ・SREチームの主に組織的な部分を紹介しましたが、今回はより技術的な取り組みを一部紹介します。

https://zenn.dev/coconala/articles/da8787cbade8d0

ココナラではクラウドリソースの管理にTerraformを利用しています。今回このTerraformリポジトリのディレクトリ構造を見直すこととしたので、どのような考え方・ポリシーで構成を考えたのかを本記事で紹介します。

修正前の状態

自分が関わる前はどのような構成だったかというと、以下のような状態でした。

terraform
├── service1/
│    ├── common/
│    ├── development/
│    ├── production/
│    └── stg/
├── service2/
│    ├── production/
├── service2-dev/
├── aws/
│    ├── modules/
├── datadog/
├── digdag/
├── gcp/
├── global/
├── module/
・・・

記載したのは一部ですが、改善の余地しかない状態でした。こうなってしまった背景には以下のような理由があります。

  • ディレクトリ構造を考えるほどのゆとりがなかった
  • 当時IaC化を行ったメンバーのTerraform歴が浅かった

整理しよう

Terraform plan/applyのgithub actions化という別の取り組みを進めるにあたって障壁になっていたことや、環境増設において混乱が発生しやすい状態であったため、重い腰をあげディレクトリ整理活動をはじめました。弊社における整理の思考過程を順に説明します。

ちらかっている第1階層をなんとかする

何よりもまずはterraform配下のごった返している状態をなんとかしたいと考えました。
現時点ではTerraformでの管理対象リソースは、クラウドリソース・Saasの一部設定と考えているため、第1階層は「管理対象のクラウドサービス/Saas/Iaas名」としました。

terraform
├── aws/
├── gcp/
├── datadog/
├── pagerduty/

次に第2階層以降を考えます。
第1階層を上記のようにわけましたが、それぞれに属するサービスの種類・数にかなりの偏りがある状態です。つまり各階層ごとに分けたい単位が異なってくるわけなので、第2階層以降はそれぞれのディレクトリごとに分割単位を変えることにしました。
例えば、DatadogではMonitors程度しか管理したいものがないので、第2階層でディレクトリは終わりです。一方で弊社のほとんどのサービスはAWS上で動いているため、aws/ディレクトリは階層をより深くしたいと考えました。しかし、第2階層以降を考えるにあたってはまだ他に考えないといけないことがあります。

ステートファイルをどうわけるか

いくつかの観点で比較検討をしてみます。

観点 環境別で区切るケース サービス別で区切るケース
applyの回数/修正箇所 ⭕️ 1回ですべて完了 ❌ 場合によって複数回・複数箇所必要
apply速度 ❌ 遅い(ステートが大きいから) ⭕️ 速い(ステートが小さいから)
影響度合い ❌ 大きい(多数のステートが共存するから) ⭕️ 少ない(ステート量が少ないから)
terraformアップデートの影響 🔺 plan確認量は少ないが、必要対応数は同じ ❌ 必要対応数は同じだが、plan確認量が多い
#1. 環境別で区切る例
├── envs/
│    ├── development/
│    ├── production/
│    └── stg/
├── module/

#2. サービス別で区切る例
├── serviceA/
│    ├── module/
│    ├── development/
│    ├── production/
├── serviceB/
│    ├── module/
│    ├── development/
│    ├── production/

メリット・デメリットがそれぞれにありますが、心理的安全性をとりたい気持ちが強く、「サービス別で区切る」方策を選択しました。

  • 万が一誤った(destroyしてしまった等)処理をしてしまったとしても、ステートが小さければその影響範囲が小さくてすむ
  • 複数人が並行して変更作業をいれる場合にステートが大きいと、一方が他方の作業を待つ必要が出たり、ブランチを切ってplanを立てたら自分の知らない変更(destroy)が入って確認の手間が増えたりといったことを防ぐ(可能性はゼロではないのと運用フローで防げるものではある)

もちろんステートファイルが多くなり、terraformのバージョンを更新するときにplanを確認する箇所が多くなり手間がかかるといったデメリットは甘んじて受け入れます。

ところで、環境の切り替えという点ではTerraform workspaceという便利機能が存在しますね。

workspace機能を使うかどうか

結論としては、以下の理由から使用しないこととしました。

Terraform workspaceには、コードを共有しながら環境(ステートファイル)を分けることが可能という強力な強みがあります。
この点だけを見れば導入はぜひともしたいツールなのですが、現実を直視するとなかなかそうもいきません。
環境ごとのリソース差分がほとんどなく開発・ステージング・本番の各リソースがかなりキレイに保たれているシステムである場合にworkspaceは威力を発揮するものと考えています。その点弊社に限らず様々なところで同じだと思いますが、やはり環境間でのリソース差異が指折りで数え切れないほど存在しています。
この状態でworkspaceを使用すると、コード内にその差異を制御するためのcountが都度出現するようになり、結果的に全体の見通しが悪くなる可能性がかなり高いです。
またこれは個人的な経験ですが、いまどこのワークスペースを向いているか間違えたことが過去にあってちょっとトラウマなこともあり、使用を控えました。

public moduleを使うかどうか

AWSやGCPといったメジャーなプロバイダであれば、公式が便利なモジュールを用意しています。
もちろん便利なのですが、こちらも以下の点で不採用とします。

やったか!?

以上のような考えを巡らせつつ、一旦以下のようなディレクトリ構造でFixさせました。

terraform
├── aws/
│     └── coconala/
│        ├── serviceA/
│        │  ├── develop/
│        │  ├── module/
│        │  └── production/
│        ├── serviceB/
│        │  ├── develop/
│        │  ├── module/
│        │  └── production/
│        ├── serviceC/
│           ・・・
├── gcp/

「くぅ〜疲れましたw これにて完結です!」と行きたかったわけですが。。。

3日後👮🏼「あれ。踏み台用のサーバとかdeploy用のサーバってどこにも入れどころがないなあ・・・」

サービスにばかり主眼をおいた結果、マネジメント系のECサーバの居所がなくなってしまいました。そこでリソースの「目的」を明示する階層を設けることにしました。

terraform
├── aws/
│  └── services/    # ← ここを追加
│     └── coconala/
│        ├── serviceA/
│        │  ├── develop/
│        │  ├── module/
│        │  └── production/
│        ├── serviceB/
│           ・・・
│  └── managers/    # ← ここを追加
├── gcp/

今度こそやったか!?

また3日後👮🏼「あれ、共通で使っているバケットはどこに入るのがいいんだろうか?」

serviceAでもserviceBでも利用している共通のリソースが一定数あり、どこで管理するのかかなり迷いました。(serviceに優位性をつけて最優位レイヤーで管理するなど)
この類に関しては仕方がないと割り切り、AWSのリソース種別ごとに管理することとしています。このディレクトリにあるもの=共通リソースであるというマーキングにもなるだろうという考えでもあります。
ただしこのディレクトリにすべてを入れ込むことが可能にはなってしまうため、原則services配下におくという決まりにはしています。

terraform
├── aws/
│  └── services/
│     └── coconala/
│        ├── serviceA/
│        │  ├── develop/
│        │  ├── module/
│        │  └── production/
│        ├── serviceB/
│           ・・・
│  └── managers/
│  └── resources/ # ← ここを追加
│     └── lambda/
│     └── iam/
├── gcp/

正解はひとつじゃない

このように一度Fixさせたあとに、考慮から漏れていたリソースたちがいくつか出てくることはあるでしょう。
これまでのケースでは現行の構成を大きく崩さずに取り込むことができましたが、そうでないケースが出てきたらもう一度いちから考え直すつもりでいます。
またterraformの構成そのものに正解はなく、自チームの運用に沿う形式でポリシーを持って整理できていればそれがベストというのが自分の考えです。

tfファイル記載方法の統一

moduleやmoduleを呼び出すときにいつも決まって書くようなコード(バージョン定義など)の記載方法もこの機会にコーディング規約として明記しました。

module

  • variableにはdescriptionを明記
  • docを生成
    • terraform-docを使用
  • providerとversionは">="で指定

module呼び出し側

  • 環境間にリソース差異がなければ、基本はmain.tfだけ
  • providerとversionは"="で固定(後々tfupdateをかけやすいように)
  • 環境ごとに異なるものを{resource}.tfに書く
    • → diffはmain moduleに渡すvariable diffと、このtfファイルをみれば基本はわかるように
versions.tf
# バージョン固定記載の一例
terraform {
  required_version = "1.3.7"
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "4.49.0"
    }
  }
}

おわりに

わたしたちと一緒に働いてくれるメンバーを募集しています!
もっと詳細に話を聞いてみたい方がいましたら、以下フォームを参照ください。

ブログの内容への感想、カジュアルにココナラの技術組織の話をしてみたい方はこちら

https://open.talentio.com/r/1/c/coconala/pages/70417
 ※ブログ閲覧者の方限定のカジュアル面談の応募フォームとなります!

エンジニアの募集職種一覧はこちら

https://coconala.co.jp/recruit/engineer

SRE求人はこちら

https://open.talentio.com/r/1/c/coconala/pages/49719

Discussion