約1,600リソースのTerraformモノリスを、22個のstateに分割しました。terraform state mv ではなくゼロからコードを書き直し、import blockで既存リソースを引き取る「フルリライト + import」方式です。AIコーディングエージェント(Claude Code)の活用で、当初見積もり8週間の作業が1週間で完了しました。

「ビズリーチ・キャンパス」のインフラ開発を担当しているyoshidaです。本記事ではその移行戦略を共有します。肥大化したIaCのリソースの構成を見直したい方、AIをインフラ領域で活用する事例を探している方の参考になれば幸いです。

移行前の構成と課題

移行前は1つのstateに全リソースが入っており、環境切り替えは terraform workspace で検証用 (staging) と本番用 (prd) を管理していました。main.tf には54個のmoduleブロックが並び、互いのoutputsを直接参照することで密結合した状態になっていました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 旧 aws/main.tf — 54 個の module が密結合
module "alb" {
  source                = "./alb"
  vpc_id                = module.shared_base.vpc_id
  certificate_arn       = module.shared_app.certificate_arn
  logs_s3_bucket        = module.logs_platform.s3_bucket_name
  # ...
}

module "service_a" {
  source           = "./services/service_a"
  vpc_id           = module.shared_base.vpc_id
  alb_listener_arn = module.alb.listener_arn
  db_endpoint      = module.database.endpoint
  # ...
}
# ... あと 52 個続く

この構成は以下の課題を抱えていました。

なぜ state mv ではなくフルリライトか

state mv 自体に問題があるわけではありません。今回フルリライトを選んだのは、主にチーム事情によるところが大きいです。

  1. 各チームの開発を止めたくなかった — 各チームがインフラまでを含めた機能を自律的に開発するようになり、加えてAIの活用も進んだことで、Terraformを触る機会が増えていました。移行のために開発を長期間止めるリスクをなるべく小さくしたかったのです。
  2. 段階的に進めにくいstate mv は移動した瞬間から旧stateの plan に差分が出るため、中間状態が不安定で長期の段階移行には向きません。
  3. やりながら理想をブラッシュアップしたい — 例えばmonitoring層は、走り出した後で「独立させた方が良い」と気づいて切り出しました。フルリライトなら新stateを捨ててやり直せるので、構成を柔軟に変えられます。

そこで採用したのは、次の4ステップを各スタックで繰り返すアプローチです。

  1. 新しいディレクトリに理想の構成でゼロからコードを書く
  2. import blockで既存のAWSリソースを新しいstateに取り込む
  3. terraform planimportのみ・add/change/destroyゼロ(以降「変更差分ゼロ」と呼びます)であることを確認して apply
  4. 旧stateは一切触らず並行運用し、全て完了後にまとめて廃止

最大のメリットは旧stateに一切触れないことです。移行作業中に本番環境が壊れるリスクがありません。

補足すると、このアプローチが成立するのはTerraformに特有の事情があるからです。一般的な機能開発では「正しい仕様 = コード」で、書き直したコードが正しいかを確認する手段は限られます。しかしIaCでは「正しい状態 = 既に動いているAWSリソース」で、新コードが現状と一致するかは plan で機械的に照合できます。機能開発で言うと本番挙動を完全に再現するE2Eテストが全て揃っている状態に近く、これがフルリライトを現実的な選択肢にしている理由です。

移行後の構成

レイヤー構成

新構成は4層のレイヤーに分かれ、各レイヤーは独立したstateを持ち、terraform_remote_state で下位レイヤーのoutputsを参照します。

