はじめに
ある日、上長から**「StandbyDB(#2~#4)のHAProxy閉塞を一括でパパッと実行できるスクリプトを作ってほしい」**という要望(ミッション)が下りました。
私たちの環境では、東西に分かれたZBX(運用管理)サーバーや、複数のHAProxy、そしてPostgreSQLのクラスターが複雑に絡み合っています。これまでは障害時やメンテナンスの際、個別の制御スクリプトを1台ずつ手動で叩いて対応していましたが、「オペレーションミスを減らし、もっと安全かつ爆速で切り替えたい」というのが今回の自動化の背景です。
現場特有の泥臭い仕様や、東西サーバー間のネットワーク跨ぎ、開発環境(ラボ)ならではのパスのズレなど、考慮すべき仕様が山積みでした。
そこで今回は、AI(Gemini)を臨時の「シニアペアプロ相手」として迎え、一緒にスクリプトを作り上げることにしました。Gemini先生、よろしくお願いします!
結果、紆余曲折ありつつも3時間程度で完成しました。
この記事は、単に「完成した便利なスクリプトのコード」を共有するだけのものではありません。一見すると一発ではAIに伝わりそうにない複雑な業務要件を、どのように段階的なプロンプト(指示)で具体化し、エラーの壁を2人で乗り越えて堅牢なスクリプトへ昇華させていったかという、AIとのリアルなペアプロの軌跡をまとめたナレッジ共有記事です。
背景とシステム構成:なぜこのスクリプトが必要なのか?
今回作成したスクリプトの全体像を理解する上で、まずは私たちの環境の「DB参照設計」と、運用上で抱えていた「致命的な課題」について整理します。
1. 通常時の設計:HAProxyによるDB参照の振り分け
私たちの環境では、HAProxyを使って以下のような優先順位でDB(PostgreSQL)への参照を固定で振り分けています。
-
優先順位:
自サイト(#4 -> #3 -> #2)➔他サイト(#4 -> #3 -> #2) - 通常時の挙動: 普段、東サーバーのHAProxyは東のDB(Active)を参照し、西サーバーのHAProxyは西のDB(Standby)を参照しています。
2. 発生していた課題:東西NW分断時の「サイレントな誤作動」
問題が起きるのは、東西間のネットワーク分断等により、東西のDB同期が外れてしまったときです。
HAProxy自体は「背後のDBが生きているか(プロセスがUPしているか)」しか見ていないため、「東西間のDBレプリケーション(同期)が外れていること」には気づけません。
その結果、西サーバーのHAProxyは、同期が切れた(=分断以前の古い情報が残ったままの)西DBマスターをそのまま参照し続けてしまい、システム各種に致命的な動作影響(誤作動)を及ぼすリスクがありました。
3. 今回の要件と「運用の割り切り」
このリスクを回避するため、上長から**「西DB(Standby)#2~#4のHAProxy閉塞を一括で行うスクリプト」**の作成が求められました。
上図の通り、西側のHAProxyから見て西DB(#2~#4)を一括で閉塞(×印)してしまえば、HAProxyのルーティング順に則って、西のHAProxyは強制的に他サイト(東の正常なActive DB)へ通信を見に行くようになるからです。
💡 運用の割り切りポイント
東西のNW障害の度合いによっては、西のHAProxyが東のDBにすら辿り着けないことにより、全損エラーになる可能性もゼロではありません。しかし、**「古い情報を中途半端に参照させてシステムを誤作動させるくらいなら、いっそエラーで綺麗に落ちてくれた方がマシ」**という、実運用に即した非常に合理的な割り切り(設計思想)のもと、このスクリプトの作成が進められました。
開発の足跡:Geminiにぶつけた4つの要件と紆余曲折
ここからは、私がGeminiへどのように要件を伝え、どのような壁にぶち当たり、それをどうやって一緒に乗り越えていったかというペアプロの全記録です。
段階的に機能を肉付けしていく「インクリメンタル(反復的)開発」のリアルなプロセスをご覧ください。
Step 1:複数台の一括処理と、出力の最適化
🔹 私が伝えた要件
既に haproxy_change.sh(HAProxy操作スクリプト)が存在していたので、過去のスクリプト作成経験からなんとなく、「この個別スクリプトを必要ノード分、for文で複数回回せばいけるんじゃない?」という仮説はたててました。Geminiに個別スクリプトのコードや haproxy.list・動作例を共有した上で、以下のように投げました。
「既存の
haproxy.list(CSV形式)から、指定したSYSTEM(tom1/tom2)とKYOTEN(omc/khn)に合致するバックエンドDBのノード名(4列目)を自動で抽出して、個別操作スクリプトhaproxy_change.shをループで複数回呼び出す形にしたい」
❌ 発生した問題:ログのゲシュタルト崩壊
最初のコードはすぐに出来上がりました。CSVから対象ノードを綺麗に重複排除して抽出し、順次実行するシンプルなループ構造です。
しかし、実際に -status(状態確認)を動かしてみると、致命的な見づらさに気づきます。
既存の個別操作スクリプトは「指定されたDBノードが属するサービス(db等)の状況を、フロントエンド側からまとめて全て取得する」という仕様でした。そのため、対象ノードが3台あれば、3回とも全く同じ全体ステータスが画面に繰り返し出力されてしまい、ログが恐ろしく無駄に長くなるという問題が発生したのです。
✨ 解決策:status時は「代表1台」のみ実行
Geminiに画面ログをそのままフィードバックしたところ、以下のスマートな条件分岐を提案してくれました。
-
-enableや-disableは、当然全台分ループ実行する。 -
-statusの時だけは、head -n 1で取得した 最初の1台だけを代表として実行する。
これで、画面出力が被ることなく、一瞬でスッキリと全体のステータスを確認できるようになりました。
Step 2:東西ZBXのフル連動と「排他制御」の罠
🔹 私が伝えた要件
「HAProxy(フロント)は東と西の両環境で個別に稼働している。東のDBを閉塞したい時は、東ZBXから制御するHAProxyだけでなく、西ZBXから制御するHAProxyでも東のDBを閉塞してあげる必要がある。どちらのZBXでコマンドを叩いても、自動的もう片方のZBXへSSHして東西両方で閉塞処理が走るようにしたい」
❌ 発生した問題:共有ストレージによる排他制御(二重起動)の壁
参考となる別の東西連動スクリプト(LB用)をヒントとして渡し、対向ZBXへ sshpass + ssh 経由で自分自身(親スクリプト)を遠隔起動させるロジックを組み込みました。
しかし、いざテスト環境で実行すると、対向側で以下のエラーを出して激しく弾かれました。
排他制御:別セッションで実行中です。
原因は、私たちの環境のツールディレクトリが、東西のZBXサーバー間で共有ファイルシステム(NFS等)として共通化されていたことでした。
東ZBXで親スクリプトを起動した時点で共有ストレージ上にロックファイルが作られるため、西ZBXへSSHして再度親スクリプトを起動しようとすると、共通関数の二重起動チェックに引っかかって自爆してしまったのです。
✨ 解決策:対向では「個別スクリプト(子)」を直接ループ実行する
ここで、ヒントとして渡していたLB用スクリプトの構造をGeminiが再分析してくれました。「LB用スクリプトでは親ではなく、個別スクリプト(子)を直接SSHで叩いていますね」と。
そこで、対向ZBXに対して自分自身(親)を呼ぶのをやめ、抽出したノードに対して個別操作スクリプト(haproxy_change.sh)を直接SSH経由で送り込んでループ実行する形へシフトしました。
子スクリプト側は排他制御フラグが 0(チェックしない)になっていたため、ロックに干渉することなく、東西フル連動を安全にバイパス・実現することに成功しました。
Step 3:開発環境特有の「パスのズレ」を自動吸収
🔹 私が伝えた要件
「本番環境は東西で同じパスだけど、開発(ラボ)環境(dev)に限っては、大人の事情で東側と西側でディレクトリ名が別々に配置されている。これも人間が意識せずに、裏でパスを相互変換して動くようにしたい」
❌ 発生した問題:対向のパスが自分のパスに引っ張られる
単純に自分の環境の変数を引き継いでSSHを実行させてしまうと、西側から東側へSSHした際、東側には存在しない西用のパスを見に行ってしまい、No such file or directory で落ちてしまいました。
✨ 解決策:ホスト名と実行ディレクトリによる動的マッピング
「今自分がどの環境の、どのディレクトリにいるか」によって、対向サーバーのパス(CHANGE_SCRIPT2)を動的に決定する入れ子(ネスト)構造の条件分岐を実装しました。
if [ "${HOSTNAME:0:3}" = "dev" ]; then
if [ "${BASE_DIR}" = "/tom/tools" ]; then
CHANGE_SCRIPT2="${BASE_DIR}_khn/opebin/haproxy_change.sh"
elif [ "${BASE_DIR}" = "/tom/tools_khn" ]; then
CHANGE_SCRIPT2="/tom/tools/opebin/haproxy_change.sh"
...
これにより、開発環境のどちらのディレクトリでコマンドを叩いても、対向側の正しいパスへピンポイントに処理を委譲できるようになりました。
Step 4:DBクラスター状態からの「Standbyサイト自動判定」と堅牢化
🔹 私が伝えた要件
「そもそも人間が
-k omcのように『どっちがStandbyか』を調べて引数を指定するのすら面倒だし、オペミスが怖い。既存のpgdb_cluster_stat.shの出力結果を利用して、今Standbyになっている拠点をスクリプトに自動判別させたい。でも、緊急メンテ用に手動で明示的に指定(-k)できる仕組みも残してほしい」
❌ 潜んでいたリスク:不完全な自動判定は障害時に牙をむく
最初は、ログの中に It is not a Primary site. という文字列があれば自サイトがStandby、無ければ対向サイトがStandby、という単純な2択(if / else)で組んでいました。
しかし、ここで**「もし東西分断やDB全損、スプリットブレインなどの超異常状態で、どちらのサイトもPrimaryを特定できないログを吐いていたらどうする?」**という、インフラ特有の恐ろしいリスクに気づきます。2択のままだと、異常事態のログであっても強引に「対向サイトがStandby」と誤判定して突き進んでしまいます。
✨ 解決策:3段階のエラーハンドリングによる「安全な緊急停止」
この懸念を解決するため、条件分岐を3段階(if / elif / else)に厳格化しました。
-
if: ログにIt is not a Primary site.があれば ➔ 自サイトがStandby -
elif: ログにPrimaryがあれば ➔ 自サイトがPrimary(=対向サイトがStandby) -
else: どちらの文言も確認できない場合 ➔ 異常状態として即座にexit 1(停止)
else
# どちらの文言も確認できない場合 ➔ 異常状態としてエラー終了
echo " [ERROR] クラスター状態の自動判定に失敗しました。Primaryサイトを正常に特定できません。"
echo "--- pgdb_cluster_stat.sh 出力ログ ---"
echo "${STAT_OUT}"
exit 1
fi
万が一 else に落ちて緊急停止する際は、現場のオペレーターが「何が原因で自動判定に失敗したのか」をその場で即座に判断できるよう、生ログを画面にそのままダンプする親切設計にしました。
さらに、確認メッセージの手前で判定結果をエコーバックさせることで、作業者が今から何が起きるかを100%確信して「yes」を押せる、極めて堅牢な運用スクリプトへと進化させました。
完成したスクリプトの骨格(汎用リネーム版)
プロダクションコードは機密保持のためそのまま公開できませんが、今回の東西連動・自動判定ロジックのエッセンスを100%詰め込んだ汎用テンプレートを作成しました。
拠点間を跨ぐリモート一括制御スクリプトを書きたい方の参考になれば幸いです。
#!/bin/bash
# 実行ユーザーチェック等の初期処理
if [ ! "$USER" = "tool_user" ]; then
echo "Error: Invalid user."
exit 1
fi
export SCRIPT_NAME=$(basename $0)
export SCRIPT_DIR=$(cd $(dirname $0); pwd)
export BASE_DIR=$(echo $SCRIPT_DIR | sed -e 's/\/[a-zA-Z0-9_-]*$//')
# 環境固有の共通定義ファイル等の読み込み(環境に合わせて調整)
# [ -f ${BASE_DIR}/common.env ] && . ${BASE_DIR}/common.env
function func_usage {
cat <<EOF
Usage:
$(basename ${0}) -s SYSTEM [-k SITE] -disable
$(basename ${0}) -s SYSTEM [-k SITE] -enable
$(basename ${0}) -s SYSTEM [-k SITE] -status
EOF
}
# --- 引数チェック & バリデーション ---
function func_option_check {
while [ $# -gt 0 ]; do
case "${1}" in
-s) export SYSTEM=${2}; shift ;;
-k) export SITE_OPT=${2}; shift ;;
-disable) ope="disable" ;;
-enable) ope="enable" ;;
-status) ope="status" ;;
*) echo "Invalid option"; func_usage; exit 1 ;;
esac
shift
done
# 自拠点の環境判定(ホスト名やパスから判定するロジック)
# 例としてここではデフォルト値を設定
MY_SITE="east"
OTHER_SITE="west"
if [ ! "${ope}" = "disable" ] && [ ! "${ope}" = "enable" ] && [ ! "${ope}" = "status" ]; then
func_usage; exit 1
fi
}
func_option_check $*
# 各種依存スクリプトの定義
LISTFILE=${BASE_DIR}/list/target.list
CHANGE_SCRIPT=${SCRIPT_DIR}/node_change.sh
STAT_SCRIPT=${SCRIPT_DIR}/cluster_stat.sh
# --- 対象拠点の決定(手動指定 or ステータスによる自動判定) ---
if [ -n "${SITE_OPT}" ]; then
echo "[-k] オプションが指定されたため、自動判定をスキップします。"
export TARGET_SITE="${SITE_OPT}"
else
echo "クラスター状態を確認し、対象サイトを自動判定しています..."
STAT_OUT=$("${STAT_SCRIPT}" -s "${SYSTEM}" 2>&1)
if echo "$STAT_OUT" | grep "It is not a Primary site." > /dev/null 2>&1; then
export TARGET_SITE="${MY_SITE}" # 自サイトが Standby と判定
elif echo "$STAT_OUT" | grep "Primary" > /dev/null 2>&1; then
export TARGET_SITE="${OTHER_SITE}" # 対向サイトが Standby と判定
else
# 【Step 4の実装】どちらも特定できない異常系は安全に緊急停止
echo "[ERROR] クラスター状態の自動判定に失敗しました。生ログを出力します。"
echo "${STAT_OUT}"
exit 1
fi
fi
# 決定されたターゲット拠点に応じて、リスト内の検索パターンを決定
if [ "${TARGET_SITE}" = "east" ]; then
NODE_PATTERN="node-e"
else
NODE_PATTERN="node-w"
fi
# 【Step 1の実装】直感的な grep パイプラインで対象ノードを一括抽出(重複排除)
BACKNODES=$(cat "$LISTFILE" | grep -v "^#" | awk -F, '{print $4}' | grep "${SYSTEM}" | grep "${NODE_PATTERN}" | sort | uniq)
if [ -z "$BACKNODES" ]; then
echo "対象ノードが見つかりませんでした。"
exit 0
fi
# 実行確認(変更処理の場合のみ)
if [ "${ope}" != "status" ]; then
echo "--> 対象サイトは [ ${TARGET_SITE} ] です。"
# 必要に応じてここに確認プロンプト(read等)を挟む
fi
# --- 1. 自拠点での処理(ローカル実行) ---
if [ "${ope}" = "status" ]; then
# 【Step 1の実装】status時は重複を避けるため代表1台のみ実行
REPRESENT_NODE=$(echo "$BACKNODES" | head -n 1)
bash "${CHANGE_SCRIPT}" -n "${REPRESENT_NODE}" -status
else
# 変更時は全ノードを順次実行
for node in $BACKNODES; do
bash "${CHANGE_SCRIPT}" -n "${node}" -"${ope}" -FORCE
done
fi
# --- 2. 他拠点(対向)での処理(リモート実行) ---
# 対向の接続先ホストの決定
if [ "${MY_SITE}" = "east" ]; then
remote_host="remote-zbx-west"
else
remote_host="remote-zbx-east"
fi
# 【Step 3の実装】開発環境等のパスのズレを動的に吸収するロジック
if [[ "${HOSTNAME}" =~ "dev" ]]; then
if [ "${BASE_DIR}" = "/path/to/tools" ]; then
CHANGE_SCRIPT2="/path/to/tools_west/opebin/node_change.sh"
else
CHANGE_SCRIPT2="/path/to/tools/opebin/node_change.sh"
fi
else
CHANGE_SCRIPT2="${CHANGE_SCRIPT}"
fi
echo "対向のサーバー(${OTHER_SITE})でも同じ処理をリモート実行します..."
if [ "${ope}" = "status" ]; then
REPRESENT_NODE=$(echo "$BACKNODES" | head -n 1)
# 【Step 2の実装】親ではなく「子スクリプト」を直接SSHで叩いて排他制御を回避
ssh ${remote_host} "bash ${CHANGE_SCRIPT2} -n ${REPRESENT_NODE} -status"
else
for node in $BACKNODES; do
ssh ${remote_host} "bash ${CHANGE_SCRIPT2} -n ${node} -${ope} -FORCE"
done
fi
echo "すべての処理が完了しました。"
exit 0
AI(Gemini)とペアプロして感じたメリット
今回、1から自分で書いていれば数日はデバッグに頭を抱えていたであろうスクリプトが、Geminiとのペアプロによってわずか数時間で本番投入可能なクオリティまで仕上がりました。感じたメリットは以下の3点です。
-
暗号のようなコードのリファクタリングが爆速
awkやsedの記号が密集した複雑な文字列抽出ロジックを伝えた際、処理内容はそのままに「誰が見ても直感的に理解できる可読性の高いgrepパイプライン」へと一瞬でリファクタリングしてくれました。当初、リストから特定のシステムと拠点パターンに合致するノードを引っこ抜く際、Geminiからは以下のコードが提案されました。
▼ 当初提案されたコード(ちょっと暗号っぽい)
BACKNODES=$(cat "$LISTFILE" | grep -v "^#" | awk -F, -v sys="${SYSTEM}" -v pat="${NODE_PATTERN}" '$4 ~ sys && $4 ~ pat {print $4}' | sort | uniq)これでも完璧に動作はするのですが、
-vや~、&&などの記号が密集しており、パッと見で何をしているのかが分かりづらく、チームへの引き継ぎに少し懸念がありました。
そこで「処理内容は同じで、もっと記号を減らして可読性を上げてほしい」とリクエストしたところ、一瞬で以下のコードに生まれ変わりました。▼ リファクタリング後のコード(直感的な grep パイプライン)
BACKNODES=$(cat "$LISTFILE" | grep -v "^#" | awk -F, '{print $4}' | grep "${SYSTEM}" | grep "${NODE_PATTERN}" | sort | uniq)高度な
awkの中で条件判定を完結させるのではなく、「先に4列目(ノード名)を取り出してから、システム名で絞り、さらに拠点パターンで絞る」という一連の流れを使い慣れたgrepのパイプで繋ぐ形です。これなら、シェルスクリプト特有 of 暗号感が薄れ、左から右へ本を読むようにストレートにロジックが理解できます。チーム開発において、誰が見てもメンテナンスしやすいクリーンなコードへ瞬時に直してくれるのは非常に大きな価値だと感じます。
-
エラーログからの「意図を汲み取った」リカバリ力
対向ZBXでの排他制御エラーで詰まった際、私が「別のLB用スクリプトではこんな感じでSSHを叩いている」と参考例を渡しただけで、設計思想を即座にパースして軌道修正してくれました。当初、対向ZBXへは自分自身(親スクリプト)をそのまま引き継ごうとして二重起動エラーになりました。
▼ 当初の失敗コード(自分自身をリモート起動)
sshpass -p ${C_LinuxPass} ssh ${ssh_option} ${C_LinuxUser}@${node2} ${SCRIPT_DIR}/${SCRIPT_NAME} -s ${SYSTEM} -k ${OTHER_KYO} -${ope} -Fここで「LB用スクリプトの参考例」を渡したところ、Geminiは「LB用は親ではなく子スクリプトを直接叩いてロックを回避している」と見抜き、以下の実装へ一瞬で切り替えてくれました。
▼ 修正後の解決コード(個別操作の子スクリプトを直接起動)
sshpass -p ${C_LinuxPass} ssh ${ssh_option} ${C_LinuxUser}@${node2} "bash ${CHANGE_SCRIPT2} -n ${node} -${ope} -F"共有ストレージ上の排他制御に一切干渉しないため、東西フル連動が安全に開通しました。
-
インフラの「異常系」に配慮したガードレールの作成
「自動判定をさせたい」という人間のざっくりした要望に対し、ハッピーパス(正常系)だけでなく、予期せぬ異常事態の安全弁(ガードレール)を自ら提案・実装してくれたのには驚きました。最初は、ログの中に特定のキーワードがあるかないかの単純な2択で組んでいました。
▼ 当初の2択コード(正常系のみ考慮)
if echo "$STAT_OUT" | grep "It is not a Primary site." > /dev/null 2>&1; then export S_KYOTEN="${ACTIVE}" # 自サイトが Standby else export S_KYOTEN="${SAIGAI}" # 対向サイトが Standby fiしかしこれだと、東西分断やスプリットブレインなどの超異常状態で、どちらのサイトもPrimaryを特定できないログを吐いていた場合でも、強引に「対向がStandby」と誤判定して突き進んでしまいます。
Geminiはそこを危惧し、以下の3段階の堅牢なエラーハンドリングを実装してくれました。▼ 修正後の3段階ガードレール(異常検知付き)
if echo "$STAT_OUT" | grep "It is not a Primary site." > /dev/null 2>&1; then export S_KYOTEN="${ACTIVE}" elif echo "$STAT_OUT" | grep "Primary" > /dev/null 2>&1; then export S_KYOTEN="${SAIGAI}" else # どちらの文言も確認できない場合は、生ログを出して緊急停止 echo "[ERROR] クラスター状態の自動判定に失敗しました。" echo "${STAT_OUT}" exit 1 fiインフラ特有の「想定外のステータス」の際に確実に安全に止めるという、実運用で最も重要なガードレールをAI側から自発的に作り込んでくれた瞬間でした。
おわりに
Geminiに仕様を「丸投げ」するのではない、「現場の泥臭い前提条件」や「実際に発生したエラーログ」、「過去の優れたスクリプトの設計思想」を人間が正しくフィードバックし続けること。これこそが、AIの持つポテンシャルを120%引き出し、最高の成果物を作るための鍵だと実感しました。
もし、レガシーで複雑なインフラ運用の自動化に悩んでいる方がいれば、ぜひAIを「優秀なシニアエンジニア」として隣に座らせ、ペアプロを試してみてください!
おわおわり


