terraform plan が遅いとき — refresh の負荷、並列度、スタック分割の合図

読了 4分

スタックが育つと、ある日から terraform plan が数分かかり始めます。結論を先に書くと、plan 時間の本体は変更計算ではなく refresh です。state にあるリソースごとに実際の API を読んで現在の状態を確認する段階なので、所要時間はコードの複雑さではなくリソース数 × API 往復で決まります。 だから対策も計算の最適化ではなく API 読み取りを減らす方向で、オプションでしのいで限界が来たらスタック分割が根本対策です。state と plan の動作原理は基礎 #5 state の正体で扱いました。

時間はどこへ行くのか — refresh の構造 #

plan は 3 段階で動きます。state を読み、state に記録されたリソースごとに provider を通じて実際の状態を照会し(refresh)、その結果とコードを比較して変更を計算します。最後の計算はローカルの演算で、リソース数千個でも一瞬です。ボトルネックは真ん中の refresh で、リソース 1 つにつき最低 1 回の API 呼び出しが出ます。

リソース 500 個のスタックなら 500 回以上の API 往復です。デフォルトの並列度 10 で割っても往復の遅延が積もり、より大きな問題は AWS API の rate limit です。呼び出しが集中するとスロットリングが掛かり、provider がバックオフの再試行をする間に体感時間が急激に伸びます。plan の遅いスタックで CloudTrail を見ると Describe 系の呼び出しが壁のように積まれているのが、この構造です。

オプションのはしご — それぞれの正しい用途と限界 #

-refresh=false は refresh を飛ばして state をそのまま信じるオプションです。効果は劇的ですが前提があります。state と実物が一致しているという確信です。正しい用途はコードを繰り返し修正しながら plan を何度も回す開発ループの途中です。さっき refresh されたばかりだとわかっているので、繰り返しの回では飛ばしても安全です。逆に最終 apply 前の plan、PR レビューに載せる plan は refresh を含める必要があります。drift があるのに飛ばすと、存在しない状態を根拠に変更を計算することになります。drift の管理自体は運用 #2 の主題です。

-parallelism はデフォルト 10 の同時作業数の調整です。リソースが多くスロットリングの掛かっていない区間では 20〜30 に上げて短縮の効果が出ますが、rate limit にすでにぶつかっているスタックでは、上げるほどスロットリングと再試行が増えて逆効果です。上げてみて CloudTrail の ThrottlingException が増えたら戻す、という実測の調整が必要です。

-target は性能の道具としてよく誤用されます。特定リソースだけ指定すれば refresh の範囲が減って速くなるのは事実ですが、このオプションの実際の効果は依存グラフの一部だけが適用された状態を作ることです。-target を繰り返す運用は、スタック全体の整合がいつ最後に確認されたのか誰も知らない状態へ流れていきます。緊急対応の隔離手段としてだけ使い、日常の性能問題の答えにはしません。

CI 側には構造的な選択肢がもう 1 つあります。PR ごとに回る plan とは別に、drift 検知用の refresh を夜間スケジュールに分離する方式です。PR の plan は直前のスケジュール実行の信頼を土台に速く回り、全体の refresh は 1 日 1 回回って drift を捕まえます。

根本対策 — 分割の合図を読みます #

オプションは症状の緩和で、リソース数自体が大きいスタックの根本対策は分割です。次の合図が重なったら分割の時期です。

  • plan が 5 分を超え始めます: PR レビューのループで plan を待つ時間がレビュー自体を妨げる水準が基準です。
  • state のリソース数が数百個を超えます: 絶対の基準ではありませんが、数百個を超えると refresh 時間とロックの競合が一緒に増えます。
  • 変更頻度の違うものが 1 つのスタックにあります: 1 日に何度も変わるアプリ層と年に数回しか変わらないネットワーク層が 1 つの state にあると、アプリのデプロイのたびにネットワークまで refresh する無駄が構造化されます。
  • ロック待ちが日常になります: 複数の人・パイプラインが 1 つの state を巡ってロックの競合を起こす頻度が増えたら、性能の問題であると同時に協業の問題です。

分割の基準(層別・変更頻度別・影響範囲別)とスタック間の参照方法は運用 #5 大規模の構造化で扱いました。性能の観点で要点だけ足すと、分割の効果は plan 時間をリソース数に比例して減らすと同時に、よく変わるスタックだけがよく refresh されるように頻度を再配置する点にあります。

まとめ #

  • plan 時間の本体は refresh で、所要はリソース数 × API 往復で決まります。rate limit に掛かると非線形に悪化します。
  • -refresh=false は開発の繰り返しループ専用です。最終 apply 前と PR 用の plan では refresh を飛ばしません。
  • parallelism は実測で調整します。スロットリングがすでにあるスタックで上げると逆効果です。
  • -target は性能の道具ではなく隔離の道具です。繰り返しの使用は部分適用状態を日常化します。
  • plan 5 分超、リソース数百個、変更頻度の混在、ロック競合が重なったら、オプションではなくスタック分割が答えです。
X