Terraformアンチパターン(2019年版)


はじめに

  • Infrastructure as Code(以下IaCと略します)って最近では当たり前のように実践されてますよね。特にterraformはかなりユーザが多く、開発のスピードも速い印象です。
  • IaCを実現できたインフラエンジニアの皆さんの多くが次に直面する問題はコードの保守運用に関する事柄ではないでしょうか?
  • terraformもコードなので、アプリケーションのコードと同じように保守性(テスト容易性、理解容易性、変更容易性)を意識する必要があります。ただコード化しただけでは属人性を排除したとは言えないと思います。
  • 保守性の高いterraformって具体的にどう書けばいいの?と周りに聞いてみても、巷には「ぼくのかんがえた最強のterraformベストプラクティス」が乱立していて、自転車置き場の議論になりがちです。
  • また、v0.12前後でterraformの記法が大きく変わったので、古い情報はあまり参考にならなかったりします。
  • 一方で、terraformのつらい話をするとみんなウンウン頷いてくれるので、アンチパターンについては共有認識を作りやすいのかなとふと思いました。
  • これまで、複数の現場で中規模以上のterraformの運用保守をやってきて、何度かこれはイケてないなと思うterraformを読んだり書いたり(ゴメンナサイ)直したりしてきたので、terraformのアンチパターンをまとめてみました。

Terraformアンチパターン集

依存関係が足りていないresource

  • マネジメントコンソールをポチポチしながらリソースを作成し、後でterraform化した時にありがちなパターンです。
  • terraformで依存関係の定義が足りておらず、別の環境で試してみるとエラーとなってしまいます。
  • 途中まで手動でリソースを作成したときは、一度関連するリソースを全て削除してテストしてみましょう。

moduleを使わず1つの階層内で全てのresouceを定義する

構成例

## [補足]実際にはもっとファイル数が多かったり、命名規則がぐちゃぐちゃだったりします。
.
├── bar.tf
├── ec2.tf
├── ecs.tf
├── elb.tf
├── foo.tf
├── iam.tf
├── lambda.tf
├── outputs.tf
├── rds.tf
├── s3.tf
├── security_group.tf
├── ssm.tf
├── subnet.tf
├── variables.tf
└── vpc.tf
  • terraformは1つの階層内でNamespaceを共有するため、名前の衝突が起きる可能性が高いです。これを避けるためにはresource名を長くしてしまいがちです。
  • moduleを使わないと抽象化ができないため、大きなプロジェクトになると全体を把握しづらくなります。

大規模プロジェクトを1つのstateで管理する

  • 「1アカウントにつき1stateで運用しなければならない」なんてルールはどこにも存在しないのに、なぜか先入観を持ってこの構成にしてしまうケースが多いです。
  • この構成にすると1か所変更してplanするだけで、全てのリソースの現在の状態を確認してしまうため、実行時に非常に時間がかかります。
  • 1つのモジュールや変数を変更した時の影響範囲が大きくなって、変更容易性が失われます。

DRYでないコード

  • 複数のstateを分けて運用し始めると今度は同じリソース定義を何度も記述しがちです。
  • DRYでないコードを書くと変更容易性が落ちる上に、古い設定が残り続けてトラブルの元になります。
  • 別のstateで管理しているリソースの状態を参照したい場合はData Sourcesを使いましょう。

古い記法で記述されたコード

  • terraformはかなり開発の流れが早く、古い記法をつかっていると最新のバージョンが使えないこともあります。

resource, variables, outputsを同じファイルに書く

  • terraformではvariablesだけ参照したいケースが多発するため、ファイルを分けたほうが読みやすいです。

秘匿情報をハードコードしてしまう

  • terraformに限らずやめたほうがいいです。
  • terraform実行時のログに秘匿情報が残ってしまいます。
  • プライベートリポジトリで運用していたとしても、意図せず秘匿情報をそのままコピー/フォークして事故が起きる危険性があります。
  • SSMパラメータストア, Hashicorp vaultなど秘匿情報を管理するSaaSにデータを格納しておきましょう。

