0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Javaを書けない僕がAIエージェント(IBM Bob)で障害検証用Javaアプリを作ってみた

0
Posted at

はじめに

社内で Instana の検証環境を整備する機会があって、「意図的にインシデントを起こせるアプリ」が必要になった。JVM メモリリークや DB 接続プール枯渇を API 一発でトリガーできるやつだ。

こういう用途のアプリはさすがに世の中に転がっていない。一から作るしかないのだが、正直書きたくないな……という気持ちがあった。実装よりも検証したいのに、そのための環境を用意する作業でエネルギーを使い果たすのが嫌だったのだ。

そこで「IBM Bob に丸投げしてみよう」と思い立った。

Bob は VS Code ライクな UI の中で動く AI エージェントで、ワークスペース内のファイルをコンテキストに読み込みながらコード生成・修正・コマンド実行まで一気にやってくれる。IBM が提供していることもあって、エンタープライズ用途でどこまで実用になるのかが気になっていた。

Cursor や GitHub Copilot 系のエージェントと比べてどうか、という観点でも見てみたかったのだが、結論から言うと今回の検証だけでは他ツールとの明確な比較はできなかった。同じタスクを別のツールで試していないし、Bob 固有の強みと言えるかどうかを切り分けるには情報が足りない。この記事はあくまで「Bob を単体で使ってみた体験談」として読んでほしい。

補足

ちなみに、この記事はこの補足を除いて、IBM Bobのアウトプットに基づいて全てIBM Bobに書かせた。
おかしな部分は都度都度コメントして直させたが、僕はこの記事に関してはこの補足を除いて一切書いていない。

なお、タイトルにJavaを書けないとは書いたが、コードは読める。
ただし、普段Java開発をしているわけではないため、Spring Bootアプリを一から実装しろと言われても難しい。
僕の本業は、インフラ・運用なんで。

ちなみにこの記事を書かせるのにかかったコストは約4ボブコインだった。(300円くらい)


実施内容

対象プロジェクトの概要

Instana の動作検証用アプリ。構成はシンプルな 3 層アーキテクチャで、Nginx → Tomcat(Spring Boot WAR)→ MySQL という構成。

主な機能要件(最終的に出来上がったもの):

  • 各レイヤー(Nginx / Tomcat+JVM / アプリ / DB)でインシデントを発生させる REST API
  • 発生させたインシデントを復旧させる API
  • Zabbix で収集するためのメトリクスエンドポイント
  • Basic 認証によるアクセス制御

インシデントの種類は合計 16 パターン。JVM のメモリリーク・CPU 高負荷・スレッドプール枯渇、DB のスロークエリ・接続プール枯渇・ディスク容量逼迫など、監視ツールが検知すべきシナリオを一通りカバーする仕様になっている。

Bob に与えた指示

ここが今回一番「なるほど」と思ったポイントなのだが、最初から整理された仕様書があったわけではない。

最初にやったのは、頭の中にあったぼんやりした要望を Bob にそのままぶつけることだった。

「Instana の検証に使いたい。JVM のメモリリークとか DB の接続枯渇とか、インシデントを意図的に起こせて、API で操作できるアプリが欲しい。Tomcat にデプロイする WAR で。Java で」

こんな程度の雑な投げかけだ。ここから Bob との会話が始まって、「どのレイヤーのインシデントが必要か」「復旧操作は必要か」「OS は何か」と聞き返されながら要件が固まっていった。

その過程で Bob が自律的に docs/ 配下にドキュメントを整理し始めた。

docs/
  requirements.md        # 要件定義(インシデントパターン・API 仕様等)
  architecture.md        # システム設計(コンポーネント設計・DB スキーマ等)
  incident-patterns.md   # 各インシデントの実装方針・パラメータ仕様
  application-design.md  # アプリケーション詳細設計
  implementation-plan.md # 実装計画書

自分が「つくって」と言ったわけでもなく、会話の流れの中でいつの間にか生成されていた。これは正直驚いた。会話しながら設計書が育っていく感覚で、後から「あのとき何を決めたっけ」という状況が起きにくかった。

コードはそのドキュメントをコンテキストに読みながら生成された、という流れだ。