1
2
3
4
5
6
7
foundation          ← 依存なし(VPC, Subnet, NAT 等)
core                ← foundation に依存
services/*          ← foundation や core に依存(計 19 services)
monitoring          ← core, services/* に依存

依存はすべて一方向で、循環依存はありません。

環境別ディレクトリ方式

terraform.workspace をやめ、stagingprd を物理的に別ディレクトリに分けました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
aws-main/
├── modules/                              # 再利用モジュール
├── foundation/
   ├── main.tf, variables.tf, outputs.tf
   └── environments/{staging,prd}/       # staging = 検証, prd = 本番
├── core/
   ├── database.tf, logging.tf, ...
   └── environments/{staging,prd}/
├── services/
   ├── service_a/
      ├── main.tf, data.tf, variables.tf, outputs.tf
      └── environments/{staging,prd}/
   ├── service_b/
   └── ...                               # 計 19 services
└── monitoring/
    ├── main.tf, data.tf
    └── environments/{staging,prd}/

これにより、terraform workspace select prd のような切り替え操作が不要になります。実行前に今いるディレクトリを確認する必要は残りますが、環境の誤操作リスクは大きく下がります。

層間のデータ連携

各サービスは terraform_remote_state で上位レイヤーのoutputsを参照します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# aws-main/services/service_a/data.tf
data "terraform_remote_state" "foundation" {
  backend = "s3"
  config = {
    bucket  = "${var.env}-aws-main-terraform-state"
    key     = "foundation/terraform.tfstate"
    region  = "ap-northeast-1"
    profile = var.env
  }
}

data "terraform_remote_state" "core" {
  backend = "s3"
  config = {
    bucket  = "${var.env}-aws-main-terraform-state"
    key     = "core/terraform.tfstate"
    region  = "ap-northeast-1"
    profile = var.env
  }
}

旧コードで module.shared_base.vpc_id のように暗黙的だった依存が、terraform_remote_state を経由することで明示的になります。どのスタックが何に依存しているかが、データソースの定義一覧から一目で分かるようになりました。

import blockによる移行の流れ

各スタックで以下の4ステップを繰り返します。

Step 1: 新コードを書く(stateは空)

まず新しいディレクトリで理想の構成を書き、terraform init を実行します。この時点ではstateが空なので、plan は全リソースを「新規作成」として表示します。AWS上にリソースは既に存在していますが、新しいstateがそれを知らないためです。

Step 2: import blockを書く

Terraform 1.5で導入された import blockを使い、既存リソースとTerraformコードの対応を宣言します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# environments/staging/imports.tf
import {
  to = module.foundation.aws_vpc.main
  id = "vpc-xxxxxxxxxxxxxxxxx"
}

import {
  to = module.foundation.aws_subnet.public["a"]
  id = "subnet-xxxxxxxxxxxxxxxxx"
}

# ... 全リソース分

Step 3: planで変更差分ゼロを確認し、apply

1
2
3
4
5
6
7
8
$ terraform plan
# → Plan: 17 to import, 0 to add, 0 to change, 0 to destroy.
#   import のみ、add/change/destroy ゼロ
#   = 新コードが既存リソースを正確に表現している

$ terraform apply
# → 17 imported, 0 added, 0 changed, 0 destroyed.
#   AWS 上のリソースには変更なし

plan の変更差分ゼロ」がこのアプローチの肝です。これが達成できれば、新しいコードが既存リソースと完全に一致していることをTerraform自身が保証してくれます。

Step 4: import blockを削除して最終確認

import blockは一度applyすれば不要になります。削除してもstateにはリソースが残るので、最後に imports.tf を消し、再度 terraform planNo changes. となることを確認して完了です。

段階的な移行(フェーズ分割と並行運用)

レイヤー構成のおかげで、移行を段階的に進められます。各フェーズは独立して変更差分ゼロを検証でき、前のフェーズが完了してから次に進みます。失敗しても影響範囲はそのフェーズの新stateだけで、旧環境には一切影響しません。

Phase 対象 リソース数 備考
0 modulesコピー + CI整備 - 準備作業
1 foundation 約30 VPC, Subnet等
2 foundationのみに依存するservices(7個) 約140 coreを待たずに進められる
3 core 約840 最大の作業
4 coreにも依存するservices(12個) 約470 ECSサービス中心
5 monitoring 約160 全servicesのoutputsを参照
6 旧state削除 + クリーンアップ - 36,000行削除

移行期間中、チームメンバーは旧構成で plan / apply を自由に実行できます。新stateはAWS上の同じリソースを参照しているだけで、旧stateにも引き続き同じリソースが記録されているためです。移行作業がチームの日常業務をブロックしません。

最終切替時には、移行中に旧側で行われた変更を新コードに反映し、再度変更差分ゼロを確認します(追いつきステップ)。

AIコーディングエージェントの活用

このアプローチの最大のトレードオフは、コード量が多いことです。今回は約12,000行のTerraformコードを新規に書きました。これを現実的なコストに収めるため、Claude Codeを活用して人間とAIで役割を分担しました。

役割 担当 具体的な作業
コード生成 AI 旧コードを参考に新構成の .tf ファイルを生成
terraform.workspacevar.env 変換 AI 機械的な置換
ディレクトリ構成・定型ファイル生成 AI environments/backend.tf, provider.tf
AWSリソースIDの特定 + import block生成 AI AWS CLIや terraform state list で実IDを確認しimport blockを生成
plan 差分の解消 AI 差分が出た場合のコード修正
apply の実行判断 人間 import実行の最終判断、import時changeの確認

作業量の約9割はAIが担当し、人間はapplyの最終判断と、後述するimport時changeの確認に集中する形になりました。state mv での段階移行は、フェーズごとに他チームのマージ(apply)待ちが発生し、その積み上げで見積もりは8週間でした。一方、フルリライト + importは旧stateに触れず待ち時間が発生しないため、AI併用で検証込み1週間で完了しています。

短いプロンプトを連発するのではなく、移行手順そのものをClaude Codeとあらかじめ計画ドキュメントとして詰めておき、「このドキュメントに従って実装してください」と作業させるスタイルが安定しました。計画ドキュメントを蓄積していくことで、セッションをまたいでも文脈を引き継げます。

plan 変更差分ゼロの難しさ

このアプローチで最も注意が必要なのは、plan の変更差分をゼロにする作業です。新コードが既存リソースの設定と1つでも食い違うと差分が出ます。

よく遭遇したケースは2つあります。

outputsの定義漏れは、AIに plan 実行と結果からの修正をループさせることで解消できました。人間が手動で対応したのはimport時のchangeが既存リソースに影響しないかを確認する判断のみです。

まとめ

フルリライト + importによる移行は、次の状況で特に有効です。

コード量が多いというトレードオフは、AIコーディングエージェントによって現実的なコストに収まる時代になりました。これまで選択肢になりにくかった「書き直す」が、改めて有力な選択肢として浮上しています。state分割やIaCリファクタリングを検討中の方の参考になれば幸いです。

ビズリーチでは、新しい仲間を募集しています。

お客様にとって価値あるモノをつくり、働く環境の変革に挑戦する仲間を募集しています。
募集中のポジションやプロダクト組織の詳細は、ぜひキャリア採用サイトをご覧ください。

ビズリーチ採用サイト
吉田 芳弘
吉田 芳弘

ビズリーチ・キャンパスのインフラ開発を主に担当しています。IaC や AI を活用した開発基盤づくりに取り組んでいます。