2-5. cron - 定期実行とその落とし穴

所要時間: 30-50分(がっつりなら2セッション分) ゴール: crontab を読み書きでき、cron 特有の罠(PATH最小、メール挙動、エラー埋没)を回避できる コミット内容: 自作 cron スクリプトと検証ログを ~/learn/linux/day205/ に保存

この章が終わるとできること

  • crontab の5フィールド 分 時 日 月 曜日 を即読み書きできる
  • */5 1-5 1,3,5 の記法と「曜日と日両指定で OR になる」罠を知っている
  • cron の PATH が最小である理由と 対策3つ を答えられる
  • >/dev/null 2>&1 を正しい順序で書ける
  • cron と systemd timer の使い分け基準を即答できる
  • flock で重複起動を防止できる

Day 11 とのつながり

  • 昨日学んだ systemd には .timer ユニットがあり、cron の 現代的代替 となる
  • 昨日と同じく「環境変数が無い」「相対パスNG」が cron でも刺さる ─ 構造は同じ
  • 「常駐は systemd、定期は cron か systemd timer」で運用の80%が片付く

これができると何が嬉しいか

  • ログ削除を手動でやる」から卒業 ─ OS が定時に勝手にやってくれる
  • 深夜3時のバックアップ失敗に翌朝気づく」を避けられる(失敗通知パターンを実装)
  • Day 13(ログ管理)の logrotate も、裏では cron か timer で動いている ─ 仕組みが見えてくる

大前提: なぜ「定期実行」がバックエンドの基本技能か

バックエンドシステムには 「定時に動かしたい処理」が無数にある

  • 毎日午前3時にDBバックアップ
  • 5分おきに監視対象 API のヘルスチェック
  • 毎時間ごとに集計バッチ
  • 1日1回 SSL 証明書の更新確認(certbot)
  • 古いログを30日後に圧縮、90日後に削除
  • オフピーク時間に重い分析クエリを実行

これを毎回手動で叩くのは現実的ではない。「人手を介さずに OS に時刻を任せて実行させる」仕組みが必要。

そのために何十年も使われ続けているのが cron。1975年に Unix V7 で初登場、今も全 Linux サーバーで現役。

ただし cron は 古いがゆえに罠が多い。「PATH が違って動かない」「エラーが起きても誰も気づかない」「環境変数が無いせいでスクリプトだけ別人の挙動」など、本番で痛い目を見るパターンは無限。

このレッスンでは、cron を「とりあえず動かす」のではなく、事故らない使い方を体系的に身につける。さらに、モダンな代替である systemd timer との使い分けも理解する。


セッション①: crontab の文法と仕組み(25-30分)

0. 録画スタート

mkdir -p ~/log ~/learn/linux/day205
cd ~/learn/linux/day205
script ~/log/linux_day205.log
 
# cron が動いているか
systemctl status cron     # Debian/Ubuntu
systemctl status crond    # CentOS/RHEL

1. crontab の基本

cron を 「使い始める」ための最初の3コマンド。これだけ覚えれば日次バッチ登録は可能です。

  • 何のコマンドか: crontab -e で自分のジョブを編集、crontab -l で確認、crontab -r で全削除(事故注意)
  • いつ使うか: 日次バックアップを登録、定期的なヘルスチェック、データ集計バッチの登録。サーバーに SSH してまず叩くコマンドのひとつ
  • 解決する具体的な問題: 「/etc/crontab を直接編集して文法エラーで cron が動かなくなる」事故を、crontab -e 経由の自動検証で予防。ユーザーごとの cron はその実行ユーザーで動くため権限分離も自然にできます
# 自分の crontab を編集
crontab -e
 
# 自分の crontab を表示
crontab -l
 
# 自分の crontab を削除(全部)
crontab -r        # 危険、ほぼ使わない
 
# 別ユーザーの crontab(root のみ)
sudo crontab -u myapp -l
sudo crontab -u myapp -e

cron の仕組み

  1. crond というデーモンが常駐している(systemd で管理)
  2. 毎分、各ユーザーの crontab を読みに行く
  3. 「現在時刻」と各行のスケジュールを照合
  4. 一致したら、そのユーザー権限でコマンドを fork して実行

