26.

Django のバッチを cron で実行する|管理コマンド・絶対パス・flock

編集
この記事の要点
  • Django のバッチは カスタム管理コマンドmanage.py コマンド名)にして cron から呼ぶ
  • cron には PATH も仮想環境も無いpythonmanage.py絶対パスで書く
  • DJANGO_SETTINGS_MODULE が要る場合は cron 側で環境変数を渡す
  • 出力は必ずファイルへ。>> log 2>&1 を付けないと失敗しても気づけない
  • 多重起動を防ぐため flock をかける。処理が長引いても重ならない

1. バッチ本体を管理コマンドにする

スクリプトを単独の .py にすると Django の設定読み込みを自分で書く必要があります。管理コマンドにすれば manage.py が全部やってくれます。

myapp/
  management/
    __init__.py
    commands/
      __init__.py
      cleanup_sessions.py
# myapp/management/commands/cleanup_sessions.py
import logging
from django.core.management.base import BaseCommand
from django.utils import timezone
from myapp.models import Session

logger = logging.getLogger(__name__)

class Command(BaseCommand):
    help = "期限切れのセッションを削除する"

    def add_arguments(self, parser):
        parser.add_argument("--days", type=int, default=30)
        parser.add_argument("--dry-run", action="store_true")

    def handle(self, *args, **options):
        limit = timezone.now() - timezone.timedelta(days=options["days"])
        qs = Session.objects.filter(expired_at__lt=limit)
        count = qs.count()

        if options["dry_run"]:
            self.stdout.write(f"[DRY] {count} 件が対象です")
            return

        qs.delete()
        logger.info("期限切れセッションを %d 件削除しました", count)
        self.stdout.write(self.style.SUCCESS(f"{count} 件削除しました"))
python manage.py cleanup_sessions --days=60 --dry-run   # まず動作確認

作り方の詳細は cron用コマンド・ファイルの作成 を参照してください。ここからは cron 側の設定を扱います。

2. crontab の書式

┌── 分   (0-59)
│ ┌── 時   (0-23)
│ │ ┌── 日   (1-31)
│ │ │ ┌── 月   (1-12)
│ │ │ │ ┌── 曜日 (0-7  0と7が日曜)
│ │ │ │ │
* * * * * 実行するコマンド
書き方意味
0 3 * * *毎日 3:00
*/10 * * * *10 分おき
0 9 * * 1-5平日の 9:00
0 0 1 * *毎月 1 日の 0:00
0 2 * * 0毎週日曜の 2:00

3. 登録する

crontab -e          # 編集
crontab -l          # 一覧
crontab -u www-data -l   # 別ユーザーの分を見る
SHELL=/bin/bash
MAILTO=""
DJANGO_SETTINGS_MODULE=config.settings.production

# 毎日 3:00 に期限切れセッションを削除
0 3 * * * cd /srv/myproject && /srv/myproject/.venv/bin/python manage.py cleanup_sessions --days=30 >> /var/log/myproject/cleanup.log 2>&1

ポイントは 3 つです。

  • 仮想環境の python を絶対パスで指定source activate は要りません。.venv/bin/python を直接呼べば、その環境のライブラリが使われます
  • cd でプロジェクトルートへ移動manage.py と相対パスの設定ファイルを見つけるため
  • 出力をログへリダイレクト2>&1 を付けてエラーも同じファイルに落とす

4. 動かないときに見るところ

症状原因
何も起きないcron の PATH は /usr/bin:/bin だけ。すべて絶対パスで書く
ModuleNotFoundError: No module named 'django'システムの python が使われている。.venv/bin/python を指定する
ImproperlyConfigured: settings are not configuredDJANGO_SETTINGS_MODULE が渡っていない
.env の値が読めないcron はログインシェルを通らない。python-dotenv で明示的に読む
ログが空書き込み先の権限。cron を動かすユーザーで書けるか確認する
時刻がずれるサーバーのタイムゾーンは UTC のことがある。timedatectl で確認
# cron が起動したかどうかはシステムログで分かる
sudo grep CRON /var/log/syslog | tail -20      # Debian / Ubuntu
sudo journalctl -u cron --since "1 hour ago"

# 環境の違いを再現して手で試す
env -i /srv/myproject/.venv/bin/python /srv/myproject/manage.py cleanup_sessions

最後のコマンドは環境変数を空にして実行します。手元で動くのに cron で失敗する場合、これで同じ失敗を再現できます。

5. 多重起動を防ぐ

*/5 * * * * /usr/bin/flock -n /tmp/import.lock -c "cd /srv/myproject && /srv/myproject/.venv/bin/python manage.py import_data" >> /var/log/myproject/import.log 2>&1

5 分おきの処理が 6 分かかると、前回が終わる前に次が始まります。flock -n を付けるとロックが取れないときは何もせず終了するため、二重実行によるデータ破壊を防げます。

6. systemd timer という選択肢

# /etc/systemd/system/cleanup.service
[Unit]
Description=Django cleanup batch

[Service]
Type=oneshot
User=www-data
WorkingDirectory=/srv/myproject
Environment=DJANGO_SETTINGS_MODULE=config.settings.production
ExecStart=/srv/myproject/.venv/bin/python manage.py cleanup_sessions

# /etc/systemd/system/cleanup.timer
[Unit]
Description=Run cleanup daily

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now cleanup.timer
systemctl list-timers cleanup.timer      # 次回実行時刻を確認
journalctl -u cleanup.service -n 50      # ログはこちらに出る

cron に比べてログが journald に集約され、実行結果とかかった時間が残るのが利点です。Persistent=true を付けると、サーバーが停止していて実行できなかった分を起動後に補います。

関連

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 環境構築とプロジェクト/アプリの作成
  2. MVC(MVT)のそれぞれの使い方と説明
  3. データベースへの接続と操作
  4. Django Administration
  5. git管理
  6. エラー一覧
  7. バージョンの確認方法
  8. ログ出力方法
  9. SQLのログ出力方法
  10. ログのローテート設定
  11. settings.pyの定数にアクセスする方法
  12. 本番環境へのインストールとアプリのデプロイ(apache編)
  13. 本番環境へのインストールとアプリのデプロイ(nginx編)
  14. djangoアプリの本番の開始URLを変更する
  15. 静的(static)ファイルの置き場所と読み込み(画像、css、js )
  16. CSRFトークンをAjaxで使用する方法
  17. ajaxの使用例(POST編)
  18. ファイルのアップロードとファイルの名前
  19. クイックスタート/チュートリアル
  20. ログイン機能
  21. テンプレート側のログイン判定
  22. ビュー側のログイン判定
  23. 管理者ユーザーの作成/判定と管理画面
  24. モデルのjson化とレスポンス
  25. runserverでポートを指定する方法
  26. cronによるバッチ実行
  27. テンプレートで利用する共通のcontextを定義する方法
  28. プログラムが本番サーバーで反映されない場合の対処法
  29. APIの作成
  30. cron用コマンド・ファイルの作成

最近更新/作成されたページ