この記事はSchoo Advent Calendar2025の6日目の記事です。
技術戦略部門の @takayoshi_katsumata です。クラウドインフラをいじっています。
Terraformは、Schooに参画した2025年1月から触り始めたので、学習の毎日です。
今回の記事では、CursorのAgent機能を用いて、あるAWSアカウントの既存リソースをTerraformへImportした取り組みについて紹介します。
どんな記事か
本番ワークロードの一部を扱いながらも唯一IaC管理外、かつ設計書やパラメータシートのような管理もされていない、完全手動管理のAWSアカウントが存在していました。それは2013年ごろから2025年まで歴代の職人の手により手動運用されてきた、いわば「秘伝のタレ」と化した、伝統と歴史の重さが凄まじいアカウントです。そのため数多の片付け忘れと思われるAWSリソースが放置され、AWS Resource Explorer上では雑にフィルタリングしても1,500以上のリソースがリストアップされる状況でした。
今回、AI Agentを利用して必要なAWSリソースの特定(350個程度)とimportができました。
この記事では、どのようにimport作業を進めていったのか、そしてCursorのAI Agentを使った作業で大切だと感じた点について紹介します。
「秘伝のタレ」が生まれた経緯
「ドキュメントがしっかりしていれば」「タグ運用がしっかりしていれば」「腰を据えて完全移行すれば」というな議論はあるかもしれません。私も2025年参画なので思います。
しかし2013年から現在に至るまでのIaC技術の普及と過渡期を振り返ると、これは歴史の長いプロダクトを持つ少人数の開発組織であれば、容易に陥りうる構造的な課題だと思います。
問題のアカウントが今の状態に至った背景には、以下のような経緯があります。
経緯
- IaC黎明期 (2013年〜): 当時、IaCはまだ一般的ではなく、マネジメントコンソールでの手動構築が一般的でした。この時期にAWSの利用を開始しています。
- Terraformへの部分的移行と安定化 (2016年〜): IaCが普及し始め、SchooでもTerraformの導入が進みました。主要なリソースを別アカウントへ移設してIaC化したことで、全体の運用負荷は大きく下がりました。 これにより、移行が容易でない手動管理リソースが残っていても運用上の致命的な問題は発生していませんでした。BCP(Business Continuity Plan)の観点と本番適用時がリスキーである点が課題として残りました。
- 事業貢献ゆえの優先順位: この環境は長年にわたり、会社の売上を支え続けていました。「運用が回っていて、しっかりお金を稼いでいる」 からこそ、事業成長のための新規開発などが優先され、コストをかけて既存基盤を刷新する優先度はどうしても下がりがちでした。
- コンテキストの喪失: 手動変更を行うリソースは特定の箇所に偏っていたため、「変更頻度の低い細々としたリソース」については、調査しないと詳細がわからない状況が、長い時間をかけて形成されていきました。
歴代の職人たちもただ座視していたわけではなく、当時の状況で実現可能なベストな対策を適宜行っていました。 また過去の至らなかった点も反省・学び、未来で同じ轍は踏まぬようにしています。
通常のimport手順とその課題
importコマンド vs importブロック
Terraformにはimportコマンドが存在し、以下のようなコマンドを実行することで手動作成リソースをTerraform管理下にできます。
terraform import aws_instance.web i-1234567890abcdef
このコマンドを実行すると、即座にtfstateが更新され、i-1234567890abcdefはaws_instance.webとしてTerraform管理下に入ります。しかし、importコマンドは「実行するだけでtfstateが変更される」ため、以下のようなチーム開発に向かない課題がありました。
- 次のapplyまでにimport対象のresource記述を用意しないと、削除や変更として扱われてしまう恐れがある
- 実行履歴を残しづらく、CI/CDワークフローへ統合しにくい
Terraform 1.5から導入されたimportブロックを使うと、applyするまではtfstateの変更が行われず、安全に作業ができるようになりました。私たちはTerraform 1.10.1 を利用しており、moved/removedといった操作についてもブロック記述を用いていたため、import作業も原則importブロックを使いたいと考えていました。
importブロックによるimport手順
一般的には以下のようになります。
- importしたい対象のidを調べる
- importブロックを記述する
- importブロックのtoで記載したresource実体を記述または生成する
- planを実行し、差分や競合を解消するようにresource記述を修正
小規模であれば問題ありませんが、リソース規模が大きくなると手動対応は非現実的です。大規模な場合は terraformerなどのツールや、AWS CLIと連携したスクリプトを作成して省力化を図るのが一般的ですが、今回のケースでは以下の課題がありました。
importの課題
- 対象の絞り込みの問題: 既存ツールはサービス単位の一括importが主であり、適切なタグ付け等がされていないと対象の限定や除外が難しい
- ハードコーディング: 自動生成されるコードは参照部分(IDなど)が基本的にハードコーディングされてしまう
- 修正コスト: 結局、plan差分の解消作業は残る
- ツールの対応状況: terraformer はimportブロック未対応、balcony は対応しているもののデフォルトでサポートするAWSリソースが少ない
import作業をどうするか検討
既存ツールでの「importブロック記述によるimport」の自動化は難しく、かといってスクリプトを自作するにも、AWS CLI等を駆使して複雑なフィルタリングロジックを組む必要がありました。
そこで、まずはimport対象環境と既存コードの状況を整理しました。
対象環境の状況
- 対象: import対象のAWSサービスは大体把握できている
- ノイズ: 不要なリソース(ゴミ)が多数ある
- タグ: 整備されておらず、フィルタリングの手掛かりにならない
- 命名規則: 統一されていない(時代によって変遷している)
- 依存関係: VPC Peeringで、他のIaC管理下のAWSアカウントと共有されている
- 規模: 必要なリソースは少なくとも100以上あり、正確な全容はチームの誰も把握していない
特に「必要なリソースの特定」が難題でした。利用状況を詳細に調査する必要がある上、他アカウントから依存されているリソースは、アカウント内部では孤立して見える場合もあります。単純作業で進めると、plan差分の解消に膨大な時間がかかることが予想されました。
既存コードの状況
-
構成: Terraform + Terragruntによる管理
- 主に依存関係とtfvarsファイルの切り替えに terragrunt.hcl を利用
- 参照: IaC管理下のAWSアカウントが問題のアカウントのリソースを参照するときは、ARNなどをハードコーディングしている
- モジュール: 自己管理モジュールはなく、基本的にリソース記述のコピー&ペースト
Terragruntによる複雑さはあるものの、import作業に対する本質的なブロッカーではないと判断しました。
作業方法の検討
私たちの状況における要件は以下の通りです。
- 大量のplan差分や競合解消のコストを下げたい
- 不要なリソースをimport対象から除外したい(徒労とコード肥大化の防止)
- コードスタイルを既存のTerragrunt管理のスタイルに合わせたい
- 明らかな依存関係はハードコーディングせず、適切に参照させたい
当初はこれらを解消するツールをAI支援のもと作成しようと考えましたが、「AI Agentに任せれば、これら全てを実現できるのではないか?」と思い至りました。 当時はコンテキストエンジニアリングやKiroのリリース、AGENTS.mdの策定など、AI Agentムーブメントの只中で、個人でAI Agentを作ったりClaudeCodeでAgenticコーディングをしてみたりしていたものの、業務でのAgent活用は未実施という状況でした。
個人開発とは異なり、大規模かつ複雑な状況でCursorのAI Agentが通用するかは未知数でしたが、試す価値はあると判断し、初めて業務レベルでのAI Agent適用に挑戦しました。
AI Agentによるimportの自動化
- 当時はPlanモードがなかったため、ルールファイルにて擬似的なPlanモード(Markdownファイルとして計画を生成させる)を実現
用意したコンテキスト
- 既存のコードベース(実装サンプル・依存情報として)
- ルールファイル
- AWS API MCP
- テスト代わりのコマンドalias
- コマンドのAllowリスト
1. 既存のコードベース
他のアカウントは既にIaC管理下にあるため、実装スタイルのサンプルとして参照させました。 また、既存コード内で「問題のアカウントのリソースをARNベタ書きで参照している箇所」を、importの必要性判断の材料としても利用させました。
2. ルールファイル
ルールファイルは 禁止ではなく推奨 を意識しました。そのほか、状況に応じて変化するルールは含めませんでした。
状況に応じて変化するルールとしては「import対象とすべきリソースの条件」などがありますが、こうしたものをルールファイルに書く場合、網羅的に記載する必要があるのと、タスク内容と無関係な別タスクのルールが常にコンテキスト消費し、かつ注意機構(Attention)にも影響を与えるだろうと考えたためです。
また、作成したルールファイルの妥当性を評価するために、作成したルールファイルをコンテキストとして与えた上で「このルールファイルにしたがって〇〇をimportしてくださいという指示を受けたらどのように行動しますか?」といったLLMへのインタビューを実施して内容をブラッシュアップしていきました。
ルールファイルに書いた情報の例
- 背景情報
- 未使用のAWSリソースが多数ある
- 一部のリソースは他のアカウントからARN指定で参照されている
- タグは整備されていないため手掛かりとして利用できない
- 使用すべきコマンド/MCP
- AWS API MCP
- aliasで用意したplan用コマンド
- 依存関係の管理
- VPC, 共有RDSなどの多数から参照されるリソースは専用ディレクトリで管理されていて、参照側はterragrunt.hclで明示的依存関係を指定する
- import対象のAWSリソースと強い依存関係があるセキュリティグループやIAM Roleなどはimport対象のAWSリソースと同一ディレクトリに配置し暗黙的依存関係で管理する
- 大まかな作業手順
- AWS API MCPを利用してimport対象となるリソースを調査
- import対象のリストおよび実行計画をMarkdownファイルとして生成し、ユーザーのレビューと承認を待つ
- 既存実装を参考にimportブロックとresourceブロックの記述を行う
- aliasで用意したplan用コマンドの結果が
will be importedになることをゴールとする
- plan結果の判断基準
- AWS側の状態が正でありterraform記述はそれに合わせる
-
will be updatedの時はAWSアカウント上の実態に合わせてtfファイルでの設定値指定などを修正する -
will be createdとなっているリソースは、AWSアカウントに存在しないリソースが作られるかimport不足を示すため、tfファイルを修正する -
+がついている項目はAWSアカウントに存在するリソースが追加・変更されることになるため、tfファイルを修正する -
-がついている項目はAWSアカウントに存在するリソースが削除されることになるため、tfファイルを修正する
3. AWS APIを叩くMCP
最初はsteampipeのMCPを利用することを検討していましたが、AWS公式からAWS API MCPがリリースされたためこちらを採用しました。READ_OPERATIONS_ONLY = trueに設定し、読み取り専用操作に制限できる点が決め手でした。念の為MCPが利用するクレデンシャルもReadOnly権限にしました。
4. テスト代わりのコマンドalias
当時のCursorの特性かLLMのモデルの問題かは不明ですが、planの結果がwill be importedに到達すると、Agentが気を利かせて「applyしましょう!」とapplyを試みるケースがありました。ReadOnly権限なので事故にはなりませんが、エラー修正にAgentが時間を費やすのは無駄です。そこで、aliasにplanを実行する専用コマンドを定義し、それを使わせるようにしました。
5. コマンドのAllowリスト
aliasコマンドやRead系のコマンドを許可リストに入れました。Agentが推奨コマンド以外(生のterraformコマンドなど)を実行しようとすると、人間の許可が必要になります。これにより放置しても安全になるのもありますが、コンテキスト喪失やタスク肥大化の検知サインとしても利用しました。
AI Agentでimportさせてみる
import作業の流れ
以下のフローで進めました。調査開始時とレビュー時に人間がプロンプトで指示を出します。
![]() |
| Import作業の流れ |
- CursorのAgentモードを利用(タスクに適切なモデルがわからないのでAuto)
- 状況に応じて変化するルールについては、入力するプロンプトで都度指示
- 不要なAWSリソースの判定基準(「メトリクスがない」「トリガーがない」)等
実際にAI Agentに作業させてみると、ほぼ放置で成果が上がってくるだけでなく、次のようなメリットを感じました。
柔軟性が高い
自然言語でフィルタリング条件を指定できるのが強みです。また「AIが学習している一般的な判断基準で不要なものをリストアップし、理由と共に提示させる」といった対話的な絞り込みができるため、リソース名やタグに頼れない状況での特定が非常に楽でした。
最初から事情を汲んでくれる
環境ごとのパラメータ差異(tfvarsやlocalsの使い分け)など、私たちのチーム独自の構成ルールや「お作法」を、既存コードやプロンプトから読み取ってくれます。最初からルールに則ったコードが出力されるため、手直しが最小限で済みました。
依存関係のハードコーディングが少ない
コンテキストの与え方にもよりますが、IDのベタ書きではなく、適切なリソース参照を用いたコードを書いてくれます。仮にハードコーディングされても、後から「依存関係を修正して」と指示するだけでリファクタリング可能です。
AI Agentを使ってみて重要だと思ったこと
AI AgentはLLM(大規模言語モデル)やLMM(大規模マルチモーダルモデル)を利用していますが、本記事ではLLMを前提として記述しています。
禁止より推奨したほうが良い
ルールファイルや入力プロンプトといったユーザーの指示は、LLMにとって「回答を探す範囲の絞り込み」です。
安易な禁止表現は選択肢を広げてしまう上に、否定のパラドックスの問題も孕んでいます。そのため推奨行動で表現できないかを念頭において禁止表現をできる限り排除すると、LLMをうまくナビゲートできると感じました。ルールファイル等に記載する禁止表現は最悪無視されても問題がないような内容とし、別の手段で禁止や推奨のフィードバックを実行時に与えるのが良いと思いました。
選択肢が広がる例
禁止表現(「ゴミをポイ捨てしないで」)
ポイ捨て以外の全ての行動、ゴミを食べる、燃やす、ポケットに入れる、ゴミ箱に捨てる……これら全ての可能性の中から、学習データに基づきつつ文脈的に最も確率が高いものを選ばなければならない。学習データに偏りがあったり、文脈が特殊だったりすると、意図しない行動(例:「窓から投げる」など)を選んでしまうリスクがあります。
推奨表現(「ゴミはゴミ箱に捨てて」)
推測の余地がなく、一点に定まります。学習データによる「常識の補完」に頼る必要がなくなります。
否定のパラドックスの例
人間でも「ピンクの象を想像しないでください」と言われると逆に想像してしまう(ピンクの象問題)のと同様、LLMも禁止された概念に関連するベクトルが活性化してしまいます。 「正解」を直接指示することは、この「禁止ワードへの不要な注目」を避ける効果もあります。禁止表現(「ゴミをポイ捨てしないで」)
「ポイ捨て禁止」と言われると、LLMのAttention(注意機構)は「ポイ捨て」という単語に強く反応。禁止された概念に関連するベクトルが活性化してしまい、結果として禁止したはずの行動や単語が出力に混入してしまうリスクがある。
推奨表現(「ゴミはゴミ箱に捨てて」)
推奨された概念に関連するベクトルが活性化するので期待通りになりやすい。
beforeShellExecutionなどのhooksの利用
今回の作業途中で追加された機能だったので当時は利用しませんでしたが、Cursorはhooksという機能が使えるようになっています。これはClaudeCodeのhooksとは思想が異なり柔軟性は少なめですが、Agentループ中の具体的なアクションに対応したフックイベントを提供しています。
例えばbeforeShellExecutionを使えばAgentが実行しようとするシェルコマンドをインターセプトし、推奨コマンドへの誘導などを自動的に行えます。
今回のようにルールファイルなどで推奨行動として安全なコマンドを案内した場合、コンテキストを喪失するなどして非推奨コマンドを利用した際にユーザーの許可待ちでAgent動作が一時停止します。hooksであれば、問題行動発生時に自動で推奨行動へ軌道修正できるので、コマンド実行のガードレールとしてはルールファイルに記載するよりもhooksの方が良い結果をもたらすと思います。
できるだけAgentに判断を委ねない方が良い
LLMは、自身が学習した判断基準とユーザーの入力プロンプトからもっともらしい応答する非決定論的挙動をします。
加えてタスクが複雑だったりすると、コンテキストが大きすぎて注意散漫になるなど期待しない動作をする不安定さを持ちます。
こうした問題からLLMでなくてもできることはLLM外でやるのが良いと思いました。
例えば、Linterなどで決定論的に判断できることはLLMに任せないといったことです。
また、コーディングルールを例にすると、for_eachを使うべきときやvariablesの数など、あえて明確化せず個人の判断に委ねがちなところを、目安で良いので言語化をすすめて選択肢を狭めておくと良いと思いました。
適切なフィードバックの用意
今回のような「差分なくimport可能か」はwill be importedの文字列で判定できますが、そこに辿り着くためにAgentが自律的修正を行うには、正確な現状把握が必要です。
今回、plan結果にheadやgrepコマンドなどを組み合わせてAgentが確認を省略することがたまにあり、自己解決できない状況に陥ることがありました。そのためplan結果などを 「Agentが解釈しやすい形」でフィードバックを与えるコンテキストエンジニアリング すると良さそうだと思いました。
具体的にはTerragruntでは、jsonオプションをつけることでplan結果をjson形式で出力することができるので、これをjqでchange.actionやtypeでフィルタするといったコンテキストを付与して渡すとより自律動作が安定すると思いました。
実際のJSON形式ログの例
{
"@level": "info",
"@message": "aws_elasticache_subnet_group.default: Plan to create",
"@module": "terraform.ui",
"@timestamp": "2025-12-01T15:41:27.498182+09:00",
"change": {
"resource": {
"addr": "aws_elasticache_subnet_group.default",
"module": "",
"resource": "aws_elasticache_subnet_group.default",
"implied_provider": "aws",
"resource_type": "aws_elasticache_subnet_group",
"resource_name": "default",
"resource_key": null
},
"action": "create"
},
"type": "planned_change"
}
Terraformの場合、構築しようとしているインフラリソースの記述に対して、ソフトウェア開発で行われるような単体テストが用意されることはあまり一般的でなく、tflintなどの静的解析とplan結果の目視チェックが現実的な事前検証となっているので、import作業だけでなく普段の開発においても効果が得られると思います。
おわりに
最終的に、10年間以上手動運用されてきた「秘伝のタレ」のような環境から、必要なリソースだけをIaC管理下に移行することができました。
今回の取り組みで強く感じたのは、ルールファイル等での制御も重要ですが、「Agentが自律的に問題を解決できる仕組み(フィードバックループ)」の設計が重要だということです。 コンテキストエンジニアリングとしてルールファイルやドキュメントの整備に目が行きがちですが、Agentが実行環境に作用した結果を正しく認識できるような環境作りに注力する方が、自律動作が安定すると感じました。
AIをうまくナビゲートする工夫を凝らすことで、長年積み上がっている泥臭い課題も、効率よく解決できる手応えを感じました。この記事が、同じように歴史あるAWS環境の整備に頭を悩ませている方のヒントになれば幸いです。
Schooでは一緒に働く仲間を募集しています!