crontab の実体は /var/spool/cron/crontabs/<username> (Debian) や /var/spool/cron/<username> (RHEL) に保存される。直接編集ではなく 必ず crontab -e 経由で(編集後に文法チェックされるため)。

システム全体の cron は /etc/crontab/etc/cron.d/ に置かれる。違いは:

配置フォーマット実行ユーザー
crontab -e(ユーザーごと)5列 + コマンドそのユーザー
/etc/crontab5列 + ユーザー名 + コマンド指定ユーザー
/etc/cron.d/*/etc/crontab と同形式指定ユーザー
/etc/cron.daily/*スクリプトを置くだけroot

2. crontab の文法

crontab の1行は 5つの時刻フィールド + コマンド という独特な書式です。慣れないと暗号のように見えますが、慣れれば一瞬で「いつ動くか」が読めるようになります。

  • 何の構文か: 分 時 日 月 曜日 コマンド の5列形式。*(全て)、*/N(N間隔)、1-5(範囲)、1,3,5(列挙)などの記法を組み合わせ
  • いつ使うか: 「毎日午前3時」「平日9-17時の30分おき」「毎月1日」など、定期実行の時刻設計
  • 解決する具体的な問題: 「*/5 * * * * と書いたつもりが 5 * * * * で毎時5分のみになっていた」のようなありがちな勘違いを、5フィールドの意味を正確に把握することで防ぎます
# ┌────── 分 (0-59)
# │ ┌──── 時 (0-23)
# │ │ ┌── 日 (1-31)
# │ │ │ ┌─ 月 (1-12)
# │ │ │ │ ┌ 曜日 (0-7, 0と7が日曜)
# │ │ │ │ │
#  *  *  *  *  * コマンド
 
# 毎分実行
* * * * * /opt/myapp/healthcheck.sh
 
# 毎時0分に実行
0 * * * * /opt/myapp/hourly.sh
 
# 毎日午前3時
0 3 * * * /opt/myapp/backup.sh
 
# 毎週月曜の午前9時
0 9 * * 1 /opt/myapp/weekly_report.sh
 
# 毎月1日 0時0分
0 0 1 * * /opt/myapp/monthly.sh
 
# 平日(月-金)の9-17時、30分おき
*/30 9-17 * * 1-5 /opt/myapp/business_hours.sh
 
# 5分おき
*/5 * * * * /opt/myapp/poll.sh

ざっくり言うと(5フィールドの覚え方)

cron の1行は 「いつ動くか」を5つの数字で表現 + コマンド。 左から「分・時・日・月・曜日」と覚える。短い周期から順に並ぶ

フィールドを縦に並べてみる

*  *  *  *  *  コマンド
│  │  │  │  │
│  │  │  │  └─ 曜日 (0-7, 0と7が日曜)
│  │  │  └──── 月 (1-12)
│  │  └─────── 日 (1-31)
│  └────────── 時 (0-23)
└───────────── 分 (0-59)

並びは「短い周期 → 長い周期」(分→時→日→月)+ おまけで曜日。これだけ覚える。

物語で覚える: 毎日午前3時

人間: 「毎日午前3時にバックアップ」
  ↓
分: 0時0分なので 0
時: 午前3時なので 3
日: 毎日なので *
月: 毎月なので *
曜日: 全曜日なので *
  ↓
0 3 * * * /opt/myapp/backup.sh

よく使うパターン

* * * * *      # 毎分
0 * * * *      # 毎時0分(毎時間)
0 3 * * *      # 毎日午前3時
0 9 * * 1      # 毎週月曜の9時
0 0 1 * *      # 毎月1日の0時
*/5 * * * *    # 5分おき
0 9-17 * * 1-5 # 平日9-17時の毎時0分

記法の対比表

記法意味
*全ての値* * * * * = 毎分
5単一の値5 * * * * = 毎時5分
1,15,30複数列挙毎時1分,15分,30分
1-5範囲毎時1〜5分
*/10N分おき*/10 * * * * = 10分おき
0-30/5範囲+ステップ0,5,10,15,20,25,30分

