午前2時、サーバーが炎上中
スマホにアラートが届く。本番サーバーのディスク使用量が94%に達している。眠い目をこすりながらSSH接続し、手作業でディレクトリを確認し、古いログファイルを一つずつ削除し、サービスを再起動する。1時間後にようやく解決――でも、来週また同じことが起きると分かっている。
その夜、自分に誓った。これを自動で処理するスクリプトを書こうと。怠けたいからじゃない。午前2時に人間はミスをするが、コンピューターはしないからだ。その瞬間、bashスクリプトは「いつか学ぶべきもの」から「今すぐ必要なもの」に変わった。
これが、あの頃あれば良かったと思うガイドだ。
基礎概念:bashスクリプトとは何か
bashスクリプトとは、シェルコマンドの一覧を上から下に実行するプレーンテキストファイルだ。それだけ。ターミナルで手入力するのと同じコマンドを、ファイルに保存して必要なときに実行するだけのことだ。
最初のスクリプト
hello.shというファイルを作成しよう:
#!/bin/bash
# これはコメントです
echo "サーバーは稼働中です: $(date)"
echo "稼働時間: $(uptime -p)"
最初の行#!/bin/bashはシェバンと呼ばれる。OSにどのインタープリターを使うかを伝える行だ――これがないと、システムがshや別のシェルでbashスクリプトを実行しようとして、思わぬ不具合が起きることがある。実行権限を付けて実行しよう:
chmod +x hello.sh
./hello.sh
変数
変数はスクリプト全体で再利用する値を格納する。鉄則が一つある:=の周囲にスペースを入れてはいけない。NAME="prod"は動く。NAME = "prod"はエラーになる。bashがNAMEをコマンドとして解釈し、実行しようとするからだ。
#!/bin/bash
SERVER_NAME="prod-web-01"
DISK_THRESHOLD=90
echo "サーバーを確認中: $SERVER_NAME"
echo "ディスク使用量がこれを超えたら警告: ${DISK_THRESHOLD}%"
変数の直後に別の文字が続く場合は${VAR}を使おう。$DISK_THRESHOLD_MBは黙って失敗する――bashが文字通りDISK_THRESHOLD_MBという名前の変数を探すからだ。${DISK_THRESHOLD}_MBと書けば、意図した通りになる。
コマンド置換
$()を使ってコマンドの出力を変数に直接格納しよう:
CURRENT_DISK=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
echo "現在のディスク使用量: $CURRENT_DISK%"
条件分岐
条件分岐により、スクリプトは単純なコマンドの再生から、システムの実際の状態に応答できるものに変わる。値の比較、ファイルの存在確認、ロジックの分岐ができるようになる:
#!/bin/bash
DISK_USAGE=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
THRESHOLD=85
if [ "$DISK_USAGE" -gt "$THRESHOLD" ]; then
echo "警告: ディスク使用量 ${DISK_USAGE}% — ログをクリーニング中"
find /var/log -name "*.log" -mtime +7 -delete
else
echo "ディスク正常: ${DISK_USAGE}%"
fi
[ ]内のスペースは必須だ――省略できない。["$DISK_USAGE" -gt "$THRESHOLD"]と書くと、bashは謎の「command not found」エラーを出す。シェルはそのスペースを必要としており、括弧をファイル名ではなくテストコマンドとして解析するためだ。
ループ
ループはリストに対して、あるいは条件が満たされている間、アクションを繰り返す:
# forループ — 項目を反復処理
for SERVICE in nginx mysql redis; do
systemctl is-active --quiet $SERVICE && echo "$SERVICE: 稼働中" || echo "$SERVICE: 停止中"
done
# whileループ — 条件が偽になるまで実行
COUNT=0
while [ $COUNT -lt 5 ]; do
echo "試行 $COUNT 回目"
COUNT=$((COUNT + 1))
done
関数
関数がなければ、10サービスのヘルスチェックは60行のコピペコードになる。関数を使えば20行で済む。一度定義すれば、必要な場所からいつでも呼び出せる:
#!/bin/bash
check_service() {
local SERVICE_NAME=$1
if systemctl is-active --quiet "$SERVICE_NAME"; then
echo "[OK] $SERVICE_NAME は稼働中"
else
echo "[FAIL] $SERVICE_NAME が停止中 — 再起動を試みます"
systemctl restart "$SERVICE_NAME"
fi
}
check_service nginx
check_service mysql
check_service redis
localキーワードは変数のスコープを関数内に限定する。これがないと、SERVICE_NAMEがグローバルスコープに漏れ出し、他の変数を黙って上書きする可能性がある――テスト中ではなく、まさに午前2時に表面化するタイプのバグだ。
実践練習:実際の監視スクリプトを作る
これは、4GB RAMを搭載した本番のUbuntu 22.04サーバーでcronを使って毎晩実行しているスクリプトだ。このスクリプトができる前は、ディスク問題の調査に手作業で20分かかっていた。今は自動でログが記録され、真夜中ではなく朝に問題が分かる。
ヘルスチェックスクリプト
#!/bin/bash
# server-health.sh — 本番ヘルスチェック
# 使い方: ./server-health.sh [--notify]
set -euo pipefail
# 設定
HOSTNAME=$(hostname)
ALERT_EMAIL="[email protected]"
DISK_WARN=85
RAM_WARN=90
LOG_FILE="/var/log/health-check.log"
# ログ出力ヘルパー
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# ディスク使用量を確認
check_disk() {
local USAGE
USAGE=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$USAGE" -gt "$DISK_WARN" ]; then
log "警告: ディスク使用量 ${USAGE}%"
return 1
fi
log "正常: ディスク使用量 ${USAGE}%"
return 0
}
# RAM使用量を確認
check_ram() {
local TOTAL FREE USAGE
TOTAL=$(free | awk '/Mem:/ {print $2}')
FREE=$(free | awk '/Mem:/ {print $4}')
USAGE=$(( (TOTAL - FREE) * 100 / TOTAL ))
if [ "$USAGE" -gt "$RAM_WARN" ]; then
log "警告: RAM使用量 ${USAGE}%"
return 1
fi
log "正常: RAM使用量 ${USAGE}%"
return 0
}
# 重要サービスを確認
check_services() {
local FAILED=0
for SVC in nginx mysql; do
if ! systemctl is-active --quiet "$SVC"; then
log "緊急: $SVC が稼働していません"
FAILED=$((FAILED + 1))
else
log "正常: $SVC 稼働中"
fi
done
return $FAILED
}
# 全チェックを実行
log "=== $HOSTNAME のヘルスチェック ==="
ALL_OK=true
check_disk || ALL_OK=false
check_ram || ALL_OK=false
check_services || ALL_OK=false
if [ "$ALL_OK" = false ]; then
log "ヘルスチェック失敗 — ログを確認してください"
exit 1
fi
log "全チェック完了"
cronで自動実行する
スクリプトを保存し、実行権限を付けてからスケジュールに登録しよう:
chmod +x /opt/scripts/server-health.sh
# crontabエディタを開く
crontab -e
# 30分ごとに実行
*/30 * * * * /opt/scripts/server-health.sh >> /var/log/health-check.log 2>&1
問題発生時のデバッグ
スクリプトは失敗する。その原因を突き止める方法を紹介しよう:
# 詳細出力で実行 — 各コマンドの実行前に表示される
bash -x ./server-health.sh
# 直前のコマンドの終了コードを確認
echo $?
# 厳格なエラー処理のためスクリプトの先頭に追加:
set -euo pipefail
# -e: エラー時に終了
# -u: 未定義変数でエラー
# -o pipefail: パイプ内のエラーをキャッチ
set -euo pipefailはすべての本番スクリプトの先頭に書くべきだ。これがないと、スクリプトが途中で黙って失敗し、原因を説明するログエントリも残らないまま、システムが中途半端な壊れた状態になりかねない。
引数の受け取り
ハードコードされたパスはスクリプトを脆くする。位置パラメーターを使えば実行時に値を渡せ、同じスクリプトを異なるディレクトリや環境で再利用できる:
#!/bin/bash
# 使い方: ./backup.sh /var/www/html /backups
SOURCE_DIR=$1
DEST_DIR=$2
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
if [ $# -ne 2 ]; then
echo "使い方: $0 <コピー元> <コピー先>"
exit 1
fi
tar -czf "${DEST_DIR}/backup_${TIMESTAMP}.tar.gz" "$SOURCE_DIR"
echo "バックアップ完了: ${DEST_DIR}/backup_${TIMESTAMP}.tar.gz"
$1と$2は第1引数と第2引数だ。$#は引数の総数を返す。$0はスクリプト名そのもの――誰かが間違った呼び出し方をした場合に、正確なusageメッセージを表示するのに便利だ。
手作業をやめよう
bashスクリプトは頭の良さを誇示するためのものではない――同じことを二度しないためのものだ。手作業でこなしている繰り返し作業はすべて、書かれるのを待っているスクリプトだ。
小さく始めよう。毎週手作業でやっていることを一つ選んで自動化する。ディスク容量の確認、ログのローテーション、ディレクトリのバックアップ――何でも構わない。スクリプトを書き、本番以外のマシンでテストし、cronでスケジュールに登録する。
目標は完璧なコードではない。次にディスクが午前2時に満杯になったとき、スマホが鳴る前にスクリプトがすでに対処している――それが目標だ。

