多層階層型Terraform

大層なタイトルをつけて大丈夫なんでしょうか私。

本日は、Infrabbitで使用している多層階層構造を持つTerraformツリーをご紹介したいかな、と思います。
やってることは単純なのですが、作るのは結構頭をこねくり回されます。コンパイルか。

まず、なぜTerraformを多層階層にするのか、って話ですね。

基本的に、Terraformを書くパターンってスタックで切り分けて、個々のスタックでTerraformを構成し、小さなまとまりをいくつか作る、というのが良くやる手なんじゃないかと思います。当然私も最初はそんなことをしていました。

で、この手法、いくつか問題を感じるわけです。

  1. terraform.tfやらproviders.tfやら大量にほぼ同じものを書く必要が出てくる(のでめんどくさい)
    シンボリックリンクに逃げてもいいんですが、今度はterraform updgradeの時に死ぬ目に合うときがある。ちゃんと常にリファクタし続けろ、という話ではあるんですが、ヒューマンリソースの問題でコードメンテが後回しになるのはよくある話ですね。

  2. data.terraform_remote_stateの嵐になる場合がある
    スタック間に関連性が高い場合に発生します。かといって同一スタックにまとめるわけにはいかない、とか。
    単一プロジェクトの単発ものならともかく、汎用化とか考え始めると、条件分岐がひどいことになっていきます。

  3. リソース相関をとらえにくい
    moduleで作成されていれば、一撃のterraform outputで済むのですが、スタックが独立した小さい構成だと他のスタックとの相関がとらえにくい、というのがあります。慣れなんでしょうが、私はきつかった。オブジェクトで書くのになれている人はいいんでしょうけどねぇ…。インフラは俯瞰するのも必要なので、分散すると脳内がとっ散らかるんですよね。

  4. data.terraform_remote_stateを書くためにあっちこっちのtfstateを参照する定義を書かないといけなくてめんどくさい大変
    これがぶっちゃけ一番ですね。どうやってもマイクロサービス的なので、相関のために他のtfstate参照は多発します。いや、terraform_remote_state使わなくて書ける程度なら最初から困ってたりしません。これを大量に書き始めると、正直わけわかんなくなってきます。しかも、ちゃんとoutputしないと参照できませんしね。
    これは一見2.の話と同じに見えますが、Organizationsでアカウント間相関を持たせた汎用コード、というものを考えたときには、2.は条件付けで発生するのではなく、そもそも主要リソース(VPCとかCIDR Blockとかが)激しく入り乱れ、相互参照を引き起こしまくり、一定条件下では起きる、という話ではなくなる、というものです。
    まあOrganizationsでアカウント間相関作る、という条件だからだ、とも言えますが。でもOrganizations使ってアカウント間相関作らないとかただアカウントがいっぱいあるだけになるだけですし。請求まとめたい、だけのOrganizationsならそれでよいですが、Tower出る前からTerraformでほぼ同じ環境を汎用コード生成させていた私からすれば、ほぼ死活問題でした。(Towerでやれよ)

結局のところ、output構造がヤバいんですね。for_eachの導入でさらに加速しました。それまでhogehog.foo.bar[0]とかのリストをfor_eachに直してoutput構造が変わるとその影響範囲がデカい。terraform state mvしたり、書き直したり、とにかくリファクタの都度えらい目に合う。
これ、参照元の更新では発見されないわけです。まあそれを何とかするのがmoduleの役割なのですが。

参照先が変わったことによって、うっかり別スタックのterraform planで見落としたりすると、リソースの再生成が走ったりして青ざめかねない。

なので、Infrabbitでは「もう一撃で関連リソース全部まとめて使えるようにしとこう」というのと、terraform_remote_stateをなんとか少なめにいけないかしら。という観点から始めていきました。結局多くはなるんですが、その境界線を明確に定めようと。

Organizationを導入済みなので、まずマスターアカウントにterraformが適用されます。この子の大きな仕事は、Organizations内のプロジェクトアカウントのアカウント間相関を定めることをお仕事とします。そのため、基本的にSecurityクラスとして作成されるアカウントへのログ/監査の集約、そのためのバケット、あるいは各アカウントでそれらに承認を出さなければならない処理。GuardDutyとかConfigとかTrailの証跡とかですね。

同時に、基本的にOrganization内で任意にVPC Peeringを可能にするため、各アカウントに割り当てるCIDRを管理。子アカウントリストを持ち、新規作成が生じればOrganizationsでアカウントの発行と、そのアカウント用の「基本的な標準構造」をデプロイするためのTerraformCodeの自動生成。これは主にネットワーク系の管理。ただし、この段階では(特に新規アカウントには)ロールがないので、子の中を自動構成はしない。CF Stackならいけるかもだけど、CFとTerraformの混在は避けたかったのでパス。

