2-5. cron - 定期実行とその落とし穴
所要時間: 30-50分(がっつりなら2セッション分) ゴール: crontab を読み書きでき、cron 特有の罠(PATH最小、メール挙動、エラー埋没)を回避できる コミット内容: 自作 cron スクリプトと検証ログを
~/learn/linux/day205/に保存
この章が終わるとできること
- crontab の5フィールド
分 時 日 月 曜日を即読み書きできる */51-51,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/RHEL1. 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 -ecron の仕組み
crondというデーモンが常駐している(systemd で管理)- 毎分、各ユーザーの crontab を読みに行く
- 「現在時刻」と各行のスケジュールを照合
- 一致したら、そのユーザー権限でコマンドを 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分 |
*/10 | N分おき | */10 * * * * = 10分おき |
0-30/5 | 範囲+ステップ | 0,5,10,15,20,25,30分 |
「*/5 と 5 の罠」
これは新人が必ずやらかすミス:
*/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.shは0 0 * * * /opt/myapp/backup.shより読みやすい。
cron 文法の検証ツール
自分の crontab 行が意図通りか不安な時:
- https://crontab.guru/ : 文字列を入れると自然言語で説明 + 次回実行時刻を表示
crontab -lで表示して目視確認は最低限- 「毎週月曜午前9時」を
0 9 * * 1か0 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>&11分後:
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:/binnvm でインストールした node、
/usr/local/binの brew コマンド、/opt/myapp/の自作バイナリ、全部見つけられない。対策3つ
- crontab 先頭で PATH を明示
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/myapp 0 3 * * * /opt/myapp/backup.sh
- コマンドを絶対パスで書く
0 3 * * * /opt/myapp/bin/myapp-backup
- スクリプト先頭で 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 が標準出力をどう扱うか
- コマンドが標準出力 or 標準エラーに何か出した
- cron はそれを集めて、
MAILTO=で指定されたアドレスにメールで送るMAILTO=未指定なら、crontab 所有者のローカルメール(/var/mail/<user>)に蓄積- 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ヶ月失敗していて、復旧不能。事故あるある。
正解パターン
- 専用ログにすべて記録 → logrotate でローテ + 監視
- 失敗時に Slack/PagerDuty に通知(後述の wrapper パターン)
- journald に集約(systemd timer 使用時)
6. 罠その3: 環境変数がほぼない
cron は .bashrc も .zshrc も読まない非対話・非ログインシェル で起動します。普段使っている環境変数のほとんどが無い、まっさらな環境で動くと思ってください。
- 何が起きるか: 対話シェルでは
LANG=ja_JP.UTF-8でも、cron では C ロケールで動いて Python/Node が文字化けや日付フォーマット異常を起こす - いつ気づくか: 「ローカルで動くのに cron だと出力が違う」「日本語ファイル名がバックアップで mojibake になっている」事象
- 解決する具体的な問題: crontab 先頭で
SHELL=/bin/bashPATH=...LANG=...MAILTO=...を明示する、スクリプト内でsource /etc/myapp/envする、で「環境依存バグ」を再現可能な形に固定します
# 試してみる
crontab -e* * * * * env > /tmp/cron_env.txt1分待って:
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>&17. 失敗を「気づける」運用にする
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.io や Dead 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.targetsudo 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 の機能が全部活きる(リソース制限、ログ統合、依存関係、再起動)。
対比表
| 観点 | cron | systemd 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.targetRandomizedDelaySec=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 -eSHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
*/5 * * * * /home/yourname/learn/linux/day205/backup.sh >> /tmp/backup.log 2>&15分待ってログ確認:
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フィールド
分 時 日 月 曜日を即答できる -
*/N1-51,3,5の記法を理解している - 「曜日と日」両方指定すると OR になることを知っている
- cron の PATH がデフォルトで最小である理由と対策3つを答えられる
-
>/dev/null 2>&1の意味と書く順序を説明できる - サイレント失敗を防ぐパターン(ラッパー、Healthchecks.io)を1つ実装できる
- cron と systemd timer の使い分け基準を3つ挙げられる
- flock を使った同時実行防止を書ける
詰まった時のチートシート
| やりたいこと | コマンド |
|---|---|
| crontab 編集 | crontab -e |
| crontab 表示 | crontab -l |
| 別ユーザーの crontab | sudo 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: ログ管理 へ