Temporal Workflowとは?コードで書く分散ワークフローエンジンの仕組みと比較
Temporalは、ワークフローを設定ファイルやYAMLではなく通常のプログラミング言語のコードとして記述できる、クラウドネイティブな分散ワークフローエンジンです。実行状態をイベント履歴として永続化するため、ワーカーがクラッシュしても別のワーカーが履歴を再生して途中から処理を続行できます(Durable Execution)。本記事では、Temporalの仕組み・WorkflowとActivityの違い・リトライや補償の設計・他エンジンとの違いを、実務で判断できる粒度で整理します。
まとめ:Temporalの要点
先に結論を整理します。
- コードで書ける:条件分岐・ループ・例外処理を、Go/Java/TypeScriptなどのSDKでそのまま記述できる。
- 状態が消えない:実行の進行をイベント履歴として保存し、障害時は履歴を再生して自動復旧する。
- リトライ・補償が組み込み:Activityはリトライポリシーで自動再試行し、失敗時はSagaパターンで補償できる。
- 長時間実行に強い:履歴上限に達する前にContinue-As-Newで継続し、事実上無期限に動かせる。
- 使いどころ:決済・注文・MLパイプラインなど、状態を持つミッションクリティカルな長時間ワークフロー。バッチ中心ならAirflow、K8sコンテナ並列ならArgoが向く。
以下、それぞれを詳しく見ていきます。
Temporalとは:コードでワークフローを書く分散実行基盤
従来のワークフローエンジンは、処理の流れをYAMLや独自DSLで定義することが多く、複雑な分岐やエラーハンドリングを表現しづらいという課題がありました。Temporalはワークフローとアクティビティを通常の関数として書けるため、型チェックやIDE補完、既存のデバッグ手法をそのまま活用できます。たとえば「ある口座から引き落とし、別口座へ入金し、失敗時は返金する」という送金処理を、引き落とし・入金・返金のアクティビティを順に呼び出すコードとして自然に記述できます。
Temporalは信頼性を最優先する設計で、「開始したワークフローは必ず完了する」ことをプラットフォーム側が担保します。状態の保持と復旧がデフォルトで効くため、開発者はビジネスロジックの記述に集中できます。
CadenceからTemporalへ(開発の背景)
Temporalは、Uberで開発されたワークフローシステム「Cadence」をフォークして生まれ、Cadenceの開発者が設立したTemporal Technologies社が開発しています。オープンソース(MITライセンス)として公開され、多言語SDKやマネージドサービス(Temporal Cloud)を中心にエコシステムが拡大しています。
アーキテクチャ:4つのサービスとTask Queue
サービス構成
Temporalクラスタは、Frontend(リクエスト受付・APIルーティング)、History(イベント履歴やタイマー、タスク状態の保持)、Matching(Task Queue管理とワーカーへのタスク割り当て)、Worker Service(cron処理などの内部システムワークフロー実行)の4サービスで構成されます。各サービスは水平スケール可能で、必要に応じて複数インスタンスをクラスタ化して負荷分散します。
データ永続化
Temporalは永続化用データベースを必須依存として持ち、イベント履歴・実行状態・タスク・ネームスペース情報などを格納します。対応DBはMySQL・PostgreSQL・Apache Cassandraなどで、可視化(検索)用途には別途Elasticsearchを併用できます。DBへ永続化されるため、サーバー自身の再起動後でもワークフロー状態は完全に復元できます。
Task Queueとワーカー
ワークフローやアクティビティの実行要求はTask Queueに蓄積され、ワーカーが空き次第ポーリングして実行します。ワーカーは自分が空いているときだけタスクを取りに行くため過負荷になりにくく、タスクはサーバー側で永続化されるので、ワーカーが落ちても別のワーカーが復旧後に再取得して実行できます。ワーカー数を増やすほど並列処理能力をほぼ直線的に向上させられます。クライアントSDKはワークフロー開始やSignal送信をFrontendへgRPCで行います。
WorkflowとActivityの違い
Workflow:決定的実行とオーケストレーション
Workflowは、複数のアクティビティ呼び出しや分岐・ループ・タイマー・シグナル待機を記述して全体の流れを制御する、オーケストレーション役のコードです。Temporalはワークフローコードを決定的(deterministic)に扱い、すべてのステップをイベント履歴に記録します。ワーカーは履歴を再生しながら関数を実行するため、同じ入力からは常に同じ経路をたどる必要があります。そのため、ワークフロー内では乱数や現在時刻の直接取得などの非決定的な処理を避け、状態変化はイベントに依存させます。
Activity:副作用とリトライ
Activityは、外部API呼び出し・DBアクセス・ファイル処理といった副作用を伴う実処理を担う関数です。非決定的な処理を含められる代わりに、Activityの実行は「少なくとも1回(at-least-once)」の保証で行われ、失敗時はリトライポリシーに従い自動再試行されます。再試行で同じ処理が二重に走っても問題が出ないよう、Activityは冪等に設計することが推奨されます(更新はトランザクションや一意キーで自己整合性を取るなど)。
導入:ローカル環境とSDK
ローカル開発(Docker Compose / CLI)
手軽に試すなら公式のtemporalio/docker-composeを利用します。リポジトリをクローンしてdocker compose upを実行すると、既定構成ではPostgreSQLとElasticsearchを使うTemporalサーバーが起動し、gRPCフロントエンドがlocalhost:7233、Web UIがlocalhost:8080で利用できます。さらに軽量に試すなら、CLIのtemporal server start-devでWeb UI付きの開発サーバーを起動できます(既定ネームスペースを作成し、インメモリDBで動作)。
SDKでワーカーを起動
各言語SDKでは、依存ライブラリを追加してワークフロー・アクティビティを実装し、ワーカーにTask Queueを登録して待機させます。TypeScriptなら@temporalio/workerのWorker.create()、Goならgo.temporal.io/sdkのworker.New()、JavaならWorkerFactoryでワーカーを生成します。起動後はTask Queueからタスクを受け取り、登録したコードを実行します。
信頼性の仕組み:リトライ・補償・長時間実行
リトライポリシー(maximumAttempts)
Activityにはリトライポリシーを設定でき、最大試行回数・バックオフ係数・再試行しないエラー種別などを細かく指定できます。重要なのがmaximumAttemptsの挙動です。
- 0(デフォルト)=無制限:タイムアウトに達するまで何度でも再試行します(Activityは既定でこの挙動)。
- 1=1回のみ実行(リトライなし):失敗してもそのまま失敗を返します。
つまり「再試行を完全に止めたい」場合はmaximumAttempts=1を指定します。なお、回数ではなくWorkflow Execution TimeoutやSchedule-To-Close Timeoutで全体の試行時間を制限する設計が推奨されます。Workflow自体は既定ではリトライしない(Activityのみ既定でリトライする)点も押さえておきましょう。
補償アクション(Sagaパターン)
分散トランザクション的な振る舞いは、補償(逆操作)をワークフロー内に書くことで実現します。たとえば送金処理では、入金で失敗したら返金を呼ぶのが補償にあたります。進行とステップ完了の追跡はTemporalが行うため、開発者は「失敗時に何を巻き戻すか」を書くだけで済みます。
import { proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { withdraw, deposit, refund } =
proxyActivities<typeof activities>({ startToCloseTimeout: '1m' });
export async function moneyTransfer(details: PaymentDetails): Promise<void> {
await withdraw(details);
try {
await deposit(details);
} catch (err) {
await refund(details); // 補償(Saga)
throw err;
}
}
※上記は仕組みを示す概念コードです。実行にはTemporalサーバーとワーカーの起動が必要です。
長時間実行とContinue-As-New
Temporalはワークフローを長期間動かし続けられますが、1つの実行のイベント履歴には上限があります。正確には51,200イベントまたは50MBで、10,240イベントまたは10MBを超えると警告が出ます。上限に達しそうなときはContinue-As-Newで「新しいRun IDの新しい履歴」として処理を引き継ぎ、事実上無期限に継続できます。年1回の請求処理のような待機の長いワークフローも、タイマーで待機しつつ復帰して処理を実行できます。
Signal/Queryで実行中のワークフローと対話する
長時間動くワークフローには、外部からの入力を待ったり、途中状態を確認したりする手段が要ります。TemporalはSignal(外部からワークフローへ値を送り状態を変える)とQuery(実行中の状態を読み取る・副作用なし)を備えます。「承認が来るまで待ち、来なければタイムアウトする」フローは次のように書けます。
import {
defineSignal, defineQuery, setHandler, condition,
} from '@temporalio/workflow';
export const approveSignal = defineSignal('approve');
export const statusQuery = defineQuery<string>('status');
export async function approvalWorkflow(): Promise<string> {
let approved = false;
let status = 'pending';
// Signal:外部から承認が届いたら状態を更新(状態変更のみ)
setHandler(approveSignal, () => { approved = true; });
// Query:現在の状態を返す(非async・副作用なし)
setHandler(statusQuery, () => status);
// 承認が来るまで最大7日待つ(来なければtimeout)
const ok = await condition(() => approved, '7 days');
status = ok ? 'approved' : 'timeout';
return status;
}
役割分担が要点です。Signalハンドラは状態を書き換えられるが戻り値は返さない、Queryハンドラは値を返すがasyncにできず、Activity実行などの副作用を起こしてはいけない(読み取り専用)。condition()は指定条件が真になるかタイムアウトするまで待つヘルパで、Signalと組み合わせると「人手承認待ち」「外部イベント待ち」を簡潔に表現できます。送信側はクライアントからhandle.signal(approveSignal)/handle.query(statusQuery)で呼び出します。
※上記は仕組みを示す概念コードです(実行にはTemporalサーバーとワーカーが必要)。
他のワークフローエンジンとの比較
Temporalは「状態を持つ長時間ワークフロー」に強い一方、用途によっては他エンジンが適します。代表的なエンジンを整理します。
| 観点 | Temporal | Airflow | Argo Workflows | Step Functions |
|---|---|---|---|---|
| 定義方法 | コード(多言語SDK) | Python DAG | YAML/CRD | JSON |
| 主な用途 | 長時間・状態保持 | ETL・定期バッチ | K8sコンテナ並列 | AWS統合 |
| 状態復旧 | 履歴再生で再開 | 限定的 | 限定的 | マネージド |
| 実行基盤 | 自己ホスト/Cloud | 自己ホスト | K8s必須 | マネージド |
| 課金 | 固定(自己ホスト) | 固定 | 固定 | 遷移数で従量 |
バッチETLならPrefectやAirflow、Kubernetesネイティブのコンテナ並列ならArgo、AWSに寄せるならStep FunctionsやAWS Glue、状態を持つ長時間・高信頼の業務やマイクロサービスオーケストレーションならTemporal——という住み分けが目安です。
実務で押さえる設計のコツ
Temporalを業務で使うときに効く設計ポイントを整理します。検索でも「temporal activity」「temporal workflow python」「temporal saga」など実装寄りの関心が多く、ここを外すと再試行や障害復旧の恩恵を活かしきれません。
- Activityは冪等に:再試行前提のため、外部呼び出しは一意キーやトランザクションで二重実行に耐える設計にする。
- Workflowは決定的に:乱数・現在時刻・直接I/Oはワークフロー本体に書かず、ActivityやSide Effectへ逃がす(履歴再生で結果がずれないため)。
- テストはリプレイで:各SDKのテスト環境でワークフローをローカル実行し、リトライや補償などの異常系を高速に検証する。
- 粒度と監視:Activityは小さく分割して再試行の影響範囲を抑え、メトリクスとWeb UIで停止ステップやエラー率を可視化する。
決定性でつまずく落とし穴と回避策
Temporalで最も多いトラブルは「ワークフローコードの非決定性」です。ワーカーは履歴をリプレイして状態を復元するため、再実行のたびに結果が変わるコードを書くと、復旧時に履歴と食い違ってnondeterminism errorでワークフローが止まります。代表的な落とし穴と回避策を整理します。
| 落とし穴 | なぜ危険か | 回避策 |
|---|---|---|
| 乱数・UUID生成 | リプレイで値が変わる | Activity/Side Effectで生成 |
| 現在時刻の直接取得 | 実行ごとに変動 | SDKのworkflow時刻APIを使う |
| 直接I/O(API・DB) | 非決定的で副作用あり | Activityに切り出す |
| setTimeout等の実時間待機 | 永続化されず復旧不可 | workflowのsleep(耐久タイマー) |
| Map/Setの順不同走査 | 反復順序が一定しない | 配列やソート済みで反復 |
| 実行中のコード変更 | 履歴と不一致になる | Versioning/Patchで分岐 |
もう一つ多いのが履歴肥大化による強制終了です。1実行の履歴は51,200イベントまたは50MBが上限のため、ループで延々とActivityを呼ぶ設計はContinue-As-Newで区切ります。前述のとおりQueryハンドラ内でActivityを呼んだり状態を書き換えたりするのも禁止(Queryは読み取り専用)。これらは一度踏むと本番で復旧不能になりやすいので、設計段階で潰しておくのが安全です。
バージョニングと運用
Worker Versioning
稼働中のワークフローコードを更新する際は、従来はgetVersion()等で新旧分岐を書いていましたが、近年はWorker Versioning(Worker Deployment)が利用できます。起動済みワークフローを起動時のワーカーバージョンに「ピン留め」できるため、コードを変更しても進行中の実行に影響しません。新コードへの切り替えは新規実行分のみに適用され、ブルーグリーン等のロールアウトと組み合わせると安全にデプロイできます。
Nexusによるサービス間連携
ネームスペースをまたいだワークフロー連携にはTemporal Nexusを利用できます。Nexusは2025年に一般提供(GA)となった機能で、サービス契約(エンドポイント)を介してNamespace内・Namespace間の呼び出しを安全に行えます。これにより、チームやアプリの境界をまたいでもDurable Executionの利点を保ったまま連携できます。
監視・トラブルシューティング
Web UIでワークフロー一覧・ステータス・イベント履歴・入出力を確認でき、Prometheus互換のメトリクスをGrafana等で可視化してエラー率や遅延を監視できます。問題調査時は履歴で停止ステップを特定し、temporal workflow describeでCLIから最新状態を取得します。
よくある質問(FAQ)
TemporalとAirflowはどう違いますか?
AirflowはDAGベースのバッチスケジューラでETLや定期実行に強い一方、状態を持つ長時間ワークフローの途中再開は限定的です。Temporalは状態をイベント履歴で保持し、障害時に正確に再開できる点が異なります。
リトライを止めるにはどう設定しますか?
リトライポリシーのmaximumAttemptsを1に設定すると1回のみ実行(リトライなし)になります。0(デフォルト)は無制限です。
ワークフローは無期限に動かせますか?
動かせます。ただし1実行の履歴は51,200イベントまたは50MBが上限のため、上限前にContinue-As-Newで新しい実行へ引き継ぎます。
どんな言語で書けますか?
Go・Java・TypeScript・Python・.NET・Ruby など複数のSDKが提供されています(言語ごとに対応機能差はあります)。
実行中のワークフローに外部から値を渡したり状態を見たりできますか?
できます。Signalで外部から状態を変更し、Queryで実行中の状態を読み取れます(Queryは副作用なしの読み取り専用)。承認待ちなどはSignalとcondition()の組み合わせで実装します。
nondeterminism errorで止まりました。原因は?
ワークフロー本体に乱数・現在時刻・直接I/Oなどの非決定的処理を書くと、リプレイ時に履歴と食い違って発生します。これらはActivityに切り出し、稼働中のコード変更はVersioning/Patchで分岐させます。