ベストプラクティスといいつつ、どのような風にやりたいかで変わるというのは往々にしてあります。 ベストプラクティスは求めても意味ないのでどうでもいいとして、いろんなパターンのメリット/デメリットを把握して現状に即しているのかどうかは考え続ける必要があります。
ということで、長年頭を悩ませて納得感があまりない代表はTerraformです。 今回は以下の記事を読んでて、普段やっているTerraformの構成について書いてなかったので記録として残しておきます。
TL;DR;
- ディレクトリ分離 + モジュールでやっているけど、基本的にローカルモジュールに寄せている
- locals.tfで差分を完全に抑えて、モジュール内は分岐なしの型で環境の違いを表現している
実はWorkspaceで分離してもさほど問題ないといえば問題ないことに読み直して、ふと考えが変わってきました。 ただし、Workspaceのstate後戻りができないのは厳しく、この環境だけしれっと追加みたいなカジュアルさは欠けるというのはあります。 そういう意味で、やはりWorkspaceは手放しで使う気にはなれないものがあり、微妙だなぁと感じます。
HashiCorp社の Terraform ベストプラクティス
ベストプラクティス自体よりも、開発元がどのようなコンセプトを持っているかは注目に値します。
Terraform開発元のHashiCorp社(以降、公式)はたびたびBest Practice的なことを謳いますが、具体的な構成を示していた昔のリポジトリはarchiveされて久しいものがあります。だめじゃん。
Nebulaworks の例
公式はさまざまなリポジトリ構成があるといいつつ、具体例をほぼ示していない。と感じつつも、Nebulaworksの例を2020/Jan/18にブログで紹介したプレゼン動画にて構成を見ることができます。(0.11.11の頃なので0.14の今ではproviderやversionsなどは古く感じます)
この中で以下のモジュール構成例を示しています。(いわゆるよくあるモジュール構成) プレゼンの内容はいうほど特筆することはないです。
. ├── env │ ├── dev │ │ ├── main.tf │ │ ├── module.tf │ │ └── reseources.tf │ ├── prod │ │ ├── main.tf │ │ ├── module.tf │ │ └── reseources.tf │ └── staging │ ├── main.tf │ ├── module.tf │ └── reseources.tf └── modules └── resources.tf
公式は構成を示しません。仕方ないのでコミュニティから学ぶ旅に出ましょう。(2021年になっても変わってない)
なぜ Terraform の構成は難しいのか
改めて、Terraformの構成が難しいのはなぜか考えてみると、terraformはパスベース + .tf拡張子で参照ファイルや変数のスコープ1が決まります。 スコープが驚くほど広いわりに、言語機能としてそのスコープ内でファイル限定といった表現力は持たないため、ファイル構造でカバーすることになります。 これが難しい原因の多くを生み出しているように感じます。 具体的にあげると、
- 言語としてはシンプル、だけどスコープも広いために収拾がつきにくい
- インフラ構成はただでさえブラストラディウスが広いにも関わらず、言語による「影響を限定させるサポート」に乏しい。2
- 同じコードを何度も書くのは事故の原因なので、なるべくDRYにいきたいが運用しやすさとのトレードオフはしないようにするサポートが乏しい。3
言語機能が貧弱というほうが近いですが、言語機能が豊富な言語は、その分複雑になりがちです。
ということで、今現在よく使っている構成と、きっかけとなったブログのセクションについて考えます。
よく使っている構成
私は実行環境をTerraform Cloudに一本化しています。(以前はatlantisを用いていました)
今現在よく使うディレクトリ構成は次のものです。 EKSは特に悩みやすいのでサンプルを置いておきます。
├── common (VPC分離の場合のみ) ├── dev │ ├── compute.tf │ ├── data.tf │ ├── iam.tf │ ├── locals.tf │ ├── modules │ │ ├── compute │ │ │ ├── ecr │ │ │ │ ├── main.tf │ │ │ │ ├── outputs.tf │ │ │ │ └── variables.tf │ │ │ ├── eks │ │ │ │ ├── iam │ │ │ │ │ ├── eks_cluster_role │ │ │ │ │ │ ├── main.tf │ │ │ │ │ │ ├── outputs.tf │ │ │ │ │ │ └── variables.tf │ │ │ │ │ ├── eks_node_role │ │ │ │ │ │ ├── main.tf │ │ │ │ │ │ ├── outputs.tf │ │ │ │ │ │ └── variables.tf │ │ │ │ │ └── eks_pod_role │ │ │ │ │ ├── data.tf │ │ │ │ │ ├── main.tf │ │ │ │ │ ├── outputs.tf │ │ │ │ │ └── variables.tf │ │ │ │ ├── main.tf │ │ │ │ ├── oidc │ │ │ │ │ ├── bin │ │ │ │ │ │ └── oidc_thumbprint.sh │ │ │ │ │ ├── main.tf │ │ │ │ │ ├── outputs.tf │ │ │ │ │ └── variables.tf │ │ │ │ ├── outputs.tf │ │ │ │ └── variables.tf │ │ │ └── kubernetes │ │ │ ├── main.tf │ │ │ └── variables.tf │ │ └── iam │ │ ├── account │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── variables.tf │ │ ├── role │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── variables.tf │ │ └── user │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── outputs.tf │ ├── providers.tf │ ├── variables.tf │ └── versions.tf ├── production ├── staging └── modules ├── iam │ └── iam_role │ ├── main.tf │ ├── outputs.tf │ └── variable.tf └── xxxxx
原則
この構成は、運用においてモジュール構成に分岐や入れ替えなどを起こさないことを目的に組んでいます。 そのため、次の原則に基づいています。
- Workspaceを使ったdev/staging/productionの環境切り替えは用いない
- 環境ごとの差分はlocal変数で表現し、モジュールやdataを含むコードは、構成が全く同じなら同じとする。(./locals.tf)
- ルートの共通Moduleには普遍的で必ず環境差分が起こらない共通化するモジュールのみ配置する。(./modules)
- モジュールには、アプリの事情を込みで記述し、ルートのlocals.tfに記述した環境ごとのパラメーターをvariables.tfで受け取る。(./dev/modulesなど)
- 環境の違いがある場合、その環境のローカルモジュールのみ変更を加える。(基本的にlocals.tfのパラメーターのみで対応できるようにモジュールに改修を入れる)
この原則だけわかっていると、ただのシンプルなModule構成です。 誰が見ても初見ですぐにわかり、影響を分離できるようにするのがゴールです。
要はモジュール構成ではモジュールを共通化するのが前提ですが、初めからローカルモジュールに展開するよう割り切った構成です。
terraform ファイルの記載内容
モジュール内部の構成は見ればわかるので、ざっくりとdevフォルダ配下のファイル説明だけ見てみます。
- data.tf: dataリソースの定義
- locals.tf: local変数定義する。環境ごとの定義はここですべて表現される。環境差分はここだけ発生する
- modules: ロジックはすべてモジュールに閉じ込める。compute.tfなどにresourceを直接書くことはなし
- outputs.tf: よくある出力定義
- providers.tf: プロバイダー一覧
- 0.13からproviderがいい感じになったので、main.tfをやめてproviders.tfとversions.tfにきっちり分離できる
- variables.tf実行時のvariables.tf差し込みはterraform cloudでのクラウドへの認証差し込みなど必須な情報以外入れない
- versions.tf: terraformブロックとプロバイダーバージョン、backend指定
残りのcompute.tfやiam.tfは、ただのモジュール参照です。 main.tfに羅列していると使いにくいので、いつでもリソースをパージできるように分けています。 tfstateの分離はもちろんしますが、するしないはプロジェクト規模でも変わって来たりするので、ここでは簡単のため同一stateとしました。
NOTE: terraform を使って upgrade とかしていると、providerとversionsはこういう形になる気がします。(0.12で自動生成されたversions.tf にはじめ戸惑いましたが、0.13で納得いく変更がきた)
この構成で最も設計しないといけないこと
ローカルモジュールの設計が大事になります。
- 異なる環境でも同じlocal変数の型でモジュールが受けるようにできるようにすること
- ローカルモジュールの凝集
- moduleのoutput
環境差分はlocals.tfにのみ許容することはやってみると全然できます。 しかし、いざやるとdevだけ2つリソース作って、stagingやproductionでは1つだけ、などといった差に出会ったりするでしょう。 そういったものは型でうまく解決するようにします。
例えば、1つ、2つといった数の差異がモジュールで閉じ込められるようにmapを使ったりmapは型表現が弱いのでset(object) を使ったりといった工夫は必要です。 共通モジュールでも、分岐ではなく、こういう型での解決をするほうが望ましいことは多いので別に違いはないといわれるとそうですね。
元記事の迷子
私もいつも迷子なので今時点の考えを書いておきます。
workspace 使うのか使わないのか問題
使いません。
とはいっても、元記事の「今どのworkspaceにいるのか」という問題が発生しません。 これは、Terraform Cloudのremote applyを必須にしているため、Workspaceを使わずとも環境ごとにTerraform CloudのWorkspaceは分離されているからです。
使わない理由は別にあります。 workspace機能を用いた場合、環境ごとに差分が生じた場合に、差分を吸収するために分岐を用いたりするのがいやだったからです。 workspaceを使うと共通リソースで完結するメリットは、「分岐を見逃した場合など発生することが当然あるであろう状況で環境を破壊するリスク」に比べて割が合わないと思っているからです。
私は「人間はミスをする」と思っているので、ミスが起こっても影響が抑えられる構成を好みます。
環境の分割方法問題
環境の分割は、見ての通り「環境ごとにディレクトリを作る」です。 シンプルな反面めんどくさくなるのが「漏れがあったら」という問題ですが、それはlocals.tfへの環境差分の限定とモジュール設計で対応します。
module 設計問題
- prod/stgそれぞれで構築する: ローカルモジュール
- 将来にわたって完全に同一で差分がない場合のみ、共通モジュールで定義
- 環境別に構築したりはしないが両方で利用する: ローカルモジュール
- 将来にわたって環境ごとの破壊的な変更が起こらず、完全に共通利用でき環境差分がない場合は、共通モジュールへ配置
- 片方だけ構築する: ローカルモジュール
今行っている構成では、まったく同じ構成なら、diffを行ってもlocals.tf以外に差分が出ません。 そのため、devで検証が終わりstagingやprodに適用する場合も、モジュールは丸っとディレクトリごとコピーで構わないので変更もれリスクが抑えられます。(localst.tfだけ残して残り丸っとコピーでいい)
共通Modules + 分岐が必要なものを、ローカルモジュールに振っているので、そういう悩みが起こらない設計にしています。 同じものを複数書くのではなくコピーにしているのは、まったくスマートではないですが、悩むことより事故は起こらないものです。
自作 module か、公式 module か問題
私は自作moduleが多いようです。
公式moduleを一時期に優先して使っていたのですが、awsモジュールなどで破壊的変更があったので苦しくなってやめました。 とはいえ、vpcなどは公式モジュールでも十分に柔軟で、破壊的変更も起こされた記憶がないので使ってもいいです。(この見極めが難しいのは公式モジュールの問題です)
variable の配置方法問題
これはベストプラクティスに乗らない理由がないので遵守します。
- ファイル分割: 分割する
- 格納データの型: 厳密に指定する。map(string) よりはobject(型) で定義して、差し込むときはmapで指定するのがよい
- デフォルトの値有無: 基本的にデフォルト値はなし
- デフォルト値があってもいいのは、入らなくても動作にまずい影響が起こらない場合のみ
- デフォルト値をないようにすると、値を意味を持って指定することになるため意図のない設計が発生しないため
NOTE: 事故があってからデフォルト値なしに考えを変えました。デフォルト値、意図がない限り基本的に避けたほうがいいです。
過去に参考にした構成
コミュニティから学ぶことは多くあります。 今の構成はこれらの構成を自分でやってみて、納得がいかない部分を変えています。
terraform-aws-providerコミッターが公開している以下の構成はよく見かけるものです。
small
. ├── README.md ├── main.tf ├── outputs.tf ├── terraform.tfvars └── variables.tf
medium (largeは未完成でmediumと同じ)
. ├── README.md ├── modules │ └── network │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── prod │ ├── main.tf │ ├── outputs.tf │ ├── terraform.tfvars │ └── variables.tf └── stage ├── main.tf ├── outputs.tf ├── terraform.tfvars └── variables.tf
他にもありますが、コンセプト似てますね。
他にもレイアード構成とかもやってた時期がありました。
書籍としては、「実践Terraform AWSにおけるシステム設計とベストプラクティス」が良書です。module構成から、ローカルモジュールにもう少し振ってもいいと思ったきっかけは、この本でした。
実践Terraform AWSにおけるシステム設計とベストプラクティス (技術の泉シリーズ(NextPublishing))
他に、「Google Cloud Platformで学ぶTerraform 〜実践編〜」などもテストが手集めに書かれています。(一方で構成はあまり触れていません)
Google Cloud Platformで学ぶTerraform 〜実践編〜 第2版
おわり
terraformを使い始めて、もう使って5年たつような気がしますが、ディレクトリ構成には今も納得がいかないです。とはいえ、過去に試した構成よりも納得度と、変更の楽さ、影響度の限定ができているので今はこれで。
CDKやPulumiのような、プログラミングの一般的なルールで構成できるものはこういうところが圧倒的に楽です。 とはいえ、PulumiやCDKを書いてからterraformに戻ると、型を意識せずに依存関係が解決されて楽極まりないのもあり、なんとも難しいものだと感じます。
どっちもいいし、どっちもまだまだ改善の余地があるので、引き続き迷子を楽しんでいきたいものです。
ベストプラクティスがないのはよいでしょう。公式なモジュールの定型もHashiCorp社はしないでしょう。残念ですが。
蛇足
リソース名のsuffix
いろんな経験を積んだ結果、アカウント分離だろうと、リソース分離だろうと、リソース名のsuffixには_dev
などの環境名を含んで設計するといいと思っています。
あと、CDKやPulumiのようなランダムリソースsuffixも好ましいですが、文字長制限で怒られたりするので2021年にもなって各クラウドはいい加減にしてくれと引っかかる度に思っています。
文字種 4もそうだけど、文字長制限5が許されるのは201x年で終わってほしかったけど、現実はそんなに甘くない。
Terraform公式で示しているベストプラクティス
公式は、主に次のようにTerraformをどのように運用に乗せていくかのベストプラクティスを示しています。 とはいえ、内容はどのように現状を把握して使える状態にもっていくかがメインです。
具体的なTerraform構成は触れていないものの、Terraform Cloudの文脈でプラクティスをいくつか示しています。 3.3.4を要約します。(要約は気になる方はぜひ元文章もどうぞ)
- VCSリポジトリ/ブランチとTerraform CloudのWorkspaceをマッピングさせて
- AppやServiceの各環境は、同じリポジトリ/ブランチのTerraformコードで管理し
- 異なる環境はvariablesで表現しつつWorkspace事に適切に設定する
NOTE: 加えて、環境ごとにブランチを分けるのではなく、1つの正規ブランチをもとに全環境に適用することにも触れています。
また、この中で自作モジュールの作成基準についてダイアグラムを示しています。(同時にナレッジシェアしようといったことも言っています)
また、Terraformのチュートリアルにおいて、モジュールをプログラミング言語によくあるライブラリ、パッケージなどと似たものと触れつつ、ベストプラクティスを示しています。
We recommend that every Terraform practitioner use modules by following these best practices:
- Start writing your configuration with modules in mind. Even for modestly complex Terraform configurations managed by a single person, you'll find the benefits of using modules outweigh the time it takes to use them properly
- Use local modules to organize and encapsulate your code. Even if you aren't using or publishing remote modules, organizing your configuration in terms of modules from the beginning will significantly reduce the burden of maintaining and updating your configuration as your infrastructure grows in complexity
- Use the public Terraform Registry to find useful modules. This way you can more quickly and confidently implement your configuration by relying on the work of others to implement common infrastructure scenarios
- Publish and share modules with your team. Most infrastructure is managed by a team of people, and modules are important way that teams can work together to create and maintain infrastructure. As mentioned earlier, you can publish modules either publicly or privately. We will see how to do this in a future tutorial in this series
しかし私は構成の例が欲しいのであった。
モジュールのベストプラクティス
モジュールを書く時の注意がいくつかあります。 とはいえ、別に大したことはいってないので普通にやればいいのではないでしょう。
- Dependency Inversion
- Multi-cloud Abstractions
- Data-only Modules