*/55 の罠」

これは新人が必ずやらかすミス:

*/5 * * * *    # 5分おき(00,05,10,15,20,25,30,...)
5 * * * *      # 毎時5分のみ(00:05, 01:05, 02:05, ...)
*/5 → 「5の倍数」= 5分おき
5   → 「ちょうど5分」= 毎時5分の1回だけ

一番覚えやすい説明

  • 5フィールドは 「分 時 日 月 曜日」(左から短い順)
  • * = 全部、5 = 5の時だけ、*/5 = 5おき
  • 自信無い時は https://crontab.guru で検証
  • @daily @hourly の特殊文字列で書ける時はそっちが読みやすい

cron 文法を読みやすくする特殊文字列

文字列意味
@rebootシステム起動時に1回
@yearly / @annually年1回 (0 0 1 1 *)
@monthly月1回 (0 0 1 * *)
@weekly週1回 (0 0 * * 0)
@daily / @midnight1日1回 (0 0 * * *)
@hourly1時間に1回 (0 * * * *)

@daily /opt/myapp/backup.sh0 0 * * * /opt/myapp/backup.sh より読みやすい。

cron 文法の検証ツール

自分の crontab 行が意図通りか不安な時:

  • https://crontab.guru/ : 文字列を入れると自然言語で説明 + 次回実行時刻を表示
  • crontab -l で表示して目視確認は最低限
  • 「毎週月曜午前9時」を 0 9 * * 10 9 * * 0 か迷ったら guru で検証

3. 「曜日と日」両方指定した時の罠

曜日と日を同時に指定すると OR になる

0 9 1 * 1

これは「毎月1日 9時」または「毎週月曜 9時」の両方 で発火する。AND ではない!

直感的には AND と思いがちなのが恐ろしい。「毎月1日が月曜日の時だけ」をやりたいなら、シェルで条件分岐するしかない:

0 9 1 * 1 [ "$(date +\%u)" = "1" ] && /opt/myapp/run.sh

でもこれは可読性が悪い。必要なら systemd timer の OnCalendar= を使う(後述、AND的に書ける)。


セッション②: cron の落とし穴(25-30分)

4. 罠その1: PATH が最小

「対話シェルで叩くと動くのに、cron に登録した瞬間に command not found になる」は cron 初学者の No.1 ハマりポイント です。

  • 何が起きるか: cron は最小の PATH=/usr/bin:/bin だけを持って起動する。/usr/local/bin や nvm/pyenv 配下のバイナリは見つからない
  • いつ気づくか: 登録後の初回実行でメールやログに command not found が出た時、または「なぜか何も実行されていない」状態で気づく
  • 解決する具体的な問題: PATH を crontab 先頭で明示、コマンドを絶対パスで書く、スクリプト内で PATH を export する、の3方針で「環境依存でジョブが動かない」を防ぎます
# 試しにこういう crontab を書く
crontab -e
* * * * * which node > /tmp/cron_path_test.log 2>&1

1分後:

cat /tmp/cron_path_test.log
# /usr/bin/which: no node in (/usr/bin:/bin)

cron の PATH は最小

対話シェルでは node が動くのに、cron で叩くと「command not found」。原因は cron が デフォルトで超最小の PATH しか持たないため。

典型的なデフォルト:

PATH=/usr/bin:/bin

nvm でインストールした node、/usr/local/bin の brew コマンド、/opt/myapp/ の自作バイナリ、全部見つけられない。

対策3つ

  1. crontab 先頭で PATH を明示
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/myapp
0 3 * * * /opt/myapp/backup.sh
  1. コマンドを絶対パスで書く
0 3 * * * /opt/myapp/bin/myapp-backup
  1. スクリプト先頭で PATH を export(推奨)
#!/usr/bin/env bash
export PATH=/usr/local/bin:/usr/bin:/bin
# 以降の処理

アンチパターン:環境依存のコマンドを絶対パスなしで叩く

# NG: cron では node が見つからない
0 3 * * * cd /opt/myapp && node script.js

正解:

# OK: nvm の node を絶対パスで
0 3 * * * cd /opt/myapp && /home/myapp/.nvm/versions/node/v20.0.0/bin/node script.js

