tech.guitarrapc.cóm

Technical updates

Terraform の構成

ベストプラクティスといいつつ、どういう風にやりたいかで変わるというのは往々にしてあります。 ベストプラクティスは求めても意味ないのでどうでもいいとして、いろんなパターンのメリット/デメリットを把握して現状に即しているのかどうかは考え続ける必要があります。

ということで、長年頭を悩ませて納得感があまりない代表が Terraform です。 今回は以下の記事を読んでて、普段やっている Terraform の構成について書いてなかったので記録として残しておきます。

blog.serverworks.co.jp

TL;DR;

  • ディレクトリ分離 + モジュールでやっているけど、基本的にローカルモジュールに寄せている。
  • locals.tf で差分を完全に抑えて、モジュール内は分岐なしの型で環境の違いを表現している。

実は Workspace で分離してもさほど問題ないといえば問題ないことに読み直して、ふと考えが変わってきました。 ただし、Workspace の state後戻りができないのが厳しく、この環境だけしれっと追加みたいなカジュアルさは欠けるというのはあります。 そういう意味で、やはり Workspace は手放しで使う気にはなれないものがあり、微妙だなぁと感じます。

HashiCorp社の Terraform ベストプラクティス

ベストプラクティス自体よりも、開発元がどのようなコンセプトを持っているかは注目に値します。

Terraform開発元のHashiCorp 社(以降、公式)はたびたびBest Practice 的なことを謳いますが、具体的な構成を示していた昔のリポジトリは archive されて久しいものがあります。だめじゃん。

github.com

Nebulaworks の例

公式はさまざまなリポジトリ構成があるといいつつ、具体例をほぼ示していない。と感じつつも、Nebulaworks の例を2020/Jan/18 にブログで紹介しプレゼン動画で構成を見ることができます。(0.11.11 の頃なので0.14の今ではproviderやversions などは古く感じます)

www.hashicorp.com

youtu.be

youtu.be

この中で以下のモジュール構成例を示しています。(いわゆるよくあるモジュール構成) プレゼンの内容はいうほど特筆することはないです。

.
├── 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

f:id:guitarrapc_tech:20210516222216p:plain
Nebulaworks Module Directory Structure

公式では構成は示されないので、仕方ないのでコミュニティに学びにいく旅に出ることになります。(2021年になっても変わってない)

なぜ Terraform の構成は難しいのか

改めて、Terraformの構成が難しいのはなぜか考えてみると、terraform はパスベース + .tf 拡張子で参照ファイルや変数のスコープ1が決まります。 スコープが驚くほど広いわりに、言語機能としてそのスコープ内でファイル限定といった表現力は持たないため、ファイル構造でカバーすることになります。 これが難しい原因の多くを生み出しているように感じます。 具体的にあげると、

  1. 言語としてはシンプル、だけどスコープも広いために収拾がつきにくい。
  2. インフラ構成はただでさえブラストラディウスが広いにも関わらず、言語による「影響を限定させるサポート」に乏しい。2
  3. 同じコードを何度も書くのは事故の原因なので、なるべく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 など)
  • 環境の違いがある場合、その環境のローカルモジュールのみ変更を加える。

この原則だけわかっていると、ただのシンプルな作りです。 誰が見ても初見ですぐにわかり、影響を分離できるようにするのがゴールです。

要はモジュール構成の共通モジュールを、初めからローカルモジュールに展開するように割り切った構成です。

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で納得いく変更がきた)

www.terraform.io

この構成で最も設計しないといけないこと

ローカルモジュールの設計が大事になります。

  • 異なる環境でも同じ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コミッターが公開している 以下の構成はよく見かけるものです。

github.com

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

他にもありますが、コンセプト似てますね。

kenzo0107.github.io

dev.classmethod.jp

future-architect.github.io

他にもレイアード構成とかもやってた時期がありました。

書籍としては、「実践Terraform AWSにおけるシステム設計とベストプラクティス 」が良書です。module構成から、ローカルモジュールにもう少し振ってもいいと思ったきっかけは、この本でした。

www.amazon.co.jp

他に、「Google Cloud Platformで学ぶTerraform 〜実践編〜」などもテストが手集めに書かれています。(一方で構成はあまり触れていません)

techbookfest.org

おわり

terraformを使い始めて、もう使って5年たつような気がしますが、ディレクトリ構成には今も納得がいかないです。とはいえ、過去に試した構成よりも納得度と、変更の楽さ、影響度の限定ができているので今はこれで。

CDK や Pulumi のような、プログラミングの一般的なルールで構成できるものはこういうところが圧倒的に楽です。 とはいえ、Pulumi や CDK を書いてから terraform に戻ると、型を意識せずに依存関係が解決されて楽極まりないのもあり、なんとも難しいものだと感じます。

どっちもいいし、どっちもまだまだ改善の余地があるので、引き続き迷子を楽しんでいきたいものです。

ベストプラクティスがないのはいいですが、公式でそろそろモジュールの典例はしめしてもいいのではと思いますが、HashiCorp社なのでしないでしょう。残念ですが。

蛇足

リソース名のsuffix

いろんな経験を積んだ結果、アカウント分離だろうと、リソース分離だろうと、リソース名のsuffix には _dev などの環境名を含んで設計するといいと思っています。 あと、CDK や Pulumi のようなランダムリソースsuffix も好ましいですが、文字長制限で怒られたりするので 2021年にもなって各クラウドはいい加減にしてくれと引っかかる度に思っています。

文字種 4もそうだけど、文字長制限5が許されるのは201x年で終わってほしかったけど、現実はそんなに甘くない。

Terraform公式で示しているベストプラクティス

公式は、主に次のようにTerraform をどのように運用に乗せていくかのベストプラクティスを示しています。 とはいえ、内容はどのように現状を把握して使える状態にもっていくかがメインです。

www.terraform.io

具体的なTerraform 構成は触れていないものの、Terraform Cloudの文脈でプラクティスをいくつか示しています。 3.3.4 を要約します。(要約は気になる方はぜひ元文章もどうぞ)

  • VCS リポジトリ/ブランチと Terraform Cloud のWorkspace をマッピングさせて
  • AppやServiceの各環境は、同じリポジトリ/ブランチのTerraformコードで管理し
  • 異なる環境は variables で表現しつつ Workspace 事に適切に設定する

NOTE: 加えて、環境ごとにブランチを分けるのではなく、1つの正規ブランチをもとに全環境に適用することにも触れています。

f:id:guitarrapc_tech:20210516213408p:plain
Part 3.3.4 Create Workspaces

また、この中で自作モジュールの作成基準についてダイアグラムを示しています。(同時にナレッジシェアしようといったことも言っています)

f:id:guitarrapc_tech:20210516214532p:plain
Part 3.2.3 Create Your First Module

また、Terraform のチュートリアルにおいて、モジュールをプログラミング言語によくあるライブラリ、パッケージなどと似たものと触れつつ、ベストプラクティスを示しています。

learn.hashicorp.com

We recommend that every Terraform practitioner use modules by following these best practices:

  1. 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.
  2. 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.
  3. 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.
  4. 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

www.terraform.io


  1. せめて local 変数がファイル内部でスコープがとどまるならまだしも

  2. 影響を限定させる手段としてモジュールやstate分割を使っていくことになる

  3. Workspace やモジュール参照で同一ソースを使うなどを使っていくことなる。

  4. - とか _ とか使える文字種がずれたりするのはとても悪い文化

  5. 64文字制限もあれだけど、32文字制限とか許せない