Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions samples/scenarios/DocumentProcessingOnAKS/Client/Client.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>b2f26a38-2066-4051-916a-b21af6b0f10e</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.17.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
</ItemGroup>

</Project>
19 changes: 19 additions & 0 deletions samples/scenarios/DocumentProcessingOnAKS/Client/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

# Copy csproj and restore dependencies
COPY ["Client.csproj", "./"]
RUN dotnet restore

# Copy all files and build
COPY . .
RUN dotnet publish -c Release -o /app/publish

# Build runtime image
FROM mcr.microsoft.com/dotnet/runtime:10.0 AS final
WORKDIR /app
COPY --from=build /app/publish .

EXPOSE 8080
# Set the entrypoint
ENTRYPOINT ["dotnet", "Client.dll"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace DurableTaskOnAKS.Client.Models;

/// <summary>
/// A document submitted for processing.
/// Shape must match the Worker's <c>DocumentInfo</c> for JSON round-tripping.
/// </summary>
public record DocumentInfo(string Id, string Title, string Content);
78 changes: 78 additions & 0 deletions samples/scenarios/DocumentProcessingOnAKS/Client/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using DurableTaskOnAKS.Client.Models;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Client.AzureManaged;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

// ---------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------

string endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? "http://localhost:8080";
string taskHub = Environment.GetEnvironmentVariable("TASKHUB") ?? "default";
string? clientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID");

var builder = Host.CreateApplicationBuilder();
builder.Services.AddDurableTaskClient(b => b.UseDurableTaskScheduler(
BuildConnectionString(endpoint, taskHub, clientId)));

using var host = builder.Build();
await host.StartAsync();
var client = host.Services.GetRequiredService<DurableTaskClient>();

// ---------------------------------------------------------------------------
// Sample documents
// ---------------------------------------------------------------------------

DocumentInfo[] docs =
[
new("doc-1", "Cloud Migration Strategy",
"Plan to migrate on-prem workloads to Azure over 18 months."),
new("doc-2", "Quarterly Incident Report",
"Summary of production incidents and remediation steps for Q4."),
new("doc-3", "ML Model Evaluation",
"Transformer model achieved 94% accuracy on document classification."),
];

// ---------------------------------------------------------------------------
// Submit and wait for each orchestration
// ---------------------------------------------------------------------------

Console.WriteLine($"Endpoint: {endpoint} | TaskHub: {taskHub}");
Console.WriteLine($"Submitting {docs.Length} documents...\n");

foreach (var doc in docs)
{
string id = await client.ScheduleNewOrchestrationInstanceAsync(
"DocumentProcessingOrchestration", doc);

Console.WriteLine($" Scheduled [{id}] '{doc.Title}'");

var meta = await client.WaitForInstanceCompletionAsync(id, getInputsAndOutputs: true);

if (meta.RuntimeStatus == OrchestrationRuntimeStatus.Completed)
Console.WriteLine($" -> {meta.ReadOutputAs<string>()}\n");
else
Console.WriteLine($" -> {meta.RuntimeStatus}: {meta.FailureDetails?.ErrorMessage}\n");
}

Console.WriteLine("Done.");

// In non-interactive/container environments (including AKS), keep the process alive for log inspection; locally (interactive) just exit.
if (!Environment.UserInteractive || Console.IsInputRedirected)
await Task.Delay(Timeout.InfiniteTimeSpan); // keep pod alive for log inspection

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

static string BuildConnectionString(string endpoint, string taskHub, string? clientId)
{
string auth = endpoint.StartsWith("http://localhost")
? "None"
: !string.IsNullOrEmpty(clientId)
? $"ManagedIdentity;ClientID={clientId}"
: "DefaultAzure";

return $"Endpoint={endpoint};TaskHub={taskHub};Authentication={auth}";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: client
namespace: default
annotations:
azure.workload.identity/client-id: {{ .Env.AZURE_USER_ASSIGNED_IDENTITY_CLIENT_ID }}
labels:
azure.workload.identity/use: "true"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: client
namespace: default
labels:
app: client
spec:
replicas: 1
selector:
matchLabels:
app: client
template:
metadata:
labels:
app: client
azure.workload.identity/use: "true"
spec:
serviceAccountName: client
containers:
- name: client
image: {{ .Env.SERVICE_CLIENT_IMAGE_NAME }}
env:
- name: ENDPOINT
value: {{ .Env.DTS_ENDPOINT }}
- name: TASKHUB
value: {{ .Env.DTS_TASKHUB_NAME }}
- name: AZURE_CLIENT_ID
value: {{ .Env.AZURE_USER_ASSIGNED_IDENTITY_CLIENT_ID }}
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
31 changes: 31 additions & 0 deletions samples/scenarios/DocumentProcessingOnAKS/DurableTaskOnAKS.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Worker", "Worker\Worker.csproj", "{69C79C25-F84F-479E-3E79-EC68C71F7F70}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Client.csproj", "{AE7EEA74-2FE0-136F-D797-854FD87E022A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{69C79C25-F84F-479E-3E79-EC68C71F7F70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{69C79C25-F84F-479E-3E79-EC68C71F7F70}.Debug|Any CPU.Build.0 = Debug|Any CPU
{69C79C25-F84F-479E-3E79-EC68C71F7F70}.Release|Any CPU.ActiveCfg = Release|Any CPU
{69C79C25-F84F-479E-3E79-EC68C71F7F70}.Release|Any CPU.Build.0 = Release|Any CPU
{AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CE89663C-31F8-4259-9DC2-3C41C5629266}
EndGlobalSection
EndGlobal
185 changes: 185 additions & 0 deletions samples/scenarios/DocumentProcessingOnAKS/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Durable Task Scheduler on AKS

A document-processing sample built with the [.NET Durable Task SDK](https://www.nuget.org/packages/Microsoft.DurableTask.Client.AzureManaged) and [Azure Durable Task Scheduler](https://learn.microsoft.com/en-us/azure/durable-task-scheduler/), deployed to **Azure Kubernetes Service (AKS)** with `azd`.

## How It Works

The app has two components — a **Client** and a **Worker** — that communicate through the Durable Task Scheduler (DTS).

The **Client** submits three sample documents and waits for each to be processed. The **Worker** hosts a `DocumentProcessingOrchestration` that runs a two-stage pipeline:

```
Client ──▶ DocumentProcessingOrchestration
├─ 1. ValidateDocument (activity chaining)
├─ 2. ClassifyDocument × 3 (fan-out / fan-in)
│ ├─ Sentiment
│ ├─ Topic
│ └─ Priority
└─ 3. Assemble result string ──▶ return to Client
```

**Key patterns demonstrated:**

- **Activity chaining** — `ValidateDocument` must pass before classification begins.
- **Fan-out / fan-in** — Three `ClassifyDocument` activities run in parallel (Sentiment, Topic, Priority) and the orchestration awaits all three.
- **Client scheduling** — `ScheduleNewOrchestrationInstanceAsync` + `WaitForInstanceCompletionAsync` for fire-and-wait semantics.
- **Workload Identity** — In AKS, pods authenticate to DTS using federated credentials on a user-assigned managed identity (no secrets stored).

Both Client and Worker auto-detect the environment: locally they connect to `http://localhost:8080` with no auth; in AKS they use the DTS endpoint with managed-identity auth via the `ENDPOINT`, `TASKHUB`, and `AZURE_CLIENT_ID` environment variables injected by the Kubernetes manifests.

## Project Structure

```
├── azure.yaml # azd service definitions (host: aks)
├── scripts/acr-build.sh # Predeploy hook — builds images via ACR Tasks
├── Client/
│ ├── Program.cs # Submits 3 documents, waits for results
│ ├── Models/DocumentInfo.cs # Input record
│ ├── Dockerfile
│ └── manifests/deployment.tmpl.yaml # K8s deployment + service account
├── Worker/
│ ├── Program.cs # Registers orchestrations & activities
│ ├── Orchestrations/
│ │ └── DocumentProcessingOrchestration.cs
│ ├── Activities/
│ │ ├── ValidateDocument.cs # Checks title + content (returns bool)
│ │ └── ClassifyDocument.cs # Stub classifier (called 3× in parallel)
│ ├── Models/DocumentModels.cs # DocumentInfo, ClassifyRequest, ClassificationResult
│ ├── Dockerfile
│ └── manifests/deployment.tmpl.yaml # K8s deployment (2 replicas) + service account
└── infra/ # Bicep modules: AKS, ACR, VNet, DTS, identity, RBAC
```

## Prerequisites

- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- [Docker Desktop](https://www.docker.com/products/docker-desktop/) — for the local DTS emulator
- [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli) & [Azure Developer CLI (`azd`)](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) — for Azure deployment
- [kubectl](https://kubernetes.io/docs/tasks/tools/) — for verifying the AKS deployment

## Run Locally

### 1. Start the DTS emulator

```bash
docker run -d --name dts-emulator -p 8080:8080 -p 8082:8082 \
mcr.microsoft.com/dts/dts-emulator:latest
```

- Port **8080** — gRPC endpoint (used by Client and Worker)
- Port **8082** — [Dashboard UI](http://localhost:8082) for inspecting orchestrations

### 2. Build the solution

```bash
dotnet build DurableTaskOnAKS.sln
```

### 3. Start the Worker

```bash
cd Worker && dotnet run
```

Wait until you see `Sidecar work-item streaming connection established.`

### 4. Start the Client (in a separate terminal)

```bash
cd Client && dotnet run
```

### 5. Expected output

```
Endpoint: http://localhost:8080 | TaskHub: default
Submitting 3 documents...

Scheduled [abc123] 'Cloud Migration Strategy'
-> Processed 'Cloud Migration Strategy': Sentiment=Positive, Topic=Technology, Priority=Normal

Scheduled [def456] 'Quarterly Incident Report'
-> Processed 'Quarterly Incident Report': Sentiment=Positive, Topic=Technology, Priority=Normal

Scheduled [ghi789] 'ML Model Evaluation'
-> Processed 'ML Model Evaluation': Sentiment=Positive, Topic=Technology, Priority=Normal

Done.
```

### 6. Clean up

```bash
docker stop dts-emulator && docker rm dts-emulator
```

## Deploy to Azure

### 1. Provision and deploy

```bash
azd auth login && az login
azd up
```

This provisions all required Azure resources via Bicep (~5–10 min):

| Resource | Purpose |
|----------|---------|
| **AKS cluster** | Hosts Client (1 replica) and Worker (2 replicas) pods |
| **Azure Container Registry** | Stores Docker images (built server-side via ACR Tasks) |
| **Durable Task Scheduler** (Consumption SKU) | Managed orchestration backend |
| **VNet** | Network isolation for AKS |
| **User-assigned managed identity** + federated credentials | Workload Identity auth from pods to DTS |

### 2. Verify the deployment

```bash
# Get AKS credentials
az aks get-credentials --resource-group <rg-name> --name <aks-name>

# Check pods are running (1 client + 2 workers)
kubectl get pods

# View client output (orchestration results)
kubectl logs -l app=client --tail=30

# View worker logs (activity execution)
kubectl logs -l app=worker --tail=30
```

You can find the resource group and cluster names from `azd env get-values`.

Expected client log output:

```
Submitting 3 documents...

Scheduled [...] 'Cloud Migration Strategy'
-> Processed 'Cloud Migration Strategy': Sentiment=Positive, Topic=Technology, Priority=Normal

Scheduled [...] 'Quarterly Incident Report'
-> Processed 'Quarterly Incident Report': Sentiment=Positive, Topic=Technology, Priority=Normal

Scheduled [...] 'ML Model Evaluation'
-> Processed 'ML Model Evaluation': Sentiment=Positive, Topic=Technology, Priority=Normal

Done.
```

You can also view orchestration history in the **Azure Portal → Durable Task Scheduler → Task Hub** dashboard.

## Cleanup

```bash
azd down
```

## Resources

- [Durable Task Scheduler documentation](https://learn.microsoft.com/en-us/azure/durable-task-scheduler/)
- [.NET Durable Task SDK (NuGet)](https://www.nuget.org/packages/Microsoft.DurableTask.Client.AzureManaged)
- [AKS Workload Identity](https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview)
Loading
Loading