課題: 正本から自動適用する仕組みを作った

本番のcrontabは、リポジトリで管理する正本ファイル(以下expected_crontab.txt)から自動適用する仕組みにしました。差分があれば適用する、という単純な設計です。ただしこれは全cronを一度に書き換えられる操作なので、壊れた設定をそのまま適用してしまう事故は避けなければなりません。

最初に置いた安全装置は、crontabコマンド自身が持つ構文検査モード(-nオプション。ファイルを指定して構文だけ検証し、実際には適用しない)でした。不正な行があれば弾いてくれるはずです。

実測: 空ファイルは「正常」と判定される

不正な行を含むファイルと、空のファイルをそれぞれ検証しました。

$ crontab -n bad_cron.txt
"bad_cron.txt":1: bad minute
errors in crontab file, can't install.
$ echo $?
1

不正な行は期待どおり弾かれます。ところが空ファイルを渡すと、結果が変わります。

$ : > empty_cron.txt
$ crontab -n empty_cron.txt
The syntax of the crontab file was successfully checked.
$ echo $?
0

空ファイルは「構文検査に成功した」という扱いで、終了コードも0(成功)でした。 考えてみれば筋は通っています。空ファイルには不正な行がありません。「不正な行が無いこと」を検査するモードなので、行が0本でも検査対象が無いだけで、失敗する理由がありません。

なぜこれが危険なのか

自動適用のパイプラインは、expected_crontab.txtを先に構文検査してから適用する設計にしていました。もしこのファイルが何らかの事故(git操作のミス、生成スクリプトの異常終了、書き込み失敗など)で空になっていたら、構文検査はそのまま通過します。 そして「検査に成功したのだから安全」という前提のまま、空の内容を本番のcrontabへ適用してしまいます。

結果は、本番で動いていた全cronジョブの消失です。バックアップ・監視の疎通確認・証跡ログのローテーション・各種の定期処理が、次の実行タイミングから一斉に止まります。しかも構文検査自体はエラーを出さずに完走しているので、パイプラインのログを見ても「正常に適用しました」としか出ません。

「構文検査を通したから安全」という前提そのものが崩れていました。 構文検査が保証するのは「書いてある内容が壊れていないこと」だけで、「書くべき内容が実際に書いてあること」は別の話でした。

対策: 3段の関門にした

構文検査(以下、関門1)だけに頼らず、3段構成にしました。

関門2: 実行行数のサニティ検査

コメント行・空行を除いた「実行行」の数を数え、適用前後で比較します。

# 実行行(コメント・空行を除く)を数える
count_active() {
    grep -vE '^[[:space:]]*(#|$)' "$1" 2>/dev/null | wc -l | tr -d ' '
}

正本の実行行が0なら、無条件に適用を止めます。0でなくても、現在の実行行数に対して70%を下回る減少があれば適用しません。

NEW_N="$(count_active "$EXPECTED")"
CUR_N="$(count_active "$CURRENT")"

if [ "$NEW_N" -eq 0 ]; then
    echo "[NG] 正本の実行行が0です。全cronを消す変更は適用しません。"
    exit 1
fi

if [ "$CUR_N" -gt 0 ] && [ "$FORCE" = "0" ]; then
    THRESHOLD=$(( CUR_N * MIN_RATIO / 100 ))
    if [ "$NEW_N" -lt "$THRESHOLD" ]; then
        echo "[NG] 実行行が ${CUR_N} → ${NEW_N} と大きく減ります(閾値 ${THRESHOLD} 行)。"
        echo "     正本が事故で切り詰められた可能性があります。適用しません。"
        exit 1
    fi
fi

70%という閾値は「意図した削減」と「事故による消失」を区別するための線です。 ジョブを数本整理する程度の意図的な削減は70%を下回りません。一方、ファイルが空になったり大部分が消えたりする事故は、ほぼ確実にこの閾値を割ります。意図した大幅な削減であれば--forceで通せる設計にしてあり、この閾値は機械的に止めるためのもので、人間の判断を禁止するためのものではありません。

関門3: 適用後の再検査と自動復元

適用そのものが何かの理由で不完全に終わる可能性も残ります。適用のたびにバックアップを取り、適用後にもう一度、実際のcrontabと正本を突き合わせます。

BACKUP="${HOME}/crontab.bak.$(date +%Y%m%d%H%M%S)"
cp "$CURRENT" "$BACKUP"

crontab "$EXPECTED"

if bash verify_crontab.sh; then
    echo "[OK] 適用しました(正本と一致を確認)。"
else
    echo "[NG] 適用後の検査に失敗しました。バックアップから復元します。"
    crontab "$BACKUP"
fi

一致しなければ、直前に取ったバックアップへ即座に戻します。「適用した」という事実と「正しく適用された」という事実は別物なので、両方を機械的に確認してから完了とみなします。

まとめ

  • crontabの構文検査(crontab -n)は、空ファイルを「正常」として通します。不正な行が無いことしか見ておらず、書くべき内容が存在することは保証しません
  • 正本から本番へ自動適用する仕組みを作るときは、構文検査だけを安全装置にしないでください。全件が消えるような変更を機械的に止める、実行行数のサニティ検査が別途必要です
  • 閾値には意図した削減を通すための逃げ道(--force等)を用意し、機械判定と人間の判断を両立させてください
  • 適用は「実行した」で終わらせず、適用後に正本と実環境を再度突き合わせ、失敗していたらバックアップへ自動的に戻すところまでを一連の処理にしてください