子アカウントはそれぞれの「基本構造」をデプロイする専用フォルダがスクリプト生成され(local-execでやってもよかったが)、そこで今度はVPCや基本的なIAM(標準利用向けインスタンスプロファイル、一般的なRDS向けなどのセキュリティグループの発行)を行うTerraformをテンプレートから生成して作成させる。ここの標準構成箇所は専用のConfigをスクリプト生成して、True/Falseのパラメータセットでプライベートエンドポイントの有効無効やNAT Gatewayの制御など、親から引き受けたネットワークの仕様に基づいて実際にアカウント内にネットワークインフラ、認証認可の基本、Config RuleやCloudTrailなどを構成する。アカウント個別でネットワーク内の定義を行いたい場合に向けてのConfigともいえる。というかほぼそのためのもの。

また、SSMへのWrite権限を親に対してIAMロールで発行するのもここでやっている。つまり、アカウント発行→発行したアカウントのテンプレートディレクトリができるので、「とりあえず」Terraform実行。これで基本構成をデプロイ。まあ、とりあえずの段階でネットワーク設定いじってもいいのだけれど、とりあえず、でも構わない。後から構成変更可能だから。

ここのディレクトリにはルールとして、親が出してくる基本構造を有効/無効するConfigをいじること、のみを原則とする。そして、このディレクトリを参照する、子アカウントからのterraform_remote_stateを認めない(システム的にはできないのでルールでしかないが)。
Resourceを書いてもよいが、その情報をこのアカウント以下の子アカウント(具体的にはプロジェクトマスタやクライアントマスタアカウント以下に作られる、開発、本番、検証用のアカウント)とVPC Peeringを通じて共有する場合などは、SSMを経由して情報を渡すことになる。

なので、アカウントから子アカウントに渡したいデータはすべて命名規則に沿ってSSMに登録していくことになる。

ここまでが、親が管理すべきTerraformのコードとなる。子アカウント以下はある程度の自由を認め、ここから先は個々のアカウントやプロジェクトルートにおけるCodeCommitなどに独立したコード体系を認めるものとする。

ただし、当然ルール的なものとなるが、親から発行された環境の破壊は認めない。

つまるところ、結局「どこに明確なラインを引くか」なのである。システマチックに防げない点も多いので、欠陥も多い。できれば子アカウントに発行したリソースをIAMで制御して削除不能にもしたいのだが、アホみたいにJSONが長くなるのでやめた。可読性が犠牲になるどころの話ではなかった。Tagをターゲットにすればいいのかもしれないが。それはそれで同じ名前のタグ作られると消せないリソースができるってことだよなぁ…。となって、めんどくせ、もうルールで運用回避だっ!となった。

そのうち考える。という未来永劫思考を放棄する魔法の言葉で自分をなんとかごまかした感。いや、やらないといけないんだけど。特定のTagKey文字列をConditionでdenyすれば行けるとは思うけど、やってないので何とも言えないですね。

個々のアカウント内のTerraform内では、プロジェクト全体を管理するコアのTerraformコード、そして開発用、検証用、本番用に分離したアカウント個々を管理するTerraformに分離します。が、Terraform.tfに記述するようなterraformブロックなどを除き、本番用=開発用にほとんどが一致しますので、そこはもうシンボリックリンクでつないで、varsだけ分離する方向性。この中では基本的に小さなスタックをmodule化して共通で使用しまくります。

つまるところ、分離レイヤとして

  • ネットワーク/Organizationsの統一化するセキュリティレイヤなどのコアコンポーネント
  • Peering/ログなどのアカウント間コンポーネント
  • プロジェクト内共通用コンポーネント
  • 環境構築用個別コンポーネント(本番、開発、検証を共通化)

という多層階層を組む結果となりました。

書いてて思いましたが、無駄にややこしいことになってるなぁ。自分的には直感的に触れるので困りはしない(terraform_remote_stateだらけだと正直かなり大本リソースを探さなきゃならないのと、影響範囲が見えないので困ってしまう)ので、まあこれでいいか、という感じです。汎用系コード、を突き詰めていくとmodulesですら困るのです。

汎用性のためにmodulesに多様性を持たせれば、module側のvariablesが膨れ上がっていき、AWS側のパラメータ増加でさらにカオス度を増し、Resource書いてるのとこれ変わんなくね?みたいなことになっていきます。Terraformで汎用コードとか考えること自体が実はダメなのかも、とかおもったりはしますが、やっぱり汎用コードで一撃リソース発行ってかっこいいじゃないですか。

書いてるイメージ自体はこんな感じなんですが、実際言語化するのがすごく難しいなぁと感じます。

あとはリファクタリングが少しだけ楽になった(線引きが明確なので)かな、という感じですね。昔作ってた汎用コードは相互依存がキツ過ぎて一か所upgradeしていくと引きずってmodulesを経由して他のコード箇所もupgradeしないとapplyできない、とかになっていたので…。ローカルmodules共有するなって話なんでしょうけど、それじゃ何のためのmodulesなのか、ってなってしまいますしね。