最終的な主な指示はこんな感じ:

  1. docs/ の内容をベースに、Spring Boot + Maven 構成で WAR を作って
  2. インシデントごとにサービスクラスを分けて
  3. ビルド・デプロイ・DB セットアップの手順書を docs/ に分けて作って(README に詰め込まない)
  4. OS は Amazon Linux 2023 固定で
  5. メトリクスは Zabbix で収集するので、Zabbix エージェントが拾えるエンドポイントを用意して

期待していた成果

  • pom.xml と Spring Boot のボイラープレートをゼロから書かずに済む
  • インシデント発生ロジックの骨格(特にメモリリーク・スレッド操作まわり)を生成してもらう
  • 今回は検証環境用の使い捨てアプリなので、動きさえすればいい。完成度よりスピード重視

実際の結果

どのようなアプリが生成されたか

ディレクトリ構成を含めて、一通りのファイルが揃った。

src/main/java/com/monitoring/incident/
  config/
    AsyncConfig.java
    SecurityConfig.java
  controller/
    HealthController.java
    IncidentController.java
  dto/
    IncidentRequest.java
    IncidentResponse.java
    RecoverResponse.java
  model/
    Incident.java
    IncidentLayer.java
    IncidentStatus.java
    IncidentType.java
  repository/
    IncidentRepository.java
  service/
    IncidentService.java
    AppIncidentService.java
    DbIncidentService.java
    NginxIncidentService.java
    TomcatIncidentService.java
  util/
    ProcessUtil.java
  IncidentMonitorApplication.java
src/main/resources/
  application.yml
  schema.sql

レイヤーごとにサービスクラスを分ける指示を出したところ、TomcatIncidentService / DbIncidentService / NginxIncidentService / AppIncidentService にきれいに分割された。インシデントの発生と復旧を trigger() / recover() で対称に実装する構造も、先に作ったドキュメントの設計意図をちゃんと汲んでいた。

JVM のメモリリーク実装はこんな感じで出てきた。

private void triggerMemoryLeak(String incidentId, Map<String, Object> parameters) {
    int sizeInMB = getIntParameter(parameters, "sizeInMB", 10);
    int intervalSeconds = getIntParameter(parameters, "intervalSeconds", 1);
    int maxSizeInMB = getIntParameter(parameters, "maxSizeInMB", 1024);

    List<byte[]> leakedMemory = new ArrayList<>();
    memoryLeakMap.put(incidentId, leakedMemory);

    ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
    memoryLeakExecutors.put(incidentId, executor);

    executor.scheduleAtFixedRate(() -> {
        int currentSize = leakedMemory.size() * sizeInMB;
        if (currentSize < maxSizeInMB) {
            byte[] memory = new byte[sizeInMB * 1024 * 1024];
            leakedMemory.add(memory);
        }
    }, 0, intervalSeconds, TimeUnit.SECONDS);
}

ConcurrentHashMap でインシデント ID をキーに状態を保持し、復旧時に shutdownNow() + clear() + System.gc() で解放する流れになっている。復旧処理は指示していなかった部分だが、要件定義書の「復旧可能なインシデント」という記述を読んで自分で補完していた。

スレッドプール枯渇は CompletableFuture で大量スレッドをスリープ状態にするアプローチが生成された。手法として一見それっぽいが、後述するとおり意図とのずれがある。

Zabbix 連携まわりの挙動

「Zabbix でメトリクスを拾えるようにして」という指示に対して、Bob は Spring Boot Actuator + Micrometer の Prometheus エンドポイント(/actuator/prometheus)を生成してきた。

Prometheus 形式のエンドポイントを Zabbix の HTTP エージェントアイテムで拾う構成は一つの正解ではある。ただ「Zabbix エージェントが拾える」という指示からすると、Zabbix エージェントによる JMX 監視など、より直接的な連携方式も候補に挙がってよかった。

この点は「Zabbix で収集」という指示が曖昧すぎたのが原因なので Bob を責めるのは筋違いだ。ただ、エージェント監視のユースケースに対して Prometheus エクスポーター生成という「よくある構成」に引っ張られる傾向は感じた。今回は検証環境なので生成されたまま使うことにした。

どこまで自動でできたか

