やってみた 2026年7月12日

Pythonスクリプトに@flowを付けるだけ、のPrefectを運用目線で動かしてみた

XECIN PythonワークフローオーケストレーションPrefect運用

バッチの管理は、いつまで経っても悩みの種です。

cronで動かしている集計スクリプトがある日こけていて、気づいたのは翌朝、というのを私は何度か経験しています。ログはサーバのどこかにあるはずなんですが、いざ探すと見つからない。リトライは自前でtry/exceptを書いていて、しかも中途半端。運用を考えると、この「見えなさ」が一番つらいところなんですよね。

GitHubのトレンドに Prefect が上がっていて、「Pythonスクリプトをデコレータ2つで本番ワークフローに昇格させる」という触れ込みが目に留まりました。ちょうど上に書いたような課題感があったので、使い捨てのコンテナで実際に触ってみることにしました。試したのは prefect v3.7.8(git commit b75c0606、2026-07-09ビルド)、Python 3.12.13 のクリーンな python:3.12-bookworm コンテナ上です。

pip install 一発、ただし依存はそれなりに

導入は拍子抜けするほど簡単でした。

pip install -U prefect
# Successfully installed ... prefect-3.7.8 ...
# 実測 16秒

16秒で終わったのは速いんですが、入ってくるものを眺めると fastapi / uvicorn / sqlalchemy / alembic / aiosqlite / pydantic あたりが一式ついてきます。つまりPrefectは「ライブラリ」というより、APIサーバとDBマイグレーションまで内包した小さなプラットフォームなんですね。この時点で prefect version を見ると、そのあたりの構成がはっきり出ます。

Version:              3.7.8
Server type:          ephemeral
Profile:              ephemeral
Server:
  Database:           sqlite
  SQLite version:     3.40.1

デフォルトのバックエンドがSQLiteで、しかも “ephemeral”(一時的)という言葉が並んでいます。ここが後で効いてきます。

デコレータを付けるだけ、は本当だった

READMEの例に倣って、自分の現場で一番ありがちな「複数ソースを順に取ってきて集計する」処理を書いてみました。ポイントは、失敗しやすい取得処理に retries=3 を付けたところです。

from prefect import flow, task

@task(log_prints=True, retries=3, retry_delay_seconds=1)
def fetch(source: str) -> list[int]:
    # 上流が不安定な想定。失敗するとPrefectが自動でリトライする
    ...

@flow(name="daily-etl", log_prints=True)
def daily_etl(sources: list[str]):
    for s in sources:
        load(transform(fetch(s)))

if __name__ == "__main__":
    daily_etl(["orders-db", "events-api"])

既存の関数に @task@flow を付けて、あとは普通に呼ぶだけ。ここは本当にドキュメント通りでした。

で、python etl_flow.py と叩いて最初に「おっ」と思ったのが、これです。

12:07:12 | INFO | prefect - Starting temporary server on http://127.0.0.1:8331
12:07:18 | INFO | Flow run 'gorgeous-roadrunner' - Beginning flow run ...

サーバを別途立てていないのに、実行のたびにPrefectが一時サーバをランダムポートで勝手に起動します。先ほどの “ephemeral” はこれのことでした。おかげでゼロ設定で動くのはありがたいのですが、この起動に毎回6秒ほど持っていかれます。ワンショットの軽いスクリプトを頻繁に回す用途だと、この初期化コストは頭に入れておいたほうがいいですね。

リトライは「タスク単位」で、尽きたらフローごと落ちる

一番確かめたかったリトライの挙動です。取得処理をわざと2回失敗させてから成功させると、ログにこう出ました。

Task run 'fetch-e69' - ... RuntimeError('transient error talking to events-api (attempt 1)') - Retry 1/3 will start 1 second(s) from now
Task run 'fetch-e69' - ... (attempt 2) - Retry 2/3 will start 1 second(s) from now
Task run 'fetch-e69' - fetched 7 rows from events-api on attempt 3
Task run 'fetch-e69' - Finished in state Completed()
Flow run 'responsible-fulmar' - Finished in state Completed()

自前のtry/exceptを一切書かずに、3回まで1秒間隔でリトライして、3回目で回復してくれました。これは素直に嬉しい。

