人間の対話パターンは、ユーザーからの入力を一時停止して待機してから続行するワークフローを記述します。 このパターンは、承認ワークフロー、多要素認証、および制限時間内にユーザーが応答するシナリオに役立ちます。
このサンプルでは、人間の操作を含む Durable Functions オーケストレーションを構築する方法を示します。 この例では、SMS ベースの電話検証システムを実装します。 電話番号の確認と多要素認証 (MFA) フローでは一般的です。
注
Azure Functionsの Node.js プログラミング モデルのバージョン 4 が一般公開されています。 v4 モデルは、JavaScript および TypeScript 開発者に、より柔軟で直感的なエクスペリエンスを提供するように設計されています。 v3 と v4 の違いの詳細については、 移行ガイドを参照してください。
次のコード スニペットでは、JavaScript (PM4) は、新しいエクスペリエンスであるプログラミング モデル v4 を示しています。
[前提条件]
この記事では、Durable Task SDK を使用して人間の対話パターンを実装する方法について説明します。 この例では、オーケストレーションが続行する前に、ユーザーが要求を承認または拒否するのを待機する承認ワークフローを実装します。
シナリオの概要
電話による確認は、アプリを使用するユーザーがスパム送信者ではなく、提供する電話番号を制御していることを確認するのに役立ちます。 多要素認証は、アカウントを保護するための一般的な方法です。 独自の電話検証を構築するには、ユーザーとの ステートフルな対話 が必要です。 通常、ユーザーはコード (4 桁の数字など) を取得し、 妥当な時間で応答する必要があります。
Standard Azure Functionsはステートレス (他の多くのクラウド エンドポイントと同様) であるため、この種の対話では、状態をデータベースまたは別の永続的なストアに格納する必要があります。 また、相互作用を複数の関数に分割して調整します。 たとえば、1 つの関数がコードを生成して格納し、ユーザーの電話に送信します。 別の関数は、ユーザーの応答を受け取り、元の要求にマップしてコードを検証します。 セキュリティの保護に役立つタイムアウトを追加します。 このワークフローはすぐに複雑になります。
Durable Functionsすると、このシナリオの複雑さが軽減されます。 このサンプルでは、オーケストレーター関数は、外部データ ストアなしでステートフルな操作を管理します。 オーケストレーター関数は 永続的であるため、これらの対話型フローは信頼性が高いです。
承認ワークフローは、続行する前に人間が要求をレビューする必要があるビジネス アプリケーションで一般的です。 ワークフローの要件は次のとおりです。
- 人間の応答を無期限に待機するか、タイムアウトになるまで待機します
- 承認と却下の両方 の結果を処理する
- 応答が受信されない場合のサポート タイムアウト
- 要求者が進行状況を確認できるように状態を追跡する
Durable Task SDK は、次の方法でこのシナリオを簡略化します。
- 外部イベント: オーケストレーションは、外部システムまたはユーザーによって発生したイベントを一時停止して待機できます
- 持続的タイマー: 応答が受信されない場合に作動するタイムアウトを設定します
- カスタム状態: 現在のワークフロー状態を追跡してクライアントに公開する
Twilio 統合の構成
このサンプルでは、Twilio サービスを使って、携帯電話に SMS メッセージを送信します。 Azure Functionsは既に Twilio バインディングを介して Twilio をサポートしており、このサンプルではその機能を使用しています。
最初に必要なものは Twilio アカウントです。 https://www.twilio.com/try-twilio で無料で作成できます。 アカウントを作成したら、次の 3 つのアプリ設定を関数アプリに追加します。
| アプリ設定の名前 | 値の説明 |
|---|---|
| TwilioAccountSid | Twilio アカウントの SID |
| TwilioAuthToken | Twilio アカウントの認証トークン |
| TwilioPhoneNumber | Twilio アカウントに関連付けられている電話番号。 これは、SMS メッセージの送信に使われます。 |
オーケストレーターとアクティビティ
この記事では、サンプル アプリの次の関数について説明します。
-
E4_SmsPhoneVerification: 電話検証プロセスを実行し、タイムアウトと再試行を管理する オーケストレーター関数 。 -
E4_SendSmsChallenge: テキスト メッセージによってコードを送信する アクティビティ関数 。
注
サンプル アプリのHttpStart関数とクイック スタートはオーケストレーション クライアントとして機能し、オーケストレーター関数をトリガーします。
この記事では、サンプル アプリの次のコンポーネントについて説明します。
-
ApprovalOrchestration/approvalOrchestrator/human_interaction_orchestrator: 承認要求を送信し、人間の応答またはタイムアウトを待機するオーケストレーター。 -
SubmitApprovalRequestActivity/submitRequest/submit_approval_request: 電子メールやチャット メッセージなど、人間の承認者に通知するアクティビティ。 -
ProcessApprovalActivity/processApproval/process_approval: 承認の決定を処理するアクティビティ。
Orchestrator
E4_SmsPhoneVerification オーケストレーター関数
[FunctionName("E4_SmsPhoneVerification")]
public static async Task<bool> Run(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
string phoneNumber = context.GetInput<string>();
if (string.IsNullOrEmpty(phoneNumber))
{
throw new ArgumentNullException(
nameof(phoneNumber),
"A phone number input is required.");
}
int challengeCode = await context.CallActivityAsync<int>(
"E4_SendSmsChallenge",
phoneNumber);
using (var timeoutCts = new CancellationTokenSource())
{
// The user has 90 seconds to respond with the code they received in the SMS message.
DateTime expiration = context.CurrentUtcDateTime.AddSeconds(90);
Task timeoutTask = context.CreateTimer(expiration, timeoutCts.Token);
bool authorized = false;
for (int retryCount = 0; retryCount <= 3; retryCount++)
{
Task<int> challengeResponseTask =
context.WaitForExternalEvent<int>("SmsChallengeResponse");
Task winner = await Task.WhenAny(challengeResponseTask, timeoutTask);
if (winner == challengeResponseTask)
{
// We got back a response! Compare it to the challenge code.
if (challengeResponseTask.Result == challengeCode)
{
authorized = true;
break;
}
}
else
{
// Timeout expired
break;
}
}
if (!timeoutTask.IsCompleted)
{
// All pending timers must be complete or canceled before the function exits.
timeoutCts.Cancel();
}
return authorized;
}
}
注
最初は明らかではないかもしれませんが、このオーケストレーターは 決定論的オーケストレーションの制約に違反しません。
CurrentUtcDateTime プロパティはタイマーの有効期限を計算し、オーケストレーター コードのこの時点で再生するたびに同じ値を返すので、決定論的です。 この動作により、winnerを繰り返し呼び出すたびに、Task.WhenAnyが同じになります。
開始されると、このオーケストレーター関数は次のことを行います。
- SMS 通知を送信する電話番号を取得します。
- E4_SendSmsChallengeを呼び出して SMS メッセージをユーザーに送信し、予想される 4 桁のチャレンジ コードを返します。
- 現在の時刻から 90 秒後にトリガーされる永続的タイマーを作成します。
- タイマーと並行して、ユーザーからの SmsChallengeResponse イベントを待ちます。
ユーザーは、4 桁のコードで SMS メッセージを受け取ります。 オーケストレーター インスタンスに同じコードを送信して検証を完了するのに 90 秒かかります。 間違ったコードを送信すると、同じ 90 秒のウィンドウ内でさらに 3 回試行されます。
Warnung
不要になったタイマーをキャンセルします。 上の例では、オーケストレーションはチャレンジ応答を受け入れるとタイマーを取り消します。
オーケストレーターは承認要求を送信し、人間の応答またはタイムアウトを待機します。
using Microsoft.DurableTask;
using System;
using System.Threading;
using System.Threading.Tasks;
[DurableTask(nameof(ApprovalOrchestration))]
public class ApprovalOrchestration : TaskOrchestrator<ApprovalRequestData, ApprovalResult>
{
public override async Task<ApprovalResult> RunAsync(
TaskOrchestrationContext context, ApprovalRequestData input)
{
string requestId = input.RequestId;
double timeoutHours = input.TimeoutHours;
// Step 1: Submit the approval request (notify approver)
SubmissionResult submissionResult = await context.CallActivityAsync<SubmissionResult>(
nameof(SubmitApprovalRequestActivity), input);
// Make the status available via custom status
context.SetCustomStatus(submissionResult);
// Step 2: Create a durable timer for the timeout
DateTime timeoutDeadline = context.CurrentUtcDateTime.AddHours(timeoutHours);
using var timeoutCts = new CancellationTokenSource();
Task timeoutTask = context.CreateTimer(timeoutDeadline, timeoutCts.Token);
// Step 3: Wait for an external event (approval/rejection)
Task<ApprovalResponseData> approvalTask = context.WaitForExternalEvent<ApprovalResponseData>(
"approval_response");
// Step 4: Wait for either the timeout or the approval response
Task completedTask = await Task.WhenAny(approvalTask, timeoutTask);
// Step 5: Process based on which task completed
ApprovalResult result;
if (completedTask == approvalTask)
{
// Human responded in time - cancel the timeout timer
timeoutCts.Cancel();
ApprovalResponseData approvalData = approvalTask.Result;
// Process the approval
result = await context.CallActivityAsync<ApprovalResult>(
nameof(ProcessApprovalActivity),
new ProcessApprovalInput
{
RequestId = requestId,
IsApproved = approvalData.IsApproved,
Approver = approvalData.Approver
});
}
else
{
// Timeout occurred
result = new ApprovalResult
{
RequestId = requestId,
Status = "Timeout",
ProcessedAt = context.CurrentUtcDateTime.ToString("o")
};
}
return result;
}
}
このオーケストレーターは、次のアクションを実行します。
- 承認者に通知するアクティビティを呼び出して、承認要求を送信します。
- クライアントが進行状況を追跡できるように、カスタム状態を設定します。
- タイムアウト期限の持続的タイマーを作成します。
- 承認者が発生させる外部イベント (
approval_response) を待機します。 -
WhenAny、when_any、またはanyOfを使用し、承認またはタイムアウトのいずれかが最初に終了するのを待機します。 - 完了したタスクに基づいて結果を処理します。
Warnung
不要になったタイマーをキャンセルします。 C# の例では、オーケストレーションは承認を受けるとタイムアウト タイマーを取り消します。
アクティビティ
E4_SendSmsChallenge アクティビティ関数
E4_SendSmsChallenge関数は、Twilio バインディングを使用して、4 桁のコードを含む SMS メッセージをユーザーに送信します。
[FunctionName("E4_SendSmsChallenge")]
public static int SendSmsChallenge(
[ActivityTrigger] string phoneNumber,
ILogger log,
[TwilioSms(AccountSidSetting = "TwilioAccountSid", AuthTokenSetting = "TwilioAuthToken", From = "%TwilioPhoneNumber%")]
out CreateMessageOptions message)
{
// Get a random number generator with a random seed (not time-based)
var rand = new Random(Guid.NewGuid().GetHashCode());
int challengeCode = rand.Next(10000);
log.LogInformation($"Sending verification code {challengeCode} to {phoneNumber}.");
message = new CreateMessageOptions(new PhoneNumber(phoneNumber));
message.Body = $"Your verification code is {challengeCode:0000}";
return challengeCode;
}
注
サンプルを実行するには、Microsoft.Azure.WebJobs.Extensions.Twilio NuGet パッケージをインストールします。 メインの Twilio NuGet パッケージ は、バージョンの競合やビルド エラーを引き起こす可能性があるため、インストールしないでください。
アクティビティは承認要求を送信し、応答を処理します。
承認要求アクティビティを送信する
using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
[DurableTask(nameof(SubmitApprovalRequestActivity))]
public class SubmitApprovalRequestActivity : TaskActivity<ApprovalRequestData, SubmissionResult>
{
private readonly ILogger<SubmitApprovalRequestActivity> _logger;
public SubmitApprovalRequestActivity(ILogger<SubmitApprovalRequestActivity> logger)
{
_logger = logger;
}
public override Task<SubmissionResult> RunAsync(
TaskActivityContext context, ApprovalRequestData input)
{
_logger.LogInformation(
"Submitting approval request {RequestId} from {Requester} for {Item}",
input.RequestId, input.Requester, input.Item);
// In a real system, this would send an email, notification, or update a database
var result = new SubmissionResult
{
RequestId = input.RequestId,
Status = "Pending",
SubmittedAt = DateTime.UtcNow.ToString("o"),
ApprovalUrl = $"http://localhost:8000/api/approvals/{input.RequestId}"
};
return Task.FromResult(result);
}
}
プロセス承認アクティビティ
using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
[DurableTask(nameof(ProcessApprovalActivity))]
public class ProcessApprovalActivity : TaskActivity<ProcessApprovalInput, ApprovalResult>
{
private readonly ILogger<ProcessApprovalActivity> _logger;
public ProcessApprovalActivity(ILogger<ProcessApprovalActivity> logger)
{
_logger = logger;
}
public override Task<ApprovalResult> RunAsync(
TaskActivityContext context, ProcessApprovalInput input)
{
string status = input.IsApproved ? "Approved" : "Rejected";
_logger.LogInformation(
"Processing {Status} request {RequestId} by {Approver}",
status, input.RequestId, input.Approver);
// In a real system, this would update a database, trigger workflows, etc.
var result = new ApprovalResult
{
RequestId = input.RequestId,
Status = status,
ProcessedAt = DateTime.UtcNow.ToString("o"),
Approver = input.Approver
};
return Task.FromResult(result);
}
}
// Data classes
public class ApprovalRequestData
{
public string RequestId { get; set; } = string.Empty;
public string Requester { get; set; } = string.Empty;
public string Item { get; set; } = string.Empty;
public double TimeoutHours { get; set; } = 24.0;
}
public class ApprovalResponseData
{
public bool IsApproved { get; set; }
public string Approver { get; set; } = string.Empty;
}
public class SubmissionResult
{
public string RequestId { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string SubmittedAt { get; set; } = string.Empty;
public string ApprovalUrl { get; set; } = string.Empty;
}
public class ProcessApprovalInput
{
public string RequestId { get; set; } = string.Empty;
public bool IsApproved { get; set; }
public string Approver { get; set; } = string.Empty;
}
public class ApprovalResult
{
public string RequestId { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string ProcessedAt { get; set; } = string.Empty;
public string? Approver { get; set; }
}
サンプルを実行する
サンプルの HTTP によってトリガーされる関数を使用して、次の HTTP POST 要求を送信してオーケストレーションを開始します。
POST http://{host}/orchestrators/E4_SmsPhoneVerification
Content-Length: 14
Content-Type: application/json
"+1425XXXXXXX"
HTTP/1.1 202 Accepted
Content-Length: 695
Content-Type: application/json; charset=utf-8
Location: http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}
{"id":"741c65651d4c40cea29acdd5bb47baf1","statusQueryGetUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}","sendEventPostUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1/raiseEvent/{eventName}?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}","terminatePostUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1/terminate?reason={text}&taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}"}
オーケストレーター関数は、電話番号を受信し、ランダムに生成された 4 桁の検証コード ( 2168など) を使用して、その番号に SMS メッセージをすぐに送信します。 その後、関数は 90 秒間応答を待機します。
コードに応答するには、別の関数で RaiseEventAsync (.NET) または raiseEvent (JavaScript および TypeScript) を使用するか、202 応答で sendEventPostUri HTTP POST エンドポイントを呼び出します。
{eventName}をSmsChallengeResponseに置き換えます。
POST http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1/raiseEvent/SmsChallengeResponse?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}
Content-Length: 4
Content-Type: application/json
2168
タイマーの有効期限が切れる前にイベントを送信すると、オーケストレーションが完了し、 output フィールドが true に設定されます。これは検証が成功したことを示します。
GET http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}
HTTP/1.1 200 OK
Content-Length: 144
Content-Type: application/json; charset=utf-8
{"runtimeStatus":"Completed","input":"+1425XXXXXXX","output":true,"createdTime":"2017-06-29T19:10:49Z","lastUpdatedTime":"2017-06-29T19:12:23Z"}
タイマーの有効期限が切れた場合、または間違ったコードを 4 回入力した場合は、状態を照会してoutputfalseに設定されていることを確認します。これは、電話による確認が失敗したことを示します。
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 145
{"runtimeStatus":"Completed","input":"+1425XXXXXXX","output":false,"createdTime":"2017-06-29T19:20:49Z","lastUpdatedTime":"2017-06-29T19:22:23Z"}
サンプルを実行するには
ローカル開発用の Durable Task Scheduler エミュレーターを起動します。
docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latestワーカーを起動 してオーケストレーターとアクティビティを登録します。
クライアントを実行 して承認ワークフローをスケジュールし、イベントを送信します。
using System;
using System.Threading.Tasks;
var client = DurableTaskClientBuilder.UseDurableTaskScheduler(connectionString).Build();
// Schedule the approval workflow
var input = new ApprovalRequestData
{
RequestId = "request-" + Guid.NewGuid().ToString(),
Requester = "john.doe@example.com",
Item = "Vacation Request - 5 days",
TimeoutHours = 24
};
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
nameof(ApprovalOrchestration), input);
Console.WriteLine($"Started approval workflow: {instanceId}");
// Simulate human approving the request
Console.WriteLine("Simulating approval...");
await Task.Delay(2000);
// Raise the approval event
var approvalResponse = new ApprovalResponseData
{
IsApproved = true,
Approver = "manager@example.com"
};
await client.RaiseEventAsync(instanceId, "approval_response", approvalResponse);
// Wait for completion
var result = await client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true);
Console.WriteLine($"Result: {result.ReadOutputAs<ApprovalResult>().Status}");
次のステップ
このサンプルでは、WaitForExternalEvent API や CreateTimer API など、高度なDurable Functions機能を示します。
Task.WhenAny (C#)、context.df.Task.any (JavaScript と TypeScript)、または context.task_any (Python) を組み合わせて、ユーザーの応答を待機するワークフローの信頼性の高いタイムアウト パターンを実装する方法を示します。 Durable Functionsの詳細については、特定のトピックに関する一連の記事を参照してください。
このサンプルでは、Durable Task SDK を使用して、構成可能なタイムアウトでユーザーが応答するのを待つワークフローを実装する方法を示します。 主な概念:
外部イベント:
WaitForExternalEventを使用して入力を待機する永続的タイマー:
CreateTimerを使用してタイムアウトを実装するレーシング タスク:
WhenAny、when_any、またはanyOfを使用して、最初に完了したタスクを処理する