前回の記事で「Apprise + 自作 Web サービスで OnCall 相当を組む」例を示しました。この記事ではよくある誤解を整理し、シフト管理を含めた自作 OnCall スタックの現実的な選択肢を深掘りします。

まずは Apprise の正しい位置付けを確認

Apprise は名前から「シフト管理ができそう」と誤解されがちですが、実際の役割は明確に分かれています。

正しい位置付け:

  • Apprise は 「通知の超便利ハブ」 — 1 つのコードで Slack / メール / SMS / LINE / Telegram など 100 種類以上の通知先に統一インタフェースで送る
  • シフト管理機能(カレンダー、ローテーション、当番判定)は持たない
  • 「シフト管理に Apprise を使う」とは、シフトロジックは別のライブラリ / DB / カレンダーで持ち、通知配信だけ Apprise に任せるという意味

つまり Apprise は「組んだシフトを確実に届ける道具」であり、「シフトを組む道具」ではありません。前回記事のコード例で get_policy_for_now() を Python で書いていたのは、まさにこの「シフト判定ロジックを自作」の実装です。

シフト管理を「自作する場合」に組み合わせる Python ライブラリ

シフトロジックを自分で書くなら、以下のライブラリが Apprise と相性が良い。

1. PyShift(point85/PyShift) — 古典的なシフトローテ

point85/PyShift は、Java 版の Shift ライブラリを Python に移植したもの。PyPI では PyWorkShift として配布されています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from PyShift.workschedule.work_schedule import WorkSchedule
from PyShift.workschedule.shift import Shift
from datetime import time, timedelta, date

# 8 時間 3 交代制
schedule = WorkSchedule("3 Shift Rotation", "Day-Swing-Night")
day_shift   = schedule.create_shift("Day",   "Day shift",   time(7, 0),  timedelta(hours=8))
swing_shift = schedule.create_shift("Swing", "Swing shift", time(15, 0), timedelta(hours=8))
night_shift = schedule.create_shift("Night", "Night shift", time(23, 0), timedelta(hours=8))

# ローテーション: 5 日勤 → 2 休 → 5 夕勤 → 2 休 → 5 夜勤 → 2 休
rotation = schedule.create_rotation("28-day cycle", "")
rotation.add_segment(day_shift,   5, 2)
rotation.add_segment(swing_shift, 5, 2)
rotation.add_segment(night_shift, 5, 2)

# A チームは 2026-01-01 開始でこのローテに従う
team_a = schedule.create_team("A team", "", rotation, date(2026, 1, 1))

# 「今日、A チームは何のシフト?」を判定
shift_today = team_a.get_shift_instance_for_day(date.today())
print(shift_today)

Apprise との連携イメージ:

1
2
3
4
5
6
7
import apprise
# 当番者を PyShift で判定
on_call_team = team_a if team_a.is_working(now) else team_b
# Apprise で通知
apobj = apprise.Apprise()
apobj.add(f"mailto://{on_call_team.email}")
apobj.notify(title="Alert", body="...")

工場・病院・ホテルなどの規則的なローテーションには強いが、IT のオンコールのように「毎週 1 人ずつ持ち回り」「祝日避けて再配置」といった柔軟性は弱め。

2. PuLP / Google OR-Tools — 最適化ベースのシフト生成

複雑な制約があるシフト割当(「夜勤明けの日勤禁止」「週 40 時間以内」「全員平等にカバー」)を最適化問題として解くライブラリ。

  • PuLP — 線形計画法、シンプル
  • Google OR-Tools — より高性能、CP-SAT ソルバ搭載

シフト表は 生成して DB / YAML に保存し、運用時はそれを読んで Apprise で通知、という構成が一般的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# OR-Tools の例(簡略化)
from ortools.sat.python import cp_model