または:

# OK: bash -lc でログインシェルとして起動(.bashrc 経由で PATH を引き継ぐ)
0 3 * * * /bin/bash -lc 'cd /opt/myapp && node script.js'

5. 罠その2: 標準出力・標準エラーがメールで届く(または埋もれる)

cron は コマンドの出力を黙ってメール送信 しようとします。MTA 未設定だとローカルメールに蓄積、設定済みだと毎分メール爆発、と両方が地獄。

  • 何が起きるか: cron で起動したコマンドが標準出力/標準エラーに何か出すと、デフォルトで crontab 所有者のメールアドレス(or ローカルメール)に送信される
  • いつ気づくか: /var/mail/<user> がパンパンになっている時、「cron 動いてるはずなのに気づかれない」サイレント失敗が3ヶ月続いた時
  • 解決する具体的な問題: > /dev/null 2>&1 で完全に黙らせる(運用前提を整えてから)、>> /var/log/myapp.log 2>&1 でログファイルに集約、MAILTO= で送信先を明示、の3パターンで意図した動作にします
* * * * * echo "hello from cron"

これを設定すると、デフォルトでは コマンドの標準出力が cron 経由でユーザーにメール送信される。MTA (postfix/sendmail) が無い環境では エラーログにだけ「メール送信失敗」が残る、というカオスな状態に。

cron が標準出力をどう扱うか

  1. コマンドが標準出力 or 標準エラーに何か出した
  2. cron はそれを集めて、MAILTO= で指定されたアドレスにメールで送る
  3. MAILTO= 未指定なら、crontab 所有者のローカルメール(/var/mail/<user>)に蓄積
  4. MTA が無いと送信失敗ログだけが /var/log/mail.err に残る

結果:

  • 「何故か /var/mail がパンパン」← ローカルメールに蓄積
  • 「ログを誰も読まない」← サイレントに失敗
  • 「メール飛びまくり」← 設定間違いでメール爆発

>/dev/null 2>&1 の正しい書き方

# メール送らない(出力は完全に捨てる)
0 3 * * * /opt/myapp/backup.sh > /dev/null 2>&1
 
# 標準出力だけ捨て、エラーはメールで通知
0 3 * * * /opt/myapp/backup.sh > /dev/null
 
# 全部ファイルに記録(ローテーション別途)
0 3 * * * /opt/myapp/backup.sh >> /var/log/myapp/backup.log 2>&1

2>&1 の意味:

  • 2> = 標準エラーをリダイレクト
  • &1 = 標準出力と同じ場所へ

順序が大事: >file 2>&1 は正しいが、2>&1 >file標準エラーがリダイレクト前の標準出力(端末)に行ってしまう。リダイレクトは左から評価される。

アンチパターン:エラーログを捨てる

# NG: エラーが起きても誰も気づかない
0 3 * * * /opt/myapp/backup.sh > /dev/null 2>&1

「うちのバックアップ動いてるよ」と思っていたら3ヶ月失敗していて、復旧不能。事故あるある。

正解パターン

  1. 専用ログにすべて記録 → logrotate でローテ + 監視
  2. 失敗時に Slack/PagerDuty に通知(後述の wrapper パターン)
  3. journald に集約(systemd timer 使用時)

6. 罠その3: 環境変数がほぼない

cron は .bashrc.zshrc も読まない非対話・非ログインシェル で起動します。普段使っている環境変数のほとんどが無い、まっさらな環境で動くと思ってください。

  • 何が起きるか: 対話シェルでは LANG=ja_JP.UTF-8 でも、cron では C ロケールで動いて Python/Node が文字化けや日付フォーマット異常を起こす
  • いつ気づくか: 「ローカルで動くのに cron だと出力が違う」「日本語ファイル名がバックアップで mojibake になっている」事象
  • 解決する具体的な問題: crontab 先頭で SHELL=/bin/bash PATH=... LANG=... MAILTO=... を明示する、スクリプト内で source /etc/myapp/env する、で「環境依存バグ」を再現可能な形に固定します