自動でできたこと:

  • 会話ベースでの要件整理と docs/ への文書化
  • pom.xml(依存関係・WAR パッケージング設定)
  • Spring Boot の設定ファイル(application.yml
  • DB スキーマ(schema.sql
  • 各サービスクラスの骨格と主要なインシデント実装
  • ビルド・DB・デプロイの手順書(Amazon Linux 2023 固定)
  • systemd サービスファイルの記述

自分では手を入れなかったこと(今回は):

今回は「動けばいい」という割り切りで、気になる点はあっても基本的にコードには手を入れていない。生成されたものをそのまま使っている。問題があると感じた箇所については、次のセクションで「本来どうあるべきか」という観点で整理する。

想定との違い

「動けばいいのでたたき台として」という期待に対しては十分だった。想定外だったのは、冒頭の「要件をドキュメントに落とす」フェーズを Bob が自律的にやってくれたことだ。自分は雑に喋っていただけなのに、気づいたら設計書が出来上がっていた。

ただ、コードをよく読むと「動くが品質的に気になる」箇所がいくつかある。


良かった点

雑な要望から設計書を整理してくれた
「こういうものが欲しい」という荒削りな言葉をぶつけているうちに、Bob が自律的に要件定義・設計・インシデントパターン一覧をドキュメントとして整理してくれた。自分が作ろうとすると「後でまとめよう」で永遠に放置するやつだ。いつの間にかできていたのは地味に助かった。

ドキュメントへの反映精度が高い
会話の中で決めた「復旧可能・不可能の区別」「インシデントごとのパラメータ仕様」が、生成されたコードに正確に反映されていた。仕様書と実装の乖離が少なかった。

ボイラープレートをほぼ書かずに済んだ
Spring Boot の設定まわり、Lombok アノテーション、@RestController の構成、JPA リポジトリの定義など、繰り返しが多い部分を全部やってくれた。体感 1〜2 時間は節約できた。

レイヤー分割の構造が自然だった
「レイヤーごとにサービスを分けて」と言っただけで、IncidentService(オーケストレータ)→ 各 *IncidentService(実処理)という依存構造が自然に出てきた。自分が設計するとしても似た構造にしていたと思う。

OS 固定の手順書が正確だった
「Amazon Linux 2023 固定で手順書を分けて」という指示に対して、dnf コマンドでのリポジトリ追加から systemd の設定まで AL2023 向けに正確に書いてくれた。Ubuntu 向けの手順が混入することもなかった。こういう「OS ごとの差分」に正確なのは、ドキュメント系タスクの信頼度を上げている。


気になった点——動いてはいるが、本来こうあるべき

今回は検証用アプリということで、コードには手を入れていない。ただ「動く」と「良いコード」は別の話なので、読んでいて気になった箇所を整理しておく。本番投入を考える場合には手を入れるべき部分だ。

CompletableFuture.runAsync() が ForkJoinPool を使う
スレッドプール枯渇の実装で CompletableFuture.runAsync() を引数なしで呼んでいる。これはデフォルトで ForkJoinPool.commonPool() を使うため、Tomcat のスレッドプールではなく JVM 全体の共通プールを枯渇させることになる。要件定義書に「Tomcat のスレッドプール枯渇」と明記されていたが、実装レベルではその文脈を追いきれていなかった。

本来は専用の ExecutorService を渡すべきで、こうなる:

// 本来こうあるべき
ExecutorService dedicated = Executors.newCachedThreadPool();
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { ... }, dedicated);

今回の用途では「何かのプールが埋まる」という状況は再現できるので許容しているが、「Tomcat のスレッドプール」を正確に再現したい場合はここを直す必要がある。

Nginx インシデントのコマンド実行が素朴
ProcessUtil.executeCommand(command) に文字列をそのまま渡す実装になっている。インシデントパラメータがコマンドに連結されるケースではコマンドインジェクションのリスクがある。検証環境専用なので今回は気にしないが、本来は ProcessBuilder でコマンドと引数を分離するのが正しい:

// 本来こうあるべき
new ProcessBuilder("sudo", "systemctl", "stop", "nginx")
    .redirectErrorStream(true)
    .start();

System.gc() を無条件に呼んでいる
メモリリーク復旧時に System.gc() が入っている。「なるべく早くメモリを返したい」という意図はわかるが、GC タイミングは JVM 任せにするのが原則で、明示的な呼び出しはアンチパターンとされている。検証環境なので実害はないが、本番コードに持ち込むと問題になりうる。

/history/{incidentId} のルーティング
GET /api/incident/historyGET /api/incident/{incidentId} が同一コントローラーに並んでいる。Spring MVC は静的パスを優先するので通常は問題ないが、history という文字列を ID として渡したときに意図しないルーティングになりうる。エンドポイントの命名規則を整理するか、ID のバリデーションを入れておくほうが安全だ。

スロークエリが SLEEP() 関数頼み
DB のスロークエリインシデントが SELECT SLEEP(N) を実行する実装だった。実際には「クエリが遅い」というより「クエリが止まっている」状態に近い。本物のスロークエリ(フルスキャン・集計処理)とは Zabbix の見え方が変わる場合がある。今回の目的(アラートが上がるかどうかの確認)には十分だが、クエリ性能の検証が目的なら作り直しが必要だ。

Zabbix に対して Prometheus エクスポーターを出してきた
前述のとおり、「Zabbix で収集」という指示に対して Prometheus エンドポイントを生成してきた。今回は Zabbix の HTTP エージェントアイテムで拾う構成で割り切ったが、本来は最初に「どの方式で収集するか」を明確に指示しておくべきだった。指示が曖昧なときに「よくある構成」に流れるのは Bob に限らない話だが、監視ツール固有の文脈は特に要注意だ。


生成 AI を開発に使う上で感じたこと

今回やってみて改めて実感したのは、「AI は壁打ち相手として最高だが、実行時の挙動を推論するのは苦手」という点だ。

最初の「こういうものが欲しい」という雑な要望を受け止めて、会話しながら要件を整理してドキュメントに落としてくれる体験は想定以上だった。自分一人だと「そのうちちゃんと書こう」で放置してしまうドキュメントが、会話の副産物として出来上がるのは実用的な価値がある。

一方で、CompletableFuture.runAsync() の問題は典型的な「見た目はそれっぽいが動かすとずれている」ケースだ。「スレッドプール枯渇を起こすコードを書く」という指示に対して、コンパイルが通るコードを出すことはできる。でも「それが Tomcat のスレッドプールに実際に影響するかどうか」という実行時の挙動レベルの判断はできていなかった。

今回は「動けばいい」という前提で受け入れたが、これが本番コードであればそうはいかない。生成されたコードを鵜呑みにせず人間が読むフェーズを必ず設ける、という運用上の前提は変わらない。

他の AI エージェントと比べてどうか、という点については正直わからなかった。今回は Bob しか使っていないし、「会話しながら設計書が出来上がる」体験が Bob 固有のものなのか、最近のエージェント系ツールなら大体そうなのか、判断する材料がない。比較はまた別の機会にやりたい。


まとめ

観点 評価
コード生成 ★★★★☆ — ボイラープレート・骨格生成は十分実用的。実行時挙動の細部に要注意
既存資産理解 ★★★★★ — 会話で生まれたドキュメントをコードに反映する精度は高かった
開発効率化 ★★★★☆ — 要件整理フェーズから入れると効果が大きい。生成物の読み込みコストは別途必要
実運用への適用可能性 ★★★☆☆ — 検証・試作レベルなら十分。本番投入には人間のレビュー工程が必須

Bob が有効だと感じたケース:

  • 頭の中にある要件が整理しきれていないとき——壁打ちしながら文書化してくれる
  • Spring Boot のような「お作法の多いフレームワーク」の初期実装
  • OS やミドルウェアのセットアップ手順書など、正確なコマンド列が必要なドキュメント生成
  • 「動けばいい」割り切りができる、検証用・試作用のアプリ開発

人間が主導した方が良いと感じたケース:

  • 並行処理・非同期処理まわりの設計(スレッドモデルの理解が必要)
  • 監視ツール固有の連携設計(Zabbix エージェントの取得方式など、ツール特有の文脈)
  • セキュリティが絡む実装(コマンド実行・認証・入力バリデーション)
  • 「動く」と「正しい」を区別して判断する必要があるコードレビュー全般

「AI に書かせる」ではなく「AI と一緒に考えながら書く」、そして「生成されたものを読む時間をケチらない」。今のところそれが一番しっくりきている。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?