The goal of this project is to demonstrate a pragmatic approach to developing and hosting an actor-based application using ASP.NET Core, Azure App Service + WebJobs, Container Instances, and Orleans.
The codebase is separated into two parts:
-
/base contains the core frontend and backend implementation, but no application-specific code. The Docker containers created from this code can be re-used across multiple solution stacks.
-
/app contains a simple example app built upon the foundation code in /base. Specifically it contains an actor implementation and ASP.NET Core controller and webjob listener code that invokes the actor.
At runtime, the application-specific code is injected into the runtime host processes using dynamic assembly loading. This eliminates the need for static .NET dependencies and allows solution-specific code to be developed and tested independently from the core runtime.
- API- and event-based abstractions between actors and the external world (no direct access to actors)
- logical and physical separation between actor host and actor abstractions... allow each to be configured and scale independently
- logical separation between infrastructure code and solution code... maximize app developer productivity by hiding infra details as much as possible
- allow all infrastructure knobs to be twisted, if/when desired (ACI or Orleans config, etc.)
- run and debug the full solution stack on your laptop (no Azure needed)
- single command build and deploy of the full solution stack to Azure, using lightweight PaaS services where possible
A major drawback of most actor-based systems is the need to manage infrastructure. Durable Entities solves this problem by layering an actor implementation on top of serverless Azure Functions; however, the DE actor implementation is rudimentary compared to other options and there are relatively few "knobs to twist" to optimize performance/scale for specific scenarios.
Azure Container Instances provide a simple mechanism to run containers in Azure, without the overhead and management burden of a full-blown Kubernetes orchestrator underneath. You can configure VM size, vnet integration, etc. for running containers.
One drawback of ACI is the lack of an autoscale mechanism (something AKS does well). This project addresses this by hooking alerts surfaced from Azure Monitor metrics. Alert rules are defined for CPU min/max thresholds, as well as webhook-based actions that execute when rule conditions are met; the webhook implementations add or remove ACI instances as needed to ensure adequate resources for in-flight, actor-based workloads.
Azure Kubernetes Service is a great option for hosting elastically scaled infrastructure like actors. However, there are project and organizational contexts in which AKS and Kubernetes more generally represent an undesirable management burden; this project is an attempt to explore other options. That said, AKS + a bit of config would make a perfectly reasonable host for this solution.
Azure Functions have built-in support for event-based triggers, and are an obvious choice to model both APIs and event-driven endpoints. The issue with Functions is that they (by design) provide limited ability to configure the underlying host, which interferes with the ability to inject startup and configuration needed for communication with remote Orleans clusters.
An alternative is to use the WebJobs runtime and SDK directly. Functions are built on top of WebJobs; the trigger mechanisms in Functions are defined in terms of WebJobs primitives and can be used from WebJobs code. In addition, WebJobs allow full configuration of the host process and are therefore a good fit here.
https://www.brianstorti.com/the-actor-model/
There are many available actor implementations:
- Akka and Akka.NET
- Service Fabric actors
- Dapr actors
- languages like Scala and Erlang
Orleans is a .NET-based implementation of the virtual actor pattern. It began as an incubation project in Microsoft Research, and has been developed, deployed, and continuously improved for over 10 years. It serves as the foundation for a number of high-scale cloud architectures both within and outside Microsoft.
In addition, Orleans has:
- a full .NET Core implementation
- a vibrant, community-oriented extensibility ecosystem
- full integration with ASP.NET Core and generic .NET Core hosts
- support for journaling and event sourcing
More info on Orleans here
- Docker (use this if you run WSL2 on Windows)
- Visual Studio Code
- VS Code Docker extension
- VS Code C# extension
- .NET Core 3.x SDK
- Azure Storage Explorer
- Postman
- Terraform
-
Run create-service-principal.sh to generate an Azure service principal used to add/remove ACI instances during autoscale events
-
Run build-base-images.sh to create the base Docker images for solution frontend (API, event listeners, and dashboards) and backend (actor host)
-
Right-click docker-compose.yml and select 'Compose Up'. This will build and launch the containers defined in the YAML file
-
Set breakpoints as desired
-
Launch the VS Code debugger with F5. VS Code will attach to the 'frontend' container with a remote debugger session
-
If desired, open http://localhost:5000/dashboard in your browser to see the Orleans telemetry dashboard
-
Using Storage Explorer, connect to Azurite emulated storage. Create a new queue called 'input' and add the following JSON payload:
{ "actorId": 5, "message": "hello world" }
-
Using Postman or curl, send an HTTP GET request to http://localhost:5000/messages/5 to verify the queue message was processed successfully
-
When finished, right-click docker-compose.yml and select 'Compose Down'
- Log into the Azure CLI
- Initialize and run terraform to create the Azure resources defined in deploy.tf:
terraform init terraform apply
- Event-sourced actor state
- Scalability and load testing (multiple ACI nodes, etc.)
- Node.js and/or Python interop (define actor logic in Node or Python, with a common .NET foundation)