model = cp_model.CpModel()
# 変数: shifts[(emp, day, shift_type)] = 0 or 1
# 制約: 1 日 1 シフトまで、夜勤明けは休、最低人数確保 ...
# 目的関数: 不公平最小化
solver = cp_model.CpSolver()
solver.Solve(model)
# 結果を schedule.yaml に出力

「シフト表を毎月自動生成 → DB に投入 → 運用中は Apprise で通知」が王道パターン。

3. Google Calendar + Python + Apprise(最も実用的)

実は最も手軽で広く使われているのはこの組み合わせ。「ガチガチのシフトソフト」より、Google Calendar を共有してそこで運用する方が圧倒的に現場で機能します。

「個別予定の共有」ではなく「1 つの共有カレンダー」が基本

よくある誤解として「担当者ごとに予定を作って共有するのか?」がありますが違います。「OnCall シフト」という 1 つの専用カレンダーを作り、シフトイベントを並べて、チーム全員に閲覧権限で共有するのが標準パターンです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[「Engineering OnCall」というカレンダー(1 つだけ)]
   ├─ 2026-05-08 09:00 〜 2026-05-15 09:00  「[Primary] 田中」
   ├─ 2026-05-15 09:00 〜 2026-05-22 09:00  「[Primary] 佐藤」
   ├─ 2026-05-22 09:00 〜 2026-05-29 09:00  「[Primary] 鈴木」
   ├─ 2026-05-08 09:00 〜 2026-05-15 09:00  「[Secondary] 山田」 ← 別イベント
   └─ ...

   ↓ チーム全員に「閲覧権限」で共有
   ↓ 編集権限は管理者のみ

[Web サービス(Apprise + 自作)]
   API で「今この時刻に該当するイベント」を取得
   → イベントから担当者メールを抽出
   → Apprise で通知
やり方特徴
個別の予定を各担当者と共有1 件ずつ招待を送る運用、シフト変更が面倒、全体像が見えない
1 つの共有カレンダーに当番イベントを並べるカレンダー全体を見れば全シフトが俯瞰、編集も 1 箇所

セットアップ手順

  1. 専用カレンダーを作成Engineering OnCall などの名前で、個人予定とは分離
  2. 権限を設定 — チーム全員は閲覧、シフト管理者のみ編集、API 連携用にサービスアカウントを読み取り権限で追加
  3. シフトイベントを作成 — タイトルや説明欄に担当者のメールアドレスを入れる
  4. 階層分けは「カレンダーを分ける」のが最も実装が楽OnCall-PrimaryOnCall-Secondary の 2 つに分けると API も読みやすい

「今の当番」を取得する Python コード

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from googleapiclient.discovery import build
from google.oauth2 import service_account
from datetime import datetime, timezone

# サービスアカウントで認証
creds = service_account.Credentials.from_service_account_file(
    "sa-key.json",
    scopes=["https://www.googleapis.com/auth/calendar.readonly"],
)
calendar = build("calendar", "v3", credentials=creds)

def get_current_oncall(calendar_id: str) -> str | None:
    """カレンダーから「今この時刻の当番者」のメールを返す"""
    now = datetime.now(timezone.utc)
    events = calendar.events().list(
        calendarId=calendar_id,
        timeMin=now.isoformat(),
        timeMax=now.isoformat(),  # 開始 ≤ now < 終了 が拾える
        singleEvents=True,
    ).execute()

    for event in events.get("items", []):
        summary = event.get("summary", "")
        if "@" in summary:
            return summary.split()[-1]  # "[Primary] tanaka@example.com" → "tanaka@example.com"
    return None

primary   = get_current_oncall("primary-oncall@group.calendar.google.com")
secondary = get_current_oncall("secondary-oncall@group.calendar.google.com")

# Apprise で通知
import apprise
apobj = apprise.Apprise()
apobj.add(f"mailto://{primary}")
apobj.notify(title="Alert", body="...")

