現場で起きる問題:深夜にひっそりと発生するジョブの失敗
日曜日の午前2時。Debian本番サーバーで夜間バックアップジョブが実行されます。手動テストでは何の問題もなくスクリプトが動作したため、データは安全だと確信して業務を終えました。しかし10日後、惨劇が起こります。破損したテーブルを復元しようとAWS S3バケットを開くと、過去1週間分のスナップショットが1つも存在しなかったのです。ローカルのPostgreSQLコンテナの起動に通常より8秒長くかかったことが原因で、バックアップは2日目の夜にクラッシュしていました。
手がかりを求めて必死にマシンへSSH接続し、/var/log/syslogを確認しても何も記録されていません。/var/mail/rootを調べても、実際のスタックトレースを見つけ出すのは干し草の山から針を探すようなものです。アップロードがタイムアウトしたのか?エクスポートされていない環境変数が原因でスクリプトが壊れたのか?Cronは終了コードも、実行時間も、依存関係の保護も一切提供してくれません。
4年間にわたり14台のUbuntuおよびDebianノードを管理してきた中で、私はこの痛烈な教訓を得ました。閑散期の移行作業中に発生したサイレントエラーにより、エンジニアリングチームは週末中を手動復旧に費やす羽目になったのです。このインシデントをきっかけに、私たちは重要なインフラにおいてcronを完全に廃止することを決意しました。
根本原因の分析:なぜCronはモダンなスタックで苦戦するのか
サーバーが静的でモノリシックな構成だった時代、CronはUnixにおいて優れた役割を果たしました。しかし現在のインフラは動的で、コンテナ化され、マイクロサービスに分散されています。このような環境において、Cronは主に4つの弱点によって時代遅れとなりつつあります。
- 依存関係を一切認識できない: Cronは時計の時刻だけに完全に依存しています。ネットワークインターフェースが起動しているか、データベースソケットが開いているか、NFSマウントが存在するかどうかは検証しません。外部の依存関係に遅延が生じると、ジョブは即座に失敗します。
- 失われ、断片化するログ: 標準的なcronは標準エラー出力をPostfixなどのローカルMTA(メール転送エージェント)にパイプします。最小構成のクラウドイメージではデフォルトでメールサーバーが省略されているため、重要なエラートレースは何の警告もなく
/dev/nullに消え去ってしまいます。 - 極限まで削ぎ落とされた実行環境: Cronは切り詰められた
$PATH(多くの場合は単に/usr/bin:/binのみ)を持つ、最小限のシェル内でタスクを実行します。対話型のbashセッションでは問題なく動作するスクリプトでも、aws-cliやdocker-composeなどのカスタムツールが見つからず、cron経由ではクラッシュしてしまいます。 - リソース制限やタイムアウトの欠如: ネットワーク呼び出しがハングした場合、cronプロセスは無期限に開いたままになります。翌日の夜になれば、その横に2つ目のインスタンスが生成されます。やがて暴走したジョブが積み重なり、CPU負荷が100%に跳ね上がり、利用可能なRAMを枯渇させます。
ソリューションの比較:Cron vs Systemd Timer
Ubuntu 22.04+、Debian 12、Rocky Linux 9などの最新ディストリビューションでバックグラウンドタスクをスケジュールする場合、2つのネイティブな選択肢があります。従来のcrontabとsystemdタイマーの比較は以下のとおりです。
| 機能 | Crontab | Systemd Timer |
|---|---|---|
| タイミングの精度 | 1分単位の間隔のみ | サブ秒単位の精度に加え、ジッター(RandomizedDelaySec)を標準搭載 |
| ログの可視性 | 信頼性の低いローカルメールファイルや手動のリダイレクトラッパーが必要 | PID、終了コード、実行時間を備えたネイティブなjournalctlによる追跡 |
| 依存関係チェック | なし(扱いにくいuntil pg_isready; do sleep 1; doneループが必要) |
組み込みの宣言的ディレクティブ(After=、Requires=、Wants=) |
| ダウンタイムへの対応 | サーバーが再起動中や停止中だった場合、完全にスキップされる | Persistent=trueにより、起動時に逃した実行を自動的にトリガー |
| リソース制限 | niceやcpulimitなどのサードパーティユーティリティが必要 |
ネイティブなcgroup制限(例:MemoryMax=512M、CPUQuota=50%) |
ステップバイステップ:堅牢なSystemd Timerの構築
systemdタイマーは、/etc/systemd/system/配下に配置される対となる2つのファイルで構成されます。
- サービスユニット(
*.service):トリガーする実行可能ファイル、実行ユーザー、事前に準備が必要なシステムターゲットを定義します。 - タイマーユニット(
*.timer):スケジューリングのトリガー、リトライ動作、遅延間隔を定義します。
ステップ1:メンテナンススクリプトの作成
/usr/local/bin/backup-task.shに本番環境用のバックアップスクリプトを作成しましょう。
sudo nano /usr/local/bin/backup-task.sh
厳格なエラーハンドリングを有効にした以下のスクリプトを貼り付けます。
#!/usr/bin/env bash
set -euo pipefail
echo "[$(date --iso-8601=seconds)] 自動バックアップタスクを開始します..."
# バックアップ先ディレクトリの設定
BACKUP_DIR="/var/backups/app"
mkdir -p "${BACKUP_DIR}"
# システム設定ファイルのアーカイブ
tar -czf "${BACKUP_DIR}/config-$(date +%Y%m%d%H%M).tar.gz" /etc/hosts
echo "[$(date --iso-8601=seconds)] バックアップが正常に完了しました。"
スクリプトに実行権限を付与します。
sudo chmod +x /usr/local/bin/backup-task.sh
ステップ2:サービスユニットの作成
次に、サービス設定を作成します。タイマーがサービスの起動を管理するため、ここには[Install]セクションを含めない点に注意してください。
sudo nano /etc/systemd/system/backup-task.service
以下のディレクティブを追加します。
[Unit]
Description=Automated Application Backup Service
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-task.sh
User=root
StandardOutput=journal
StandardError=journal
Restart=on-failure
RestartSec=30s
TimeoutStartSec=15m
MemoryMax=512M
これらの設定がタスクを強固にする理由は以下のとおりです。
Type=oneshot: このタスクが1回だけ実行され、処理を完了して正常終了することをsystemdに伝えます。After=network-online.target: ネットワークスタックがIPアドレスを取得し、インターネットルーティングが確立されるまでタスクを一時停止します。TimeoutStartSec=15m: バックアップが停止して15分を超えた場合にプロセスを自動終了させ、孤立したゾンビプロセスの発生を防ぎます。MemoryMax=512M: ジョブが重要なアプリケーションのメモリを圧迫する前に強制終了させ、システムの安定性を保護します。
ステップ3:タイマーユニットの作成
続いて、まったく同じベース名を使用してタイマーファイル(backup-task.timer)を作成します。
sudo nano /etc/systemd/system/backup-task.timer
スケジュールを設定します。
[Unit]
Description=Trigger Automated Backup Daily at 02:00 UTC
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=5m
[Install]
WantedBy=timers.target
これらのスケジュールディレクティブの機能:
OnCalendar=*-*-* 02:00:00: 毎日UTC午前2時に実行します。カレンダー文字列はsystemd-analyze calendar "*-*-* 02:00:00"を使用していつでもテストできます。Persistent=true: 仮想マシンがメンテナンスのために午前2時に停止していた場合でも、OSの起動が完了した瞬間にジョブを実行して追いつきます。RandomizedDelaySec=5m: 最大300秒のランダムなオフセットを導入します。50台のインスタンスが同じストレージゲートウェイにバックアップする場合でも、意図しないDDoS的なトラフィック急増を防ぐことができます。
ステップ4:有効化、テスト、および監視
systemdに新しいユニットを認識させ、タイマーを開始します。
# systemdマネージャーのリロード
sudo systemctl daemon-reload
# タイマーを有効化し、直ちに起動
sudo systemctl enable --now backup-task.timer
# タイマーがアクティブであることを確認
sudo systemctl list-timers backup-task.timer
サービスの検証のために午前2時まで待つ必要はありません。今すぐオンデマンドで実行してみましょう。
sudo systemctl start backup-task.service
journalctlを使用してリアルタイムに出力を確認します。本番環境で複数サーバーのログを集中管理したい場合は、systemd-journal-remoteによるログの一元管理を導入するのも効果的です。
sudo journalctl -u backup-task.service -n 20 --no-pager
ミリ秒単位の正確なタイムスタンプ、プロセスIDタグ、経過実行時間、明示的な終了コード(status=0/SUCCESS)など、実行の完全な可視性が得られます。さらに本格的なLinuxにおけるCgroups v2とsystemd Sliceによるリソース制限の管理や、予期せぬメモリ圧迫にはsystemd-oomdの設定を組み合わせることで、サイレントエラーも、失われるログも、予期せぬ障害も防ぐことができます。

