ネットワーク機器のConfig、変更のたびに人間の目でレビューしてます…よね? 「急ぎの要件で、とりあえず必要なポリシーだけ現用系に入れた」→「待機系に入れるのを忘れてて、いざ系が切り替わった瞬間に通らない」みたいな、自分でやらかすあるあるによく直面するので(…私だけじゃないと信じたいw)、「差分を投げたらAIが設計・セキュリティ観点でレビューしてくれる仕組み」 を CI/CD で組んでみました。レビュー頼みたくても、詳しい人はいつも忙しいですしね。
AIレビュー役に IBM Bob Shell(bobshell) を使っています。Bob は 2026年3月24日に GA になった IBM のエージェント型AI開発パートナーで、その CLI 版が bobshell。これが **非対話(スクリプト)実行に正式対応してて CI と相性がめっちゃくちゃいい!**実際に自社の検証環境のコアスイッチで回してみたら、EOL版の指摘や冗長構成の非対称までちゃんと拾ってきたので、そのメモです。
※ 本記事は仕組みの作り方の話がメインです。認証情報・社内IP・機器の生Configは伏せて一般化しています。
先に「何が出てくるか」から。NW機器のConfigを push すると、CIがこんなレビュー結果を吐きます。
Bob の指摘を構造化したもの。severity / カテゴリ / 対象機器 / 指摘 / 対処 がセットで出ます。以下、この仕組みの作り方の話です。
IBM Bob / bobshell ってなに
-
IBM Bob … 計画・設計・実装・テスト・デプロイ・運用まで、工程ごとにエージェントを持つ開発パートナー。SaaSで、
https://bob.ibm.comで30日トライアルあり。 - bobshell … その CLI 版。ターミナルで対話的に使えるほか、非対話モードでスクリプト/CIから叩ける。エージェントの各アクションを記録する監査性が売りで、規制業界向けに「AIが何をやったか追える」ことを推してるのが特徴です。
今回うれしかったのは、この非対話モード。-p に指示を渡して、stdin にパイプした内容を文脈として食わせられる、という いかにもUNIXなインターフェース なので、CIに埋め込みやすいんですね。
# 公式のサンプルもこのノリ
$ cat buildError.txt | bob -p "Explain this build error"
これを「差分を stdin、レビュー指示を -p」に置き換えれば、そのままConfigレビュー器になる、という発想です。
環境(材料)
- CI/CD: GitLab CI/CD(社内GitLab)+ Kubernetes 上の Runner
- Runner イメージ:
python:3.12-slimベース(自前ビルド) - AI: IBM Bob Shell
bobv1.0.6(非対話モード) - 対象ベンダ: Cisco IOS / Yamaha RTX / VyOS / OPNsense / Netgear(構文・ポリシーの決定論チェック)。AIレビューはベンダ非依存
- 言語: Python 3.12(決定論チェック・レポート統合スクリプト)
- ※ Bob の API キー(
BOBSHELL_API_KEY)は事前に取得して、GitLab の CI/CD Variables に Masked で入れておく
つくったものの全体像
「Config を GitLab に push したら、CIがレビューして結果を残す」だけのシンプルな仕組みです。ポイントは 2層構えにしたこと。
図の ③ aireview が今回の主役。ここで 1層目の決定論チェック(lint) で拾いきれない、設計・運用寄りのレビューを Bob に任せています。
- 1層目=決定論チェック:「telnet使ってたらアウト」みたいな、規則で機械的に判定できるものは Python + YAML ルールで先に弾く。critical違反はパイプラインを赤にしてゲートにします。ここはAIに任せない(毎回ブレられても困るので)。
- 2層目=AIレビュー:決定論では表現しづらい 「この変更の影響範囲」「意図の妥当性」「冗長構成の非対称」 みたいな、自然言語じゃないと厳しいところを Bob に任せる。
差分管理は Git 本体そのまま(commit履歴=変更履歴)。CIは毎回「直前リビジョンとのdiff」だけをレビュー対象にします。
そして地味に大事なのが AIを差し替え式にしたこと。AI_BACKEND という変数で mock(疎通確認)/ openai_compat(社内vLLM等のOpenAI互換)/ bob を切り替えられるようにして、ci/adapters/ にアダプタを置きました。開発中は Bobにキーを通す前から mock と社内LLM で本番同等に回して検証できて楽だった、というのもあるんですが、それ以上に効いてくるのがこの一点です。
「NW機器のConfigを外部に送るなんてどういうことだ!」って怒られそうな環境でも、ローカルLLMや社内LLM(OpenAI互換)にそのまま対応できる余力を残している —— ここが大きい。Config は機微な情報のかたまりなので、「データを外に出せない」現場は普通にあります。そういう場所でも AI_BACKEND を差し替えるだけで同じ仕組みがそのまま使えるようにしてあります。もちろん Bob が使える環境なら Bob のリッチな考察の恩恵を受ける、という使い分けです。
設計編:仕組みとConfig実体を「2-repo」で分ける
ここが今回いちばん気に入っている構成の話。「仕組み(フレームワーク)」と「Configの実体」を別リポジトリに分けました。
- 仕組み repo(1本) … CI/CDテンプレ + policy(YAML) + prompts(AIへの指示) を持ち、それらを焼き込んだ Runnerイメージをビルドして社内レジストリに push する。Config の実体は一切持たない。
- Config repo(テナントごと、既存 repo を使う) … 機器 Config の実体だけを置く。機器ごとには増やさず、テナント(拠点/組織)単位。
Config repo 側の .gitlab-ci.yml は、仕組み repo のテンプレを include する数行だけ。既存の機器バックアップ repo に、これを足すだけでオンボード完了です。
# テナント側 .gitlab-ci.yml はこれだけ
include:
- project: 'infra/nw-config-review' # 仕組み repo
file: '/templates/review.gitlab-ci.yml'
ref: main
variables:
AI_BACKEND: "bob"
CONFIG_DIR: "." # 既存repoはルート直下に .txt が多いので既定 "."
この分離のうれしさ:
- 仕組みの更新が全テナントに一括で効く。 スクリプト・既定 policy・prompts はイメージに焼き込んであるので、仕組み repo を直せば、各 Config repo は何も変えなくても次のパイプラインから新ルールで回る。
-
既存の Config バックアップ repo をそのまま使える。 Oxidized/AWX が吐いてる機器バックアップに
include数行で相乗り。新規 repo を作らない。 - 権限・履歴がテナントごとに閉じる。 ある拠点の Config はその repo の中だけ。横に漏れない。
個別プロジェクトごとに「ルール・観点」を自然言語で足せる
そして地味に効くのがこれ。テナントごとに、その環境固有のルールや観点を足せるようにしてあります。土台(仕組みイメージ)は1つのまま、味付けだけ拠点ごとに変えられる。
決定論ルールは YAML。1ルール = severity + カテゴリ + 対象ベンダ + 自然言語の message/remediation + 判定 regex、という形です。
rules:
- id: RTX-TELNETD-ENABLED
severity: high
category: security
vendors: [yamaha_rtx]
message: telnetd(平文telnet)が有効
remediation: "telnetd service off にし sshd service on(SSH)へ移行"
forbid: '(?i)^\s*telnetd (service on|host\s+(?!off))'
Config repo 側に policy/ を置けば、イメージ既定のルールに"後勝ち"で併用されます。「この拠点はこのVLANをこう使う」「ここは特例でこの設定はOK」みたいな環境固有ルールを、その repo の中にだけ足せるわけです。AIレビューの観点そのものも、prompts/ を置けば拠点ごとに上書きできます。
分かりやすい例が HA(冗長)ペアの同期チェック。「この2台は冗長ペアだよ」と 1ファイル教えておくだけで、パイプラインが2台のConfigを突き合わせて"非対称"を指摘してくれます。
# ha_pairs.yaml — 「この2台はHAペア」と教えるだけ
pairs:
- name: core-edge
members:
- rtx1300-01.txt
- rtx1300-02.txt
これを置いたテナントだけ HA 同期チェックが起動し、「NATディスクリプタが片側だけ」「フィルタが非対称」「ファーム版数がズレてる」みたいな、1台のConfigを見てるだけでは絶対に分からない指摘が出ます。実際これで、稼働中のHAペアで NAT設定7項目のドリフトを検出できました。個別環境ごとにここまで細かいレビューを効かせられるのがちょー嬉しいところです。
核心:bobshell を非対話でレビュー器として叩く
アダプタ本体(ci/adapters/bob.py)がやってることはシンプルで、system指示を -p に、タスク+差分を stdin に渡すだけです。
DEFAULT_CMD = "bob --accept-license --auth-method api-key --hide-intermediary-output"
# -p に指示(system)を、差分本体は stdin から文脈として渡す
instruction = system_prompt + "\n\n差分とポリシーは標準入力で与えます。指定のJSON形式のみで出力してください。"
cmd = shlex.split(DEFAULT_CMD) + ["-p", instruction]
proc = subprocess.run(
cmd, input=user_prompt, # ← user_prompt(タスク+差分)を stdin へ
capture_output=True, text=True, encoding="utf-8",
env=env, timeout=timeout_s,
)
非対話で使う際のフラグはここがキモなので、忘れずに付けます。
| フラグ | 役割 |
|---|---|
--accept-license |
ライセンス確認プロンプトでのハングを回避(自動化には必須) |
--auth-method api-key |
非対話は API キー認証を使う(環境変数 BOBSHELL_API_KEY) |
--hide-intermediary-output |
中間出力を抑えて最終結果だけ出す |
--yolo |
全ツール自動承認(=書き込みも通る)。今回は付けない(後述) |
出力は prompts/system.md で JSON強制にしています。CLIにJSON整形フラグは無いので、応答から { ... } を頑健に拾う処理を Python 側に入れておくのが安全です(コードフェンスや前置きが付いてても拾えるように)。
# AI応答からJSONを頑健に取り出す(前後に説明文やコードフェンスがあっても拾う)
m = re.search(r"```(?:json)?\s*(\{.*\})\s*```", raw, re.DOTALL)
candidate = m.group(1) if m else raw
if not m:
s, e = candidate.find("{"), candidate.rfind("}")
if s != -1 and e != -1:
candidate = candidate[s:e+1]
obj = json.loads(candidate)
Bob に渡している system プロンプトはこんな感じ(要約)。「シニアNWエンジニア兼セキュリティ監査担当として、理論より実害の観点で、決定論チェックと重複しない指摘を」とお願いしています。
Runnerイメージに bob を焼き込む(ここでハマった)
Bob CLI は curl -fsSL https://bob.ibm.com/download/bobshell.sh | bash で入るのですが、これを Dockerfile に素直に書いたら 見事にコケました。
Node.js is not installed
…そう、bobshell は Node.js 製の npm パッケージ(engines: node>=20)なんですね。python:3.12-slim には当然 Node が入ってないので、公式インストーラが即死する、というオチでした。技量不足で30分くらい悩みましたw
修正は、Node を先に入れてから公式インストーラを叩くだけです。
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates curl
# bob は Node製。先に Node 22 を入れる
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& node -v && npm -v
# Node を入れてから公式インストーラを叩けば通る
RUN curl -fsSL https://bob.ibm.com/download/bobshell.sh | bash \
&& bob --version
※ 閉域環境(外に出られないクラスタ)向けには、配布tgzを vendor/ に取り込んで npm install -g vendor/bobshell-x.y.z.tgz する構成にしておくと、インストーラが外に取りに行けなくても再現ビルドできます。SHA256 を控えておくと安心。
実測で bob --version → 1.0.6、node -v → v22.x。イメージは Node込みで250MBくらいに収まりました。
そのほかハマったところ
1. 監視エージェントのログで stdout が汚れて findings=0
これは環境依存ですが、ハマると原因が分かりにくいやつ。Bob は Node 製なので、クラスタに入れてる APMエージェントが NODE_OPTIONS で collector を注入してきて、その計測ログ(JSON)が bob の stdout に混ざるんですね。結果、こっちのJSON抽出が壊れて 指摘ゼロ件になる、という。
対処は、bob を呼ぶ subprocess だけ計測を切る+抽出時にノイズ行を捨てる、の二段構え。
env = dict(os.environ)
env["INSTANA_DISABLE"] = "true"
if "instana" in env.get("NODE_OPTIONS", "").lower():
env.pop("NODE_OPTIONS", None) # bob 呼び出しからは計測を外す
# 抽出側でも計測ログっぽい行は捨てる
for ln in raw.splitlines():
if '"@instana/' in ln or re.match(r'\s*\{"(?:level|time|threadId|name|msg|pid)":', ln):
continue
...
2. クラスタ内だと間欠的に落ちる
ローカルでは安定してるのに、クラスタ内で回すと たまに unexpected critical error を返すことがありました(egressのゆらぎ疑い)。原因を潰しきれなかったので、正直にリトライで吸収しています。数回投げれば通る。
attempts = int(os.environ.get("BOB_RETRIES", "2")) + 1 # 最大3試行
for n in range(1, attempts + 1):
... # 失敗したら軽くバックオフして再試行
3. --yolo は付けない(read-onlyで使う)
Bob の非対話は既定が read-only(非破壊ツールのみ)で、--yolo を付けると全ツール自動承認=ファイル書き込みや shell 実行まで通ります。Configレビューは configs を書き換える必要が無いので、既定の read-only のまま(--yolo を付けない)にしました。
そのうえで多層防御として、レビュー結果を push バックする直前に git checkout -- configs/ を実行して configの変更は問答無用で破棄、push するのは reviews/ 配下の md/json だけにしています。万一 Bob が何か書いても機器Configには絶対に戻らない、という念の入れよう。無限ループ防止に、書き戻しコミットは [skip ci] + git push -o ci.skip です。
実際に出てきた指摘
自社の検証環境のコアスイッチ3台(Arista + Yamaha RTX の混在)で回した実例。Bob は決定論チェックと重複しない、設計・運用寄りの考察まで出してくれました(一般化して抜粋)。
- EOL到達済のOSバージョンで動いている(セキュリティ更新が来ない)→ 計画的なアップグレードを推奨
- 管理系プロトコルが平文(telnetのみ) で、暗号化された経路が無い
- IPsecの事前共有鍵が平文で設定に残っている疑い
- 冗長構成(MLAG)が非対称:片側のスイッチにしか特定VLANが定義されておらず、フェイルオーバー時に到達性を失うリスク
- 単一障害点:あるルータが冗長化されておらず、落ちると当該セグメントが全断
「telnetダメ」みたいな単純なものは1層目の決定論チェックが機械的に拾いますが、「MLAGの片側だけVLANが無い」「この機種はもうEOL」 といった、設定全体を横断して初めて分かる指摘をAIが出してくれるのが効いてます。ここが2層構えにした狙い通りのところ。
なお、どのAIでレビューしたかは監査上わりと重要なので、レポート冒頭に 「レビュー生成AI: IBM Bob Shell」 と明記するようにしました。決定論チェックはAI非関与なので別枠表示にしています。
さいごに
というわけで、bobshell の非対話モードを「差分を stdin、指示を -p、出力はJSON強制」で叩くだけで、CI/CDに乗る立派なNWレビュー器になりました。いまは複数テナントのConfigリポジトリを、仕組み側リポジトリを include 数行でオンボードする形で運用しています。決定論ゲート+AI考察の2層は、思ったより実運用でしっくりきています。
Bob は「エージェントのアクションが追える(監査できる)」のを推してる製品なので、規制の厳しい環境のインフラ変更レビューとはかなり相性が良さそう、というのが触ってみての感想です。
今後やりたいこと(TODO):
- HA(冗長)ペアの同期チェックを、もっと多機種に対応させる
- ファーム最新版レジストリと突き合わせた版数監査の自動化(メーカーサイトの巡回)
-
read-only の custom mode(
--chat-mode)を用意して、より意図を絞ったレビューにする - OPNsense / Netgear のポリシールールを実Configでチューニング
後日、もう少し運用が回ったら結果をフィードできればと思います。それでは、"待機系に入れ忘れ" のない穏やかな運用を。
参考:
- IBM Bob(公式) https://bob.ibm.com/
- Bob Shell Docs https://bob.ibm.com/docs/shell