メリット

  1. 担当者がカレンダーを「自分の予定」として確認できる — 「来週は当番だ」を Calendar アプリが通知してくれる
  2. シフト変更がドラッグ&ドロップ — 「来週入院するから誰か変わって」を 30 秒で対応
  3. 休暇・代理対応もカレンダーで普通に編集
  4. 過去ログが残る — 「あのインシデント時の当番は誰だったか」が後から正確にわかる
  5. モバイルで全員が見られる — Calendar アプリを入れればすぐ
  6. API 連携が簡単 — Google 公式
  7. 追加コストゼロ — Workspace に既に含まれている

落とし穴

  • タイトルから担当者を抽出するのは脆弱 — 表記ゆれで失敗する。説明欄や attendees フィールドに正規化して入れるか、Extended Properties を使うと堅牢
  • タイムゾーンの扱い — UTC と JST が混ざるので API では明示的に指定(datetime.now(timezone.utc)
  • 「終日」イベントは避ける — 時刻判定が曖昧になるので必ず時刻付きイベント
  • シフト境界の瞬間 — 月曜 09:00:00 ぴったりは前後どちらか、timeMin = now - 1秒 などで安全側に倒す
  • 「今の」イベントが複数返る — 階層別カレンダーで分けるか、優先度ルールを決める

代理対応のパターン

「田中さんが来週休みなので佐藤さんに代わる」場合:

  • カレンダーで田中のイベントを佐藤に書き換える — 最も簡単
  • 田中のイベントを「OOO」に変更し、佐藤の代理イベントを追加 — 履歴を残したい場合
  • OnCall-Override 専用カレンダーを別に持つ — そちらに該当者がいればそちらを優先

「予定をひとりずつに送る」のではなく、**「全員が見られる単一のシフト表 = カレンダー」**がポイントです。

4. Microsoft 365 / Teams 環境で同じ運用を行う

「Google Calendar 運用」を Microsoft 365 / Teams 環境でやりたい場合、完全に同じパターンが成立します。コンポーネントを差し替えるだけで思想は同じです。

コンポーネントの対応関係

役割Google WorkspaceMicrosoft 365 / Teams
シフト表Google Calendar 共有カレンダーOutlook/Exchange 共有カレンダー
APIGoogle Calendar APIMicrosoft Graph API(統一 API)
認証サービスアカウントAzure AD アプリ登録 + クライアント資格情報フロー(msal)
チャット通知Google Chat / SlackTeams Incoming Webhook
専用シフト管理アプリMicrosoft Shifts(Teams 内蔵、別パターン)

パターン A: Outlook 共有カレンダー(最も直接的な移植)

Google Calendar 版とほぼ同じ構造。

セットアップ:

  1. Outlook で「Engineering OnCall」共有カレンダーを作成 — Microsoft 365 グループに紐付けるのが一般的
  2. チームに閲覧権限で共有
  3. Azure AD でアプリ登録Calendars.Read.Shared または Calendars.ReadApplication permission を付与
  4. 管理者同意(admin consent) を取得 — テナント全体への読み取り権限

Microsoft Graph API での実装:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from msal import ConfidentialClientApplication
import requests
from datetime import datetime, timezone, timedelta
import apprise

TENANT_ID     = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
CLIENT_ID     = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
CLIENT_SECRET = "zzzzzzzzzzzzzzzzzzzzzzzz"
GROUP_ID      = "engineering-oncall@example.onmicrosoft.com"

# クライアント資格情報フローでトークン取得
app = ConfidentialClientApplication(
    CLIENT_ID,
    authority=f"https://login.microsoftonline.com/{TENANT_ID}",
    client_credential=CLIENT_SECRET,
)
result = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
token = result["access_token"]

def get_current_oncall(group_email: str) -> str | None:
    """グループカレンダーから「今の当番」のメールを返す"""
    now = datetime.now(timezone.utc)
    end = now + timedelta(seconds=1)
    url = (
        f"https://graph.microsoft.com/v1.0/groups/{group_email}/calendarView"
        f"?startDateTime={now.isoformat()}&endDateTime={end.isoformat()}"
    )
    r = requests.get(url, headers={"Authorization": f"Bearer {token}"})
    r.raise_for_status()
    for event in r.json().get("value", []):
        subject = event.get("subject", "")
        if "@" in subject:
            return subject.split()[-1]
        for a in event.get("attendees", []):
            return a["emailAddress"]["address"]
    return None

# Apprise で通知(メール + Teams 同時送信)
oncall = get_current_oncall(GROUP_ID)
apobj = apprise.Apprise()
apobj.add(f"mailto://{oncall}")
apobj.add("msteams://TOKEN_A/TOKEN_B/TOKEN_C/")  # Teams Incoming Webhook
apobj.notify(title="Alert", body=f"On-call: {oncall}")

パターン B: Microsoft Shifts(Teams 内蔵のシフト管理)

Teams には Shifts というシフト管理専用アプリが標準で組み込まれています。

特徴:

  • Teams の左サイドバーに常駐 — 担当者は Teams を開くたびに自分のシフトが見える
  • シフト交換・申請・承認のワークフロー内蔵
  • タイムクロック機能(出退勤打刻)
  • Power Automate 連携可能
  • Microsoft Graph API(/teams/{id}/schedule でシフトデータ取得可能
1
2
3
4
5
6
7
8
url = f"https://graph.microsoft.com/v1.0/teams/{TEAM_ID}/schedule/shifts"
r = requests.get(url, headers={"Authorization": f"Bearer {token}"})
shifts = r.json()["value"]
current = next(
    s for s in shifts
    if s["sharedShift"]["startDateTime"] <= now.isoformat() <= s["sharedShift"]["endDateTime"]
)
user_id = current["userId"]

要求権限: Schedule.Read.All(管理者同意必要)

A vs B の使い分け

用途Outlook 共有カレンダー(A)Microsoft Shifts(B)
シフト表の編集 UIカレンダーアプリ専用 UI(直感的)
シフト交換ワークフローなし(手動編集)あり(依頼・承認)
タイムクロックなしあり
API の単純さ簡単(Calendar API)やや複雑(Schedule API)
適合用途IT オンコール、軽量運用フロントライン勤務(小売・医療等)

IT のオンコール用途なら Outlook 共有カレンダー(A)で十分。交代制勤務の本格運用なら Shifts(B)。

Apprise の Teams 通知連携

1
apobj.add("msteams://TokenA/TokenB/TokenC/")

Teams チャネルの設定で Incoming Webhook コネクタを有効化し、生成された URL の token 部分を Apprise URL に変換するだけ。

⚠️ 注意: Microsoft は Connector ベースの Incoming Webhook を Power Automate Workflow に段階的に移行しています。最新環境では Workflow 用 Webhook を使う必要がある場合あり。Apprise 側も msteamswrapper プラグインで対応中。

Azure AD アプリ登録時の落とし穴

  • Application permission を選ぶ(Delegated ではない) — サービスとして動かすため
  • 必要スコープ: Calendars.Read.Shared(A)or Schedule.Read.All(B)
  • 管理者同意(admin consent)が必須 — テナント管理者にお願いして承認
  • Client secret は Azure Key Vault などで管理、コード直書きは厳禁
  • タイムゾーンPrefer: outlook.timezone="Tokyo Standard Time" ヘッダで指定可能
  • Microsoft Graph はレート制限が厳しめ — 429 Too Many Requests のリトライ実装必須

選択指針

状況推奨
Google Workspace 中心Google Calendar + Calendar API + Apprise
Microsoft 365 / Teams 中心Outlook 共有カレンダー + Graph API + Apprise(msteams 通知)
シフト交換ワークフローも本格運用したいMicrosoft Shifts + Graph API + Apprise
シフト管理 UI を Teams に統合したいChannel Calendar アプリ(バックエンドは Outlook と同じ)+ Graph API

エンタープライズで M365 を既に契約している場合、追加コストゼロでこの構成が組めるのが大きな利点です。

シフト管理 + 通知が「最初から統合された」OSS

「自作ではなく既製品で」という選択肢:

GoAlert(Target 社 OSS)— OnCall OSS アーカイブ後の有力代替

GoAlert(Apache 2.0)は、Target 社が OSS 公開しているオンコール管理ツール。Grafana OnCall OSS のアーカイブを受けて注目度が急上昇しています。

機能:

  • ブラウザ UI でドラッグ&ドロップのシフト編集
  • エスカレーションポリシー(一定時間応答なしで次の人へ)
  • ローテーション + Override(一時的な交代)
  • API + GraphQL での連携

通知手段:

  • Twilio で SMS / 音声通話(唯一サポートされているプロバイダ
  • SMTP でメール
  • Slack チャンネル統合

GoAlert は Twilio + SMTP + Slack を直接統合する設計で、Apprise は使っていません。Apprise の 100 種類超の通知先を活かしたい場合は、別途自作ハブを挟むか、Apprise の Webhook を GoAlert の通知ターゲットに据える形になります。

GoAlert の標準範囲で足りる組織なら、自作よりずっと楽。

その他の OSS

  • Alerta — アラート集約 + ack / シェルブ機能、シフト管理は弱い
  • OneUptime — オンコール + インシデント + ステータスページの全部入り OSS
  • Karma — Alertmanager 専用 UI、通知ではなく可視化

自作スタックの完成形

前回記事の Apprise + FastAPI 自作サービスに、シフト管理を組み込んだ完成形:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[Grafana Alerting]
      ↓ webhook
[自作 Web サービス(FastAPI)]
   ┌─ 当番判定: Google Calendar API or PyShift で「今の担当者は誰か」
   ├─ ack URL 発行
   └─ Apprise で通知(メール + Slack + Pushover)
   N 分タイムアウトでエスカレーション
[次の当番者](同様にカレンダーから取得)

実装の最小増分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# ESCALATION_POLICY を時間帯別に動的取得
def get_escalation_policy_now():
    now = datetime.now()
    # Google Calendar から取得 or PyShift で計算
    primary = get_oncall_from_calendar(now)
    secondary = get_oncall_from_calendar(now + timedelta(hours=1))
    manager = "manager@example.com"
    return [
        f"mailto://{primary}",
        f"mailto://{secondary}",
        f"mailto://{manager}",
    ]

これで「カレンダーで誰でも編集できるシフト表 + Apprise の通知柔軟性 + 自作の細かい制御」を全部手に入れられます。

規模別の推奨構成(再整理)

規模推奨構成
個人 / 1〜3 人体制自作 FastAPI + Apprise + Google Calendar(シフト表として)
5〜10 人、IT オンコール中心GoAlert + Twilio + Slack(自前運用が前提)
5〜10 人、シフト規則が複雑OR-Tools でシフト生成 + 自作 + Apprise
10 人超、24/7 + 音声通話必須Zenduty / OnPage / PagerDuty SaaS
エンタープライズ + Grafana スタックGrafana Cloud IRM

Apprise はシフト管理しないこと、Grafana OnCall や GoAlert は Apprise ではなく独自の通知統合で動いていることを理解した上で、自分の環境に合うレイヤーを選ぶのが正解です。

まとめ

  • Apprise = 通知ハブ。シフト管理機能はない
  • シフト管理を自作するなら: PyShift(規則的)、OR-Tools(最適化)、Google Calendar / Outlook 共有カレンダー(実用最強)
  • 既製品で済ませるなら: GoAlert(OSS)、または有料 SaaS(Zenduty / IRM / PagerDuty)
  • Microsoft 365 環境なら Outlook 共有カレンダー + Graph API で同じ思想がそのまま動く

OnCall OSS 後の OSS 自作の現実解として、Apprise + 共有カレンダー + 自作 Web サービスが最もシンプルかつ実用的、というのが筆者の結論です。

参考リンク