# 試してみる
crontab -e
* * * * * env > /tmp/cron_env.txt

1分待って:

cat /tmp/cron_env.txt

出てくるのはおそらく:

HOME=/home/yourname
LOGNAME=yourname
PATH=/usr/bin:/bin
LANG=en_US.UTF-8
SHELL=/bin/sh
PWD=/home/yourname

cron の環境変数は最小限

  • .bashrc / .zshrc は読まれない(非対話・非ログインシェル)
  • LANG / LC_ALL がデフォルトのままで、Python/Node のロケール処理がおかしくなる
  • SHELL=/bin/sh で動くため、bash 拡張機能が動かないことがある

対策

  • crontab 先頭で必要な環境変数を定義
  • スクリプト側で envファイル を読む(source /etc/myapp/env
  • シェルを明示的に指定(crontab 先頭で SHELL=/bin/bash
# crontab 先頭で環境を整える
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=ops@example.com
LANG=en_US.UTF-8
 
# それから実際のジョブ
0 3 * * * /opt/myapp/backup.sh >> /var/log/myapp/backup.log 2>&1

7. 失敗を「気づける」運用にする

cron は黙って失敗するのが既定。本番運用ではこれを必ず可視化する。

失敗通知付きラッパーパターン

#!/usr/bin/env bash
# /opt/myapp/bin/run-with-alert.sh
set -euo pipefail
 
JOB_NAME="$1"
shift
 
LOG="/var/log/myapp/${JOB_NAME}.log"
 
{
  echo "=== $(date -Iseconds) START $JOB_NAME ==="
  "$@"
  EXIT=$?
  echo "=== $(date -Iseconds) END   $JOB_NAME exit=$EXIT ==="
} >> "$LOG" 2>&1
 
if [ "$EXIT" -ne 0 ]; then
  # Slack 通知(簡易版)
  curl -s -X POST "$SLACK_WEBHOOK_URL" \
    -H 'Content-Type: application/json' \
    -d "{\"text\":\"cron job failed: $JOB_NAME exit=$EXIT\"}" \
    > /dev/null
fi
 
exit "$EXIT"
SLACK_WEBHOOK_URL=https://hooks.slack.com/...
0 3 * * * /opt/myapp/bin/run-with-alert.sh backup /opt/myapp/backup.sh

同様の発想で OSS の Healthchecks.ioDead Man’s Snitch を使うのが業界標準。「ジョブが正常完了したら https://hc-ping.com/UUID を叩く、叩かれなかったら異常」というモデルでサイレント失敗を検知できる。

8. 同時実行を防ぐ flock

重い処理が前回終わってないのに次が起動するのを防ぐ

# NG: 1分おきに重い処理が並行起動して負荷爆発
*/1 * * * * /opt/myapp/heavy.sh
 
# OK: flock で排他
*/1 * * * * flock -n /var/lock/myapp.lock /opt/myapp/heavy.sh

flock -n: 既にロックが取られていたら 即座に失敗 して起動しない。-w 30 で「30秒待ってもダメなら失敗」も可能。

内部的には /var/lock/myapp.lock に対する flock(2) システムコールを使う。同じファイルを指定する別プロセスは、最初のプロセスが終わるまで待機 or 失敗する。

9. cron を絶対書いてはいけないこと

セキュリティ:crontab に秘密情報を直書きしない

# NG
AWS_SECRET_ACCESS_KEY=AKIA...
0 3 * * * /opt/myapp/upload-to-s3.sh

なぜダメか:

  • crontab -l で表示される(同ユーザーでログインできる人に丸見え)
  • /var/spool/cron/crontabs/ のファイルとして残る(root から読める)
  • サーバーバックアップに乗って秘密情報が外に流出

対策

  • 環境変数ファイルを chmod 600 で配置:source /etc/myapp/env.secret
  • AWS なら IAM Role でインスタンスに権限を与え、キーを置かない
  • Vault / AWS Secrets Manager / GCP Secret Manager から実行時取得

セッション③: systemd timer との比較(10-15分)

10. systemd timer の基礎

cron のモダンな代替が systemd timer.timer unit と .service unit のペアで定期実行を実現します。冗長だがその分エラー追跡やリソース制限がしっかりできる、本番向きの選択肢。

  • 何の構文か: .service で何を実行するか、.timer でいつ実行するか、を2ファイルに分けて定義
  • いつ使う/選ぶか: 新規プロジェクトの定期実行、リソース制限が必要なバッチ、ジョブ失敗を systemctl status で即座に追いたい時、ログを journald に統合したい時
  • 解決する具体的な問題: cron の「サイレント失敗」「ログ分散」「環境変数最小」「同時実行制御なし」を、systemd の管理機能(Restart, journald, MemoryMax, RandomizedDelaySec, Persistent)で一気に解決します
# /etc/systemd/system/myapp-backup.service
[Unit]
Description=Backup myapp
 
[Service]
Type=oneshot
User=myapp
ExecStart=/opt/myapp/backup.sh
# /etc/systemd/system/myapp-backup.timer
[Unit]
Description=Run myapp backup daily at 03:00
 
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=10min
 
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now myapp-backup.timer
 
# タイマー一覧と次回実行時刻
systemctl list-timers

ざっくり言うと(cron vs systemd timer)

cron = 1975年から動いてる古典。シンプルだが罠多い。 systemd timer = モダンな代替。冗長だが本番運用しやすい。

新規プロジェクトなら systemd timer 推奨。既存が cron なら無理に移行しない。

まず cron の世界

crontab を1行書く
  ↓
crond が毎分チェック
  ↓
時刻が来たら fork してコマンド実行
  ↓
出力はメールで送る(MTA 無いと埋もれる)
  ↓
失敗してもサイレント
  ↓
ログは自前管理

シンプル。だが「失敗を気づける運用」を全部自分で組む必要がある。

systemd timer の世界

.timer ファイル + .service ファイルを2つ書く
  ↓
systemctl enable --now で登録
  ↓
時刻が来たら systemd が .service を実行
  ↓
出力は journald に統合
  ↓
失敗時は systemctl status で即可視化
  ↓
重複起動も自動で防止

冗長だが、systemd の機能が全部活きる(リソース制限、ログ統合、依存関係、再起動)。

対比表

観点cronsystemd timer
文法0 3 * * * 暗号的OnCalendar=*-*-* 03:00 明示的
ファイル数1行で済む.timer + .service の2つ
環境変数最小限(自分で設定)unit に明示
ログメール or 自前journald 統合
失敗の可視化自前で実装systemctl status で即見える
リソース制限できないMemoryMax= で可能
重複起動防止flock 必要デフォルトで安全
取り逃し補完できないPersistent=true
ジッタ(負荷分散)自前RandomizedDelaySec=
「第2月曜のみ」等不可(シェル分岐必要)OnCalendar=Mon *-*-08..14
学習コスト

「OnCalendar の凄さ」

cron では書けない条件が systemd timer なら書ける:

# 第2月曜日のみ(cron では絶対無理)
OnCalendar=Mon *-*-08..14 09:00:00
 
# 起動後10秒、その後5分おき
OnBootSec=10s
OnUnitActiveSec=5min
 
# ランダムに15分ずらして実行(複数サーバーの一斉実行を分散)
RandomizedDelaySec=15min

判断フロー

新規プロジェクトの定期実行を組む
  ↓
サーバーは systemd 環境か?
  YES ↓
    リソース制限、ログ統合が要る?
      YES → systemd timer
      NO  → 単純なら cron でも可
  NO  → cron 一択

既存システムが cron だらけ
  → 既存に合わせて cron(標準化が大事)

一番覚えやすい説明

  • cron = 軽量で古典、罠は自分でカバー
  • systemd timer = 重装備モダン、失敗追跡しやすい
  • 新規 → systemd timer
  • 既存環境に合わせる → cron

OnCalendar= の柔軟性

systemd timer は cron でできない指定が可能:

# 第2月曜日のみ(cron では不可能、シェル分岐必要)
OnCalendar=Mon *-*-08..14 09:00:00
 
# 月曜から金曜の毎時0,15,30,45分
OnCalendar=Mon..Fri *-*-* *:00,15,30,45:00
 
# 起動後10秒、その後5分おき
OnBootSec=10s
OnUnitActiveSec=5min

11. systemd timer で日次バックアップ実例

実務でよく使う「日次バックアップ + ジッタ + 取り逃し補完」を systemd timer で実装します。cron では実現困難な機能が標準で揃っているのを実感する例題。

  • 何を作るか: 毎日 03:00 にバックアップを実行する .service + .timer のセット、ジッタ15分、サーバー停止時の取り逃しは起動後に1回補完
  • いつ使うか: 日次/週次の定型バッチ、データベースのダンプ、ログのアーカイブ、SSL 証明書の更新確認
  • 解決する具体的な問題: 「複数サーバーが同じ時刻に一斉実行してストレージ I/O がスパイク」を RandomizedDelaySec=15min で分散、「夜間にサーバー停止していたらバックアップ取り逃し」を Persistent=true で起動後の補完実行で解決
# /etc/systemd/system/myapp-backup.service
[Unit]
Description=Daily backup for myapp
 
[Service]
Type=oneshot
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/env
ExecStart=/opt/myapp/bin/backup.sh
 
# セキュリティ
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ReadWritePaths=/var/backups/myapp
# /etc/systemd/system/myapp-backup.timer
[Unit]
Description=Daily backup at 03:00
 
[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=15min
Persistent=true
 
[Install]
WantedBy=timers.target

RandomizedDelaySec=15min重要。同じ時刻に多数のサーバーが一斉にバックアップ実行すると、ネットワークやストレージにスパイクが出る。ジッタを入れて分散させる。


練習課題

課題1: 「日次バックアップ」を cron で

# 簡単なバックアップスクリプト
cat > ~/learn/linux/day205/backup.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
 
BACKUP_DIR="$HOME/backups"
mkdir -p "$BACKUP_DIR"
 
DATE=$(date +%Y%m%d_%H%M%S)
ARCHIVE="$BACKUP_DIR/learn_$DATE.tar.gz"
 
tar -czf "$ARCHIVE" -C "$HOME" learn/
echo "$(date -Iseconds) backup created: $ARCHIVE"
 
# 7日以上前のバックアップを削除
find "$BACKUP_DIR" -name "learn_*.tar.gz" -mtime +7 -delete
EOF
 
chmod +x ~/learn/linux/day205/backup.sh
# crontab に登録(学習用に5分ごと実行)
crontab -e
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
*/5 * * * * /home/yourname/learn/linux/day205/backup.sh >> /tmp/backup.log 2>&1

5分待ってログ確認:

ls -lh ~/backups/
cat /tmp/backup.log

課題2: 同じバックアップを systemd timer で実装

[2-4_systemd|前章] の service / timer 形式で書き直し、両方の差を体感する。

課題3: 同時実行を flock で防ぐ

# 30秒かかるダミー処理
cat > ~/learn/linux/day205/slow.sh <<'EOF'
#!/usr/bin/env bash
echo "$(date -Iseconds) start" >> /tmp/slow.log
sleep 30
echo "$(date -Iseconds) end" >> /tmp/slow.log
EOF
chmod +x ~/learn/linux/day205/slow.sh
# 1分おきに起動するが、flock で重複させない
* * * * * flock -n /tmp/slow.lock /home/yourname/learn/linux/day205/slow.sh

/tmp/slow.log の start/end 時刻を見ると、重複起動が回避されていることが分かる。


締め: git で証跡を残す

exit
 
cd ~/learn/linux/day205
git init 2>/dev/null
git add .
git commit -m "feat(linux): cron と systemd timer で定期実行を実装"
 
# crontab の内容をリポジトリに残す(バージョン管理)
crontab -l > crontab.backup.txt
git add crontab.backup.txt
git commit -m "chore(linux): crontab のスナップショットを保存"

チェックリスト

  • crontab の5フィールド 分 時 日 月 曜日 を即答できる
  • */N 1-5 1,3,5 の記法を理解している
  • 「曜日と日」両方指定すると OR になることを知っている
  • cron の PATH がデフォルトで最小である理由と対策3つを答えられる
  • >/dev/null 2>&1 の意味と書く順序を説明できる
  • サイレント失敗を防ぐパターン(ラッパー、Healthchecks.io)を1つ実装できる
  • cron と systemd timer の使い分け基準を3つ挙げられる
  • flock を使った同時実行防止を書ける

詰まった時のチートシート

やりたいことコマンド
crontab 編集crontab -e
crontab 表示crontab -l
別ユーザーの crontabsudo crontab -u <user> -l
文法検証https://crontab.guru
cron が動いてるかsystemctl status cron
cron ログ確認journalctl -u cron または /var/log/syslog
標準出力捨てる> /dev/null 2>&1
ファイルに記録>> /var/log/myapp.log 2>&1
同時実行防止flock -n /tmp/lock.file <cmd>
systemd timer 一覧systemctl list-timers
timer 次回時刻systemctl list-timers --all

「実務OK」基準

  • 新しい定期ジョブを登録する時、cron か timer かを迷わず選べる
  • cron 由来の障害(PATH、文字化け、サイレント失敗)を1分で切り分けられる
  • 失敗を必ず可視化する仕組みを最初から組む: ログ、通知、ヘルスチェック
  • 重複起動を考慮できる: 重い処理には flock or systemd の自動排他

さらに深掘りするなら

  • man 5 crontab : フォーマット詳細
  • man systemd.timer / man systemd.time : timer の OnCalendar 完全リファレンス
  • man flock : 排他制御の詳細
  • Healthchecks.io ( https://healthchecks.io/ ) : OSS でセルフホスト可能なジョブ監視
  • 書籍『O’Reilly Linux サーバー Hacks』: cron の運用 Tips が多い
  • Stripe Engineering Blog の「Idempotent Cron Jobs」記事:冪等性の重要性

アンチパターン / 初心者やらかし事例

NG 1: 相対パスで動かない

* * * * * ./backup.sh   # `.` がどこかは cron 次第。動かない

→ 対策: 絶対パス: /home/user/scripts/backup.sh。スクリプト内も全部絶対で書く。

NG 2: PATH 最小で node python3 が見つからない

0 3 * * * node /app/cleanup.js   # /usr/local/bin/node が PATH に無い

→ 対策: crontab 先頭で PATH=/usr/local/bin:/usr/bin:/bin を宣言、または /usr/local/bin/node と絶対指定。

NG 3: エラー出力を /dev/null 2>&1 で完全に捨てる

0 3 * * * /app/backup.sh > /dev/null 2>&1   # 失敗しても誰も気付かない

→ 対策: stdout は捨てても stderr は残す: > /dev/null のみ。または失敗を Healthchecks.io / Slack に通知する。

NG 4: 順番ミス 2>&1 > log

0 3 * * * /app/backup.sh 2>&1 > /tmp/backup.log   # NG: stderr は画面のまま
0 3 * * * /app/backup.sh > /tmp/backup.log 2>&1   # OK

→ 対策: > file を先に書く。Day 5 で習った原則がここで再登場。

NG 5: 「毎月第1月曜」を 1 * * 1 で書こうとして失敗

0 9 1-7 * 1 /app/monthly.sh   # 「1-7日 OR 月曜」になる(OR)

→ 対策: スクリプト内で [[ $(date +%u) = 1 ]] || exit 0 のような 2段ガード を入れる。または systemd timer の OnCalendar=Mon *-*-1..7 を使う。


自己評価チェックリスト

  • crontab の5フィールドを覚えた
  • 「曜日と日が OR になる」罠を知った
  • PATH を crontab 先頭で宣言する書き方を覚えた
  • 失敗通知(Healthchecks.io か Slack か)を1つ実装した
  • flock -n で重複起動を防げる
  • cron と systemd timer の使い分けを言える

次のレッスン: Day 13 - ログ管理

明日は ログ運用 を体系的に扱う。

syslog / journald / logrotate / 構造化ログ / 本番でのログ設計。ここまで来ると Linux サーバー上で動くアプリの 観測性 (observability) が手に入る。

今日学んだ cron も、ログ集計のバッチを動かす土台として再登場する。

Day 13: ログ管理