コンテンツにスキップ
use-cases / crontab-per-branch / hero
CRON · GIT · ブランチごとのスケジュール

ブランチごとの crontab を、コードと一緒にデプロイ

`.hoody/crontab` をジョブの隣にチェックインします。デプロイスクリプトが `main`、`feature/billing-v2`、あるいは任意のプレビューブランチ用にコンテナを起動するとき、そのファイルを新しいコンテナの Cron API に PUT します。スケジュールはブランチとともに出荷され、ブランチが消えると一緒に消えます。

Cron API を読む
use-cases / crontab-per-branch / mechanism

`.hoody/crontab` がどのように本物のスケジュールになるか

すべてのブランチコンテナは Hoody Cron を実行します。デプロイスクリプトはチェックインされた crontab を読み、それを新しいコンテナの raw-crontab エンドポイントに PUT します。コンテナは、ファイルが記述するスケジュールを実行します — それ以上でも以下でもありません。

deploy.sh · CI から push
shell · クライアント
#!/bin/sh
# このブランチ用の新しいコンテナをプロビジョニングします。
BRANCH=$(git branch --show-current)
CTR=$(hoody containers create --from main-snapshot)

# コンテナの crontab をリポジトリ内のものに置き換えます。
curl -X PUT --data-binary @.hoody/crontab \
  -H "Content-Type: text/plain" \
  https://$CTR-cron-1.hoody.com/users/root/crontab

# 完了。ブランチのスケジュールはそのコンテナの中に存在します。
echo "deployed $BRANCH → $CTR"
PUT /users/root/crontab
cron · サーバー
# Hoody Cron raw-crontab エンドポイント — ファイル全体をアトミックに置き換えます。
PUT /users/root/crontab HTTP/1.1
Host: ctr_4d72b9-cron-1.hoody.com
Content-Type: text/plain

0 2 * * * /srv/jobs/billing-rollup-v2.sh
*/15 * * * * /srv/jobs/sync-stripe.py
@hourly curl -fsS http://localhost/healthz
*/5 * * * * /srv/jobs/diff-v1-v2.sh

HTTP/1.1 200 OK
# 200 OK: cron デーモンがリロードし、スケジュールは 1 秒以内に有効化されます。

crontab は、cron サーバーが記憶する状態ではなく、ブランチが運ぶデータです。コンテナを削除すれば、片付けるべきエントリは何も残りません — ファイルはディスクとともに消えました。

use-cases / crontab-per-branch / powers

やめられる 3 つのこと

スケジュールがリポジトリ内のファイルになると、3 つのカテゴリの作業が消えます。

バージョニング

スケジュールはコードと同じ diff に乗る

`billing-rollup.sh` を v2 に変更すると、新しいスケジュールも同じプルリクエストに乗ります。レビュアーはスクリプトのすぐ隣で cron 行を見ます。1 つのコミットを取り消せば、スケジュールも一緒に元に戻ります。

ティアダウン

ブランチを削除すれば cron も消える

ブランチコンテナはエフェメラルです。ブランチをマージまたはクローズすると、コンテナを破棄します。crontab はその中に存在していたため、スケジュールは清掃係なしで消えます — 古いエントリを抱えた共有 cron サーバーは存在しません。

アイソレーション

実験はステージングでは発火しない

`experiment/llm-rollups` の毎時実験ジョブは、自分のファイルシステムを持つ自分のコンテナで動きます。ステージングの cron デーモンには見えませんし、本番の cron デーモンにも見えません。ジョブ自体の中に `if BRANCH_ENV` のガードはありません。

use-cases / crontab-per-branch / compare

共有 crontab とブランチ crontab

標準の「ops が管理する 1 つの crontab」モデルとブランチに紐づくモデルは、反対方向に失敗します。同じジョブ、まったく異なる影響範囲です。