ただ、ここで一つ現実的な落とし穴に当たりました。別のパターンとして、取得が3回とも失敗する(=リトライを使い切る)ケースを流すと、そのタスクが Failed になった瞬間にフロー全体が例外を再送出して落ち、プロセスの終了コードも 1 になります。

Task run 'fetch-ddb' - ... - Retries are exhausted
Flow run 'gorgeous-roadrunner' - Finished in state Failed(...)
# FLOW_EXIT=1

つまり「orders-dbの処理は成功していたのに、後続のevents-dbがリトライ尽きで落ちると、そこでフローが止まって残りの後片付けは走らない」わけです。当たり前と言えば当たり前なんですが、運用を考えると「1ソース失敗しても他は完走させたい」ケースは多い。その場合はタスク側で例外を握って結果オブジェクトに落とすか、return_state=True で状態を受け取って自分で分岐する、といった設計が要ります。ここは好みが分かれるところですが、「リトライを付けた=勝手に良い感じに部分成功してくれる」ではない、という点は最初に押さえておきたいところです。

UIは無料で自前ホストできる、が接続先で一度つまずいた

Prefectの魅力は、この実行結果がすべてダッシュボードで追えることです。prefect server start でサーバを立てると、Web UIが付いてきます。cronのログ探し行脚と比べると、これがあるだけで運用の見通しがまったく違います。

一時サーバではなく常設サーバを立てて、そこへ向けてフローを何本か流すと、UIに実行履歴が残りました。

Prefect UIのRuns画面。3件のフロー実行が状態別に並び、1件がFailed(赤)、2件がCompleted(緑)で表示されている

状態(Completed / Failed)、所要時間、タスク実行数が一覧で見えます。個別の実行を開くと、タスクの依存関係がラングラフとして描画され、ログもそのまま追えます。下の画面では、リトライが入った fetch タスクのバーだけが横に長く伸びていて、「ここで詰まって回復した」のが視覚的に分かります。

フロー実行の詳細画面。ラングラフとログに、fetchタスクがattempt1→2で失敗しattempt3で成功したリトライの過程が表示されている

ただ、このUIを別ホスト(今回はコンテナのポートを転送して手元のブラウザから見る構成)から開いたとき、最初は画面が真っ白で「Can’t connect to Server API at http://127.0.0.1:4200/api」というトーストが出ました。公式の手順どおり --host 0.0.0.0 で公開してもこれが出る。

原因は、UIのフロントエンドが叩きにいくAPIのURLが、既定でサーバ自身の 127.0.0.1:4200 を指していたことでした。ブラウザから見た 127.0.0.1 はブラウザ側マシンのローカルホストなので、当然サーバには届きません。サーバ起動時に PREFECT_UI_API_URL を、実際にブラウザがアクセスできる公開ポートのURLに合わせてやると、素直に表示されました。

PREFECT_UI_API_URL=http://127.0.0.1:<公開ポート>/api prefect server start --host 0.0.0.0

手元マシンで localhost:4200 をそのまま開く分には起きない話なんですが、コンテナ越しやリモートで見る構成だと踏みやすいので、書き残しておきます。

運用に載せる前の所感

半日触った範囲での、私の見立てです。

  • 導入の軽さは本物。既存のPython関数にデコレータを足すだけで、リトライ・ログ集約・実行履歴が付いてくる。cronで散らかっていた運用の可視化には効きそうです。
  • ephemeralサーバの起動コスト(毎回6秒程度) は、短命スクリプトを高頻度で回す用途だと無視できない。常設サーバに向けるか、実行形態を見直す判断が要ります。
  • 部分成功の設計は自分で持つ必要がある。リトライ尽き=フロー停止なので、「他は完走させたい」なら明示的に組む。
  • コストの観点では、UIまで含めて自前ホスト(SQLite/PostgreSQL)できるのが大きい。マネージドのPrefect Cloudを使うか、サーバ1台を自分で抱えるかは、チーム規模と「バックエンドDBの面倒を見られるか」で決まるところだと考えています。小さく始めるならSQLiteの一時運用で十分検証できました。

まずは今こけているcronバッチを1本、@flow でくるんでUIに載せてみるところから始めるつもりです。もっと筋の良い部分成功の書き方があれば、教えてもらえると嬉しいです。