再利用しづらいmodule

  • 抽象化を意識せずにmoduleを作ると、再利用しづらいmoduleができてしまいます。
  • module内部にハードコードされた設定があると、他のサービスでmoduleを再利用するときにも無理して同じ設定を使うか、任意の設定値を投入できるようにリファクタする必要が出てきます。
  • 変数にはdefault値を設定することができるので、よく使われるであろう設定値をdefaultとして定義しておくと使いやすいmoduleができます。
  • 抽象度を下げて特定のユースケースに特化したmoduleを作る場合は、それがわかるようなディレクトリ名にしておかないと、想定外の利用をされてしまう可能性があります。

Default値のない変数

  • module内でvariablesを設定したものの、default値を定義しない場合、moduleを呼び出す時に必ず値を代入しなければならず、コード量が増大します。
  • もちろん全ての変数にDefault値を設定すべきとは思いませんが、積極的に使うべきだと思います。

Typeの指定のない変数

  • string型以外の型を期待する変数では、変数を定義する時に型を明示したほうが可読性が高いです。
  • 型を明示しておくことで、想定外の型を代入された場合、実行前にエラーに気づくことができます。

深すぎる階層構造

  • 必要以上に深い階層構造を作ると全体の見通しが悪くなります。

module in module

  • terraformではmodule内部にmoduleを定義することができますが、大抵の問題は他の方法で解決できます。
  • コードの見通しが悪くなりますし、階層構造も深くなりがちです。
  • 循環参照してしまう危険もありますので、乱用するのはおすすめしません。
  • 抽象度レベルの異なるmoduleを作って、命名の工夫でわかりやすくすれば、使い所もあるかもしれません。

アンチパターンを避けた無難な構成

  • 下の例は私が最近よくやる構成です。
.
├── api_a
│   ├── backend.tf
│   ├── main.tf
│   └── variables.tf
├── api_b
│   ├── backend.tf
│   ├── main.tf
│   └── variables.tf
├── common
│   ├── backend.tf
│   ├── iam_users.tf
│   ├── route_tables.tf
│   ├── security_groups.tf
│   ├── subnets.tf
│   ├── variables.tf
│   └── vpc.tf
├── job_a
│   ├── backend.tf
│   ├── main.tf
│   └── variables.tf
├── job_b
│   ├── backend.tf
│   ├── main.tf
│   └── variables.tf
└── modules
    ├── alb
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    ├── api_gateway
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    ├── aurora
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    ├── cloudwatch_event
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    ├── codebuild
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    ├── fargate
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    ├── lambda
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    ├── rds
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    └── s3
        ├── main.tf
        ├── outputs.tf
        └── variables.tf

運用例

  • 例えばAPIを増やしたい時は、api_aをディレクトリごとコピーしてapi_cを作成、variables.tfでパラメータを調整、backend.tfでstateファイルの保存先を変更するだけでセットアップが完了します。
  • stateファイルの保存先をapiごとに分けることで、既存APIのstateファイルに影響を与えることなく新規構築できます。
  • APIの数が100個に増えたとしても、moduleを再利用しているので、コード量はそれほど増えません。
  • ネットワーク関連の設定をcommonでまとめて設定することで、CIDR重複などのリスクを防げます。
  • IAMユーザ関連の設定をcommonでまとめて設定することで、ユーザごとの権限を把握しやすくしています。
  • 一方でlambdaやfargateなどに付与するIAMロールに関してはそれぞれのmodule内で定義した方が依存関係を理解しやすいです。variablesでIAMポリシーの内容を渡せるように定義すれば、各APIごとに別の権限を割り当てたいときにも対応できます。
  • dev,stg,prdで同じコードベースを使ったデプロイをしたい場合はworkspaceを使います。

参考資料