観点共有 CRONTABブランチ CRONTAB
信頼できる出典
ops の wiki + ansible ロールスケジュールはスクリプトとは別のリポジトリに存在
リポジトリ内の .hoody/crontabcron 行が呼び出すスクリプトのすぐ隣
ジョブの追加
コードをマージ → ops に連絡 → cron ホストに SSH2 つのシステムが手作業で一致する必要あり
ファイルを編集、push、デプロイ1 つの diff、1 つのマージ、1 つのデプロイ
ブランチ分離
if [ "$ENV" = staging ]; then …すべてのジョブがすべての環境を知っている
ブランチごとに 1 コンテナスクリプト内に env フラグなし
クリーンアップ
行を削除するのを覚えておく古いエントリが何年も積み上がる
ブランチ削除 = cron 削除ファイルシステムが消えれば、スケジュールも消える
実験
本番 crontab しか存在しないどのテストも本番で発火するリスクがある
スパイクブランチ = スパイク crontabそのコンテナでのみ発火

違いは機能ではありません — スケジュールがどこに存在するかです。ブランチが運ぶファイルか、ブランチが借りる共有テーブルの行か。

use-cases / crontab-per-branch / capacity

Hoody Cron が実際に提供するもの

コンテナごとの Cron は本物の REST サーフェスです — 3 つのエンドポイントファミリ、標準の cron 構文、ユーザーごとの完全な分離。数値は Cron API 仕様によるもので、捏造したベンチマークではありません。

  1. コンテナごとの CRONTAB1

    各ブランチコンテナは自分のユーザーごとの crontab を持ちます。ファイル全体を PUT し、GET で取り戻し、アトミックに置き換えます。裏に共有スケジュールテーブルはありません。

  2. エンドポイントファミリ3

    Raw crontab (GET/PUT)、マネージドエントリ (UUID と `expires_at` 付きの POST/PATCH/DELETE)、ユーザーごとのリスト。デプロイスクリプトが必要とするものを選んでください。

  3. フィールド CRON 表現5

    標準の `min hour day month dow` に加えてマクロ: `@hourly`、`@daily`、`@weekly`、`@monthly`、`@yearly`。`.hoody/crontab` がすでに使っているのと同じ構文です。

Hoody Cron API より: 各コンテナの cron サービス URL 上で GET/PUT /users/[user]/crontab および POST/PATCH/DELETE /users/[user]/entries。

use-cases / crontab-per-branch / punchline

スケジュールは、それを動かすコードの隣、同じコンテナの中、同じブランチに存在します。

before · 覚えておくべき 2 つのシステムafter · リポジトリ内の 1 つのファイル
以前の状態スケジュールは ops/ansible に · コードは app/ に · 一致しなかったPR をマージしてから、cron ホストを更新するチケットを起票
現在の状態PUT @.hoody/crontab → cron-1.[branch].hoody.com1 つの PUT、1 つのコンテナ、1 つのスケジュール、1 つのブランチ
Cron API を読む
use-cases / crontab-per-branch / replaces

これが置き換えるもの

cron スケジュールがかつて存在していた 6 つの場所、どれもコードの隣ではありませんでした。ブランチに紐づく crontab は、それらすべてを不要にします。

  • 共有本番 crontabすべてのチームが調整する必要があった、cron ホスト上の 1 つのファイル
  • 手動 cron 設定同期コードのマージとは別に適用される Ansible ロール / Puppet マニフェスト
  • main に固定された GitHub Actions スケジュールデフォルトブランチに紐づくスケジュール、フィーチャー作業やプレビューには見えない
  • 「マージしたら cron を更新するのを忘れずに」人間のチェックリスト項目 — あなたと古いエントリの間に立つ唯一のもの
  • 別の cron 設定リポジトリ実際のコードを持つリポジトリに遅れる以外に仕事がない 2 つ目のリポジトリ
  • Atlas/Liquibase のスケジュール済みマイグレーションスケジュールが存在する場所が他になかったため、マイグレーションツールがその役を担う
use-cases / crontab-per-branch / cta

システム間でスケジュールを同期するのをやめましょう。crontab をチェックインしてください。ブランチに運ばせましょう。

Cron ドキュメントを読む
use-cases / crontab-per-branch / related

他のユースケースを読む