
この記事はSansan Advent Calendar 2025の21日目の記事です 🎄
STREET FIGHTER 6でGrand Masterになるべく日々練習しています。Sansan Engineering Unit Infrastructureグループの藤田です。 自分の所属しているチームでは主にTerraformを利用してクラウドインフラのリソースを管理しています。Claude Codeを活用して、肥大化したTerraformのStateファイル分割を試みました。
導入
注意: 本取り組みは Claude Sonnet 4、Opus 4を利用したものです。今更の投稿になってごめんなさい
背景と課題
自チームで管理しているとあるシステムのAWSインフラは、本番、ステージング、開発環境ごとに単一のTerraform State(以下State)でリソースを管理していました。各環境のStateには750個程度のリソースが含まれていました。このStateの肥大化により、次の問題が発生していました。
- 実行時間の長期化
terraform planが90秒程度かかり、開発効率が著しく低下
- 影響範囲の拡大
- 小さな変更でも全リソースのチェックが走るため、意図しない変更が混入するリスクの増加
- 並行開発の困難
- 複数の開発者が同時に作業する際、
-targetオプションに頼らざるを得ず、Stateの不整合やレビューが困難に
- 複数の開発者が同時に作業する際、
これらを解決するため、TerraformのStateファイルを分割することに取り組みました。
以前から検討していましたが、全リソースを手作業で分割・移行するには工数が多くかかるため、着手できずにいました。しかし、Claude Codeの活用で工数を大きく削減できることを期待し、今回着手を決めました。
設計
まず、Claude Codeを活用して設計書を作成しました。設計書には、次の内容を記載しました。
- ディレクトリ構成、依存関係
- ディレクトリに配置されるリソースの一覧
- 命名規則
- 実装方針
- 移行手順
ディレクトリ構成、依存関係
State分割後のディレクトリ構成、ファイル名を記載しました。これがないと実装においてClaude Codeが意図を汲み取れずに 2、3往復ぐらい余計なやりとりが発生します。Terraformのディレクトリ構成にはデファクトスタンダードが存在しません。Claude Codeにも実現したいディレクトリ構造を正確に伝えることが重要だと感じました。今回は次のようなディレクトリ構成になりました。ディレクトリ構成は組織やシステムに応じて最適なものが異なるため、参考程度と捉えてください。
root/
├── base/
│ ├── network/
│ │ ├── prod/
│ │ │ ├── main.tf
│ │ │ ├── providers.tf
│ │ │ └── terraform.tf
│ │ ├── stg/
│ │ ├── dev/
│ │ └── module/
│ │ ├── vpc.tf
│ │ ├── subnets.tf
│ │ ├── route_tables.tf
│ │ ├── nat.tf
│ │ ├── nacl.tf
│ │ └── variables.tf
│ └── audit/
│ └── ...
├── app/
│ ├── common/
│ │ ├── iam-roles/
│ │ │ └── ...
│ │ ├── kms/
│ │ │ └── ...
│ │ ├── security-groups/
│ │ │ └── ...
│ │ └── logs/
│ │ └── ...
│ ├── storages/
│ │ ├── s3/
│ │ │ └── ...
│ │ ├── messaging/
│ │ │ └── ...
│ │ └── database/
│ │ └── ...
│ ├── services/
│ │ ├── compute/
│ │ │ └── ...
│ │ ├── serverless/
│ │ │ └── ...
│ │ ├── networking/
│ │ │ └── ...
│ │ ├── secrets/
│ │ │ └── ...
│ │ └── cicd/
│ │ └── ...
│ └── iam-policy-definitions/
│ ├── iam-policies/
│ │ └── ...
│ └── iam-attachments/
│ └── ...
└── other-apps/
├── app-a/
│ └── ...
├── app-b/
│ └── ...
├── app-c/
│ └── ...
└── app-d/
└── ...
関連するAWSサービスをある程度まとめて1State、1Moduleでディレクトリを分離しました。同一AWSアカウントにメインとなるシステムのほかに複数の小規模なシステムが共存していたため、これらはシステムごとのディレクトリを配置して管理しました。
baseとappディレクトリを分離しレビューにおけるインフラチームとアプリケーションチームの責任分界点を明確にし、CODEOWNERSを機能しやすくしました(あくまでレビューの観点であり、それぞれのチームが必要に応じてPRを出します)。
State間の依存関係が一方向になるように設計しました。後述しますがState間の値の受け渡しはterraform_remote_state、tfe_outputsを利用しないため、依存関係が複雑にならないように注意しました。State間の依存関係はREADMEで管理しています。
base (network/audit) ↓ app/common (iam-roles/kms/security-groups/logs) ↓ app/storages (s3/messaging/database) ↓ app/services (compute/serverless/networking/secrets/cicd) ↓ app/iam-policy-definitions (iam-policies/iam-attachments) ↓ other-apps (app-a/app-b/app-c/app-d)
ディレクトリに配置されるリソースの一覧
Claude Codeを利用して各ディレクトリに配置されるリソースの一覧を作成しました。実装ではこの一覧を参照して新しいtfファイルを作成します。リソース一覧と既存のtfファイルを比較するスクリプトを作成して、抜け漏れがないかを静的に確認しました。実際にスクリプトで確認したところ、リソース一覧に記載できていないリソースがありました。設計の根幹に関わる部分は可能な限り静的解析によるチェックを介入させるべきだと感じました。
命名規則
State分離と合わせてリソース名を整理するため、命名規則を記載しました。実リソースに変更は加えないため、Terraformリソースのみが対象です。
- スネークケース
- リソース種別(eg: subnet, iam_role, etc...)をリソース名に含めない
- 最小3文字、最大63文字
加えて、リソース種別ごとにいくつかサンプルを記載しました。
# サブネット ## パターン: [tier]_[az] resource "aws_subnet" "public_a" {} # パブリックサブネット AZ-a resource "aws_subnet" "public_c" {} # パブリックサブネット AZ-c resource "aws_subnet" "private_a" {} # プライベートサブネット AZ-a resource "aws_subnet" "private_c" {} # プライベートサブネット AZ-c resource "aws_subnet" "database_a" {} # データベースサブネット AZ-a resource "aws_subnet" "database_c" {} # データベースサブネット AZ-c ## 用途別サブネット resource "aws_subnet" "app_a" {} # アプリケーション用 resource "aws_subnet" "lambda_a" {} # Lambda用 resource "aws_subnet" "build_a" {} # ビルド用
実装方針
Claude Codeによるコード生成で守って欲しいルールを記載しました。
State間の値の参照にterraform_remote_state、tfe_outputsを利用しない
HCP Terraformをbackendに利用している場合、State間で値を参照するためにはWorkspaceでのアクセス制限を設計する必要があります。本システム規模でそこまで複雑なアクセス制限を設計する必要はないと判断し、これらを利用しないようにしました。これによりState単体での変更が容易になると考えます。
リソースのIDは引数で渡さず、Module内でData Resourceを利用して直接参照する
今回のModuleは当システム内でのみ利用を想定しているため、汎用性の高いInputを定義する必要はないと判断しました。他Stateで作成したリソースのIDを参照したい場合、Module内でData ResourceとNameタグを利用して参照するようにしました。開発・ステージング・本番でAWSアカウントを分離していることで、全環境で同じNameタグを利用してリソースを参照できます。
移行手順
移行に必要な手順をコマンドレベルまで記載しました。内容については後述します。
実装
新しく作成するState毎に以下のプロンプトを実行して実装を進めていきました。
ultrathink
本 terraform リポジトリの State 分割を進めていきたいです. まずは以下資料を確認してください
- 設計
- <設計書のpath>
- 過去の実装サンプル
- <terraform repo>/pull/xxx
対象は app/services/serverless です. 上記に沿って, 以下を実装してください
- 新しい root module app/services/serverless の作成
- 既存 root module の修正
- 移行対象の resource を削除
- 削除した resource を参照している場合, data resource へ置き換え, 参照するように修正
- State 分離に必要な terraform state mv コマンド一覧の作成
- テスト
- コマンド
- export AWS_DEFAULT_PROFILE=<profile name>
- terraform init/plan
- 対象 ディレクトリ
- envs/dev
- app/services/serverless/dev
最初はPlan Modeで実行して、実装計画を確認してからAuto Applyで実装を進めました。過去のブログでも紹介されていますが、複雑な実装に着手する際はPlan Modeから始めるのが良いと思います。ultrathinkのキーワードも合わせて使いました。
Plan Modeを活用する場合、Opus Plan Modeが便利です。Opus Plan ModeはPlan Modeの時のみOpusを利用して、通常の実装ではSonnetを利用してくれます。実装計画という複雑なタスクのみをOpusに任せることで、コスト最適化とRate Limitの対策が可能です。
また、複数のClaude Codeのセッションを並行して実行するためにgit-worktreeを利用しました。git-worktreeは複数のブランチで同時に作業を可能とするGitの公式サブコマンドです。詳細はClaude Codeのドキュメントが参考になります。
移行
Stateの移行手順は次の記事を参考にしました。
SansanはTerraformのbackendにHCP Terraformを利用しています。HCP Terraform上で複数State間の terraform state mv を実行できないため、terraform state pull で一度ローカルにStateをダウンロードしてから terraform state mv でStateの分離を実行します。分離が完了したら terraform state push でStateをHCP Terraformに反映します。最後に terraform plan/apply で実装とStateの整合性を確認します。
terraform import による移行も検討しましたが、 terraform state mv と比較して移行漏れが発生しやすいと判断して採用を見送りました。 terraform state mv の場合、元Stateからのリソース削除と新Stateへのリソース追加が同時に行われるため、移行漏れが発生しにくいです。
加えてdefault_tagsを利用して全リソースに is_migrated=true|false タグを付与することで、作業完了後の移行漏れを検知できるようにしました。
結果
各環境のStateを1->20に分離し、Stateあたりのリソース数は多くても100程度に収まりました。 terraform plan の実行時間は 30 秒程度に短縮されました。また開発において同じStateを操作する機会が減り、 -target オプションを利用することなく terraform apply を実行できるようになりました。
また、元々1.5ヶ月分の工数で完了するスケジュールでしたが、Claude Codeを活用することで1ヶ月分の工数で完了することができました。特に序盤は試行錯誤の時間が多く含まれているため、次回はこれ以上の工数削減が期待できると考えます。
課題
実際に移行を進めてみて感じた課題、改善点について共有します。
事前のリファクタリング
移行前にリファクタリングを進めておくべきだと感じました。具体例として、deprecatedなリソースやattributeの移行に伴う実装です。 aws_s3_bucket の lifecycle_rule や aws_iam_role の managed_policy_arns などを移行する際、Claude Codeはこれらの代替となる次のリソースを実装してきました。
aws_s3_bucket_lifecycle_configurationaws_iam_role_policy_attachment
この場合、移行前後でリソース種別が変わってしまうので terraform state mv による移行ができなくなってしまいます。移行作業をシンプルに保つためにも事前にリポジトリ全体のリファクタリングを進めておくべきでした。
Linter の整備
コードを実装方針に従って修正したい場合、直接プロンプトで指示を与えるよりもLinterの結果を生成AIに与えた方が効果的だと思います。Claude Codeで実装・テスト・修正のサイクルを回すためにもtflintなどで静的解析ができる状態にしておくことが重要です。
SDD の活用
長文となる設計ドキュメントをインプットにしたり、生成AIとのラリーを繰り返したりする中で実装の精度が落ちていると感じることが何度かありました。これらを改善するためにもSpec Driven Developmentを活用してみても良いのではと考えています。現在はspec-workflow-mcpを利用した実装を検証しており、次回はこれを活用したいです。
まとめ
Claude Codeを活用することで、以前から着手を見送っていたTerraform Stateの分割作業を完遂できました。本取り組みから得られた知見を整理します。
成果
- State数: 1→20(環境あたり)、Stateあたりのリソース数: 700→最大100程度
terraform plan実行時間: 約90秒 → 約30秒-targetオプションに頼らない並行開発が可能に- 工数を33%削減
Claude Code活用のポイント
- 設計書を事前に作成し、ディレクトリ構成・命名規則・実装方針を明確化
- Plan Modeで実装計画を確認してからAuto Applyで実装
- Opus Plan Modeによるコスト最適化とRate Limit対策
- git-worktreeを利用した複数セッションの並行実行
今後に向けて
- 移行前のリファクタリング(deprecatedリソースの整理)
- tflint等によるLinter整備
- Spec Driven Developmentの活用検討
生成AIを活用した大規模なリファクタリングでは、事前の設計・ルール整備が成功の鍵となります。本記事が同様の課題を抱える方の参考になれば幸いです。
we are hiring
Sansan技術本部では中途の方向けにカジュアル面談を実施しています。Sansan技術本部での働き方、仕事の魅力について、現役エンジニアの視点からお話しします。「実際に働く人の話を直接聞きたい」「どんな人が働いているのかを事前に知っておきたい」とお考えの方は、ぜひエントリーをご検討ください。