Deploy Orleans to Azure App Service
In this tutorial, you learn how to deploy an Orleans shopping cart app to Azure App Service. The tutorial walks you through a sample application that supports the following features:
Shopping cart: A simple shopping cart application that uses Orleans for its cross-platform framework support, and its scalable distributed applications capabilities.
- Inventory management: Edit and/or create product inventory.
- Shop inventory: Explore purchasable products and add them to your cart.
- Cart: View a summary of all the items in your cart, and manage these items; either removing or changing the quantity of each item.
With an understanding of the app and its features, you will then learn how to deploy the app to Azure App Service using GitHub Actions, the .NET and Azure CLIs, and Azure Bicep. Additionally, you'll learn how to configure the virtual network for the app within Azure.
In this tutorial, you learn how to:
- Deploy an Orleans application to Azure App Service
- Automate deployment using GitHub Actions and Azure Bicep
- Configure the virtual network for the app within Azure
Prerequisites
- A GitHub account
- Read an introduction to Orleans
- The .NET 8 SDK
- The Azure CLI
- A .NET integrated development environment (IDE)
- Feel free to use Visual Studio or Visual Studio Code
Run the app locally
To run the app locally, fork the Azure Samples: Orleans Cluster on Azure App Service repository and clone it to your local machine. Once cloned, open the solution in an IDE of your choice. If you're using Visual Studio, right-click the Orleans.ShoppingCart.Silo project and select Set As Startup Project, then run the app. Otherwise, you can run the app using the following .NET CLI command:
dotnet run --project Silo\Orleans.ShoppingCart.Silo.csproj
For more information, see dotnet run. With the app running, you can navigate around and you're free to test out its capabilities. All of the app's functionality when running locally relies on in-memory persistence, local clustering, and it uses the Bogus NuGet package to generate fake products. Stop the app either by selecting the Stop Debugging option in Visual Studio or by pressing Ctrl+C in the .NET CLI.
Inside the shopping cart app
Orleans is a reliable and scalable framework for building distributed applications. For this tutorial, you will deploy a simple shopping cart app built using Orleans to Azure App Service. The app exposes the ability to manage inventory, add and remove items in a cart, and shop available products. The client is built using Blazor with a server hosting model. The app is architected as follows:
The preceding diagram shows that the client is the server-side Blazor app. It's composed of several services that consume a corresponding Orleans grain. Each service pairs with an Orleans grain as follows:
InventoryService
: Consumes theIInventoryGrain
where inventory is partitioned by product category.ProductService
: Consumes theIProductGrain
where a single product is tethered to a single grain instance byId
.ShoppingCartService
: Consumes theIShoppingCartGrain
where a single user only has a single shopping cart instance regardless of consuming clients.
The solution contains three projects:
Orleans.ShoppingCart.Abstractions
: A class library that defines the models and the interfaces for the app.Orleans.ShoppingCart.Grains
: A class library that defines the grains that implement the app's business logic.Orleans.ShoppingCart.Silos
: A server-side Blazor app that hosts the Orleans silo.
The client user experience
The shopping cart client app has several pages, each of which represents a different user experience. The app's UI is built using the MudBlazor NuGet package.
Home page
A few simple phrases for the user to understand the app's purpose, and add context to each navigation menu item.
Shop inventory page
A page that displays all of the products that are available for purchase. Items can be added to the cart from this page.
Empty cart page
When you haven't added anything to your cart, the page renders a message that indicates that you have no items in your cart.
Items added to the cart while on the shop inventory page
When items are added to your cart while on the shop inventory page, the app displays a message that indicates the item was added to the cart.
Product management page
A user can manage inventory from this page. Products can be added, edited, and removed from the inventory.
Product management page create new dialog
When a user clicks the Create new product button, the app displays a dialog that allows the user to create a new product.
Items in the cart page
When items are in your cart, you can view them and change their quantity, and even remove them from the cart. The user is shown a summary of the items in the cart and the pretax total cost.
Important
When this app runs locally, in a development environment, the app will use localhost clustering, in-memory storage, and a local silo. It also seeds the inventory with fake data that is automatically generated using the Bogus NuGet package. This is all intentional to demonstrate the functionality.
Deployment overview
Orleans applications are designed to scale up and scale out efficiently. To accomplish this, instances of your application communicate directly with each other via TCP sockets and therefore Orleans requires network connectivity between silos. Azure App Service supports this requirement via virtual network integration and additional configuration instructing App Service to allocate private network ports for your app instances.
When deploying Orleans to Azure App Service, we need to take the following actions to ensure that hosts can communicate with eachother:
- Enable virtual network integration by following the Enable integration with an Azure virtual network guide.
- Configure your app with private ports using the Azure CLI as described in the Configure private port count using Azure CLI section. The Bicep template in the Explore the Bicep templates section below shows how to configure the setting via Bicep.
- If deploying to Linux, ensure that your hosts are listening on all IP addresses as described in the Configure host networking section.
Configure private port count using Azure CLI
az webapp config set -g '<resource-group-name>' --subscription '<subscription-id>' -n '<app-service-app-name>' --generic-configurations '{\"vnetPrivatePortsCount\": "2"}'
Configure host networking
Once Azure App Service has been configured with virtual network (VNet) integration and configured to provide application instances with at least 2 private ports each, two additional environment variables will be provided to your app processes: WEBSITE_PRIVATE_IP
and WEBSITE_PRIVATE_PORTS
. These variables provide two important pieces of information:
- Which IP address other hosts in your virtual network can use to contact a given app instance; and
- Which ports on that IP address will be routed to that app instance
The WEBSITE_PRIVATE_IP
variable specifies an IP which is routable from the VNet, but not necessarily an IP address which your app instance can directly bind to. For this reason, you should instruct your host to bind to all internal addresses by passing listenOnAnyHostAddress: true
to the ConfigureEndpoints
method call, as in the following example which configures an ISiloBuilder
instance to consume the injected environment variables and to listen on the correct interfaces:
var endpointAddress = IPAddress.Parse(builder.Configuration["WEBSITE_PRIVATE_IP"]!);
var strPorts = builder.Configuration["WEBSITE_PRIVATE_PORTS"]!.Split(',');
if (strPorts.Length < 2)
{
throw new Exception("Insufficient private ports configured.");
}
var (siloPort, gatewayPort) = (int.Parse(strPorts[0]), int.Parse(strPorts[1]));
siloBuilder
.ConfigureEndpoints(endpointAddress, siloPort, gatewayPort, listenOnAnyHostAddress: true)
The above code is present in the Azure Samples: Orleans Cluster on Azure App Service repository, too, so you can see it in the context of the rest of the host configuration.
Deploy to Azure App Service
A typical Orleans application consists of a cluster of server processes (silos) where grains live, and a set of client processes, usually web servers, that receive external requests, turn them into grain method calls and return results. Hence, the first thing one needs to do to run an Orleans application is to start a cluster of silos. For testing purposes, a cluster can consist of a single silo.
Note
For a reliable production deployment, you'd want more than one silo in a cluster for fault tolerance and scale.
Before deploying the app, you need to create an Azure Resource Group (or you could choose to use an existing one). To create a new Azure Resource Group, use one of the following articles:
Make note of the resource group name you choose, you'll need it later to deploy the app.
Create a service principal
To automate the deployment of the app, you'll need to create a service principal. This is a Microsoft account that has permission to manage Azure resources on your behalf.
az ad sp create-for-rbac --sdk-auth --role Contributor \
--name "<display-name>" --scopes /subscriptions/<your-subscription-id>
The JSON credentials created will look similar to the following, but with actual values for your client, subscription, and tenant:
{
"clientId": "<your client id>",
"clientSecret": "<your client secret>",
"subscriptionId": "<your subscription id>",
"tenantId": "<your tenant id>",
"activeDirectoryEndpointUrl": "https://login.microsoftonline.com/",
"resourceManagerEndpointUrl": "https://brazilus.management.azure.com",
"activeDirectoryGraphResourceId": "https://graph.windows.net/",
"sqlManagementEndpointUrl": "https://management.core.windows.net:8443/",
"galleryEndpointUrl": "https://gallery.azure.com",
"managementEndpointUrl": "https://management.core.windows.net"
}
Copy the output of the command into your clipboard, and continue to the next step.
Create a GitHub secret
GitHub provides a mechanism for creating encrypted secrets. The secrets that you create are available to use in GitHub Actions workflows. You're going to see how GitHub Actions can be used to automate the deployment of the app, in conjunction with Azure Bicep. Bicep is a domain-specific language (DSL) that uses a declarative syntax to deploy Azure resources. For more information, see What is Bicep. Using the output from the Create a service principal step, you'll need to create a GitHub secret named AZURE_CREDENTIALS
with the JSON-formatted credentials.
Within the GitHub repository, select Settings > Secrets > Create a new secret. Enter the name AZURE_CREDENTIALS
and paste the JSON credentials from the previous step into the Value field.
For more information, see GitHub: Encrypted Secrets.
Prepare for Azure deployment
The app will need to be packaged for deployment. In the Orleans.ShoppingCart.Silos
project we define a Target
element that runs after the Publish
step. This will zip the publish directory into a silo.zip file:
<Target Name="ZipPublishOutput" AfterTargets="Publish">
<Delete Files="$(ProjectDir)\..\silo.zip" />
<ZipDirectory SourceDirectory="$(PublishDir)" DestinationFile="$(ProjectDir)\..\silo.zip" />
</Target>
There are many ways to deploy a .NET app to Azure App Service. In this tutorial, you use GitHub Actions, Azure Bicep, and the .NET and Azure CLIs. Consider the ./github/workflows/deploy.yml file in the root of the GitHub repository:
name: Deploy to Azure App Service
on:
push:
branches:
- main
env:
UNIQUE_APP_NAME: cartify
AZURE_RESOURCE_GROUP_NAME: orleans-resourcegroup
AZURE_RESOURCE_GROUP_LOCATION: centralus
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET 8.0
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: .NET publish shopping cart app
run: dotnet publish ./Silo/Orleans.ShoppingCart.Silo.csproj --configuration Release
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Flex bicep
run: |
az deployment group create \
--resource-group ${{ env.AZURE_RESOURCE_GROUP_NAME }} \
--template-file '.github/workflows/flex/main.bicep' \
--parameters location=${{ env.AZURE_RESOURCE_GROUP_LOCATION }} \
appName=${{ env.UNIQUE_APP_NAME }} \
--debug
- name: Webapp deploy
run: |
az webapp deploy --name ${{ env.UNIQUE_APP_NAME }} \
--resource-group ${{ env.AZURE_RESOURCE_GROUP_NAME }} \
--clean true --restart true \
--type zip --src-path silo.zip --debug
- name: Staging deploy
run: |
az webapp deploy --name ${{ env.UNIQUE_APP_NAME }} \
--slot ${{ env.UNIQUE_APP_NAME }}stg \
--resource-group ${{ env.AZURE_RESOURCE_GROUP_NAME }} \
--clean true --restart true \
--type zip --src-path silo.zip --debug
The preceding GitHub workflow will:
- Publish the shopping cart app as a zip file, using the dotnet publish command.
- Login to Azure using the credentials from the Create a service principal step.
- Evaluate the main.bicep file and start a deployment group using az deployment group create.
- Deploy the silo.zip file to Azure App Service using az webapp deploy.
- An additional deployment to staging is also configured.
The workflow is triggered by a push to the main branch. For more information, see GitHub Actions and .NET.
Tip
If you encounter issues when running the workflow, you might need to verify that the service principal has all the required provider namespaces registered. The following provider namespaces are required:
Microsoft.Web
Microsoft.Network
Microsoft.OperationalInsights
Microsoft.Insights
Microsoft.Storage
For more information, see Resolve errors for resource provider registration.
Azure imposes naming restrictions and conventions for resources. You need to update the deploy.yml file values for the following:
UNIQUE_APP_NAME
AZURE_RESOURCE_GROUP_NAME
AZURE_RESOURCE_GROUP_LOCATION
Set these values to your unique app name and your Azure resource group name and location.
For more information, see Naming rules and restrictions for Azure resources.
Explore the Bicep templates
When the az deployment group create
command is run, it will evaluate the main.bicep file. This file contains the Azure resources that you want to deploy. One way to think of this step is that it provisions all of the resources for deployment.
Important
If you're using Visual Studio Code, the bicep authoring experience is improved when using the Bicep Extension.
There are many bicep files, each containing either resources or modules (collections of resources). The main.bicep file is the entry point and is comprised primarily of module
definitions:
param appName string
param location string = resourceGroup().location
module storageModule 'storage.bicep' = {
name: 'orleansStorageModule'
params: {
name: '${appName}storage'
location: location
}
}
module logsModule 'logs-and-insights.bicep' = {
name: 'orleansLogModule'
params: {
operationalInsightsName: '${appName}-logs'
appInsightsName: '${appName}-insights'
location: location
}
}
resource vnet 'Microsoft.Network/virtualNetworks@2021-05-01' = {
name: '${appName}-vnet'
location: location
properties: {
addressSpace: {
addressPrefixes: [
'172.17.0.0/16',
'192.168.0.0/16'
]
}
subnets: [
{
name: 'default'
properties: {
addressPrefix: '172.17.0.0/24'
delegations: [
{
name: 'delegation'
properties: {
serviceName: 'Microsoft.Web/serverFarms'
}
}
]
}
}
{
name: 'staging'
properties: {
addressPrefix: '192.168.0.0/24'
delegations: [
{
name: 'delegation'
properties: {
serviceName: 'Microsoft.Web/serverFarms'
}
}
]
}
}
]
}
}
module siloModule 'app-service.bicep' = {
name: 'orleansSiloModule'
params: {
appName: appName
location: location
vnetSubnetId: vnet.properties.subnets[0].id
stagingSubnetId: vnet.properties.subnets[1].id
appInsightsConnectionString: logsModule.outputs.appInsightsConnectionString
appInsightsInstrumentationKey: logsModule.outputs.appInsightsInstrumentationKey
storageConnectionString: storageModule.outputs.connectionString
}
}
The preceding bicep file defines the following:
- Two parameters for the resource group name and the app name.
- The
storageModule
definition, which defines the storage account. - The
logsModule
definition, which defines the Azure Log Analytics and Application Insights resources. - The
vnet
resource, which defines the virtual network. - The
siloModule
definition, which defines the Azure App Service.
One very important resource
is that of the Virtual Network. The vnet
resource enables the Azure App Service to communicate with the Orleans cluster.
Whenever a module
is encountered in the bicep file, it is evaluated via another bicep file that contains the resource definitions. The first encountered module was the storageModule
, which is defined in the storage.bicep file:
param name string
param location string
resource storage 'Microsoft.Storage/storageAccounts@2021-08-01' = {
name: name
location: location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}
var key = listKeys(storage.name, storage.apiVersion).keys[0].value
var protocol = 'DefaultEndpointsProtocol=https'
var accountBits = 'AccountName=${storage.name};AccountKey=${key}'
var endpointSuffix = 'EndpointSuffix=${environment().suffixes.storage}'
output connectionString string = '${protocol};${accountBits};${endpointSuffix}'
Bicep files accept parameters, which are declared using the param
keyword. Likewise, they can also declare outputs using the output
keyword. The storage resource
relies on the Microsoft.Storage/storageAccounts@2021-08-01
type and version. It will be provisioned in the resource group's location, as a StorageV2
and Standard_LRS
SKU. The storage bicep defines its connection string as an output
. This connectionString
is later used by the silo bicep to connect to the storage account.
Next, the logs-and-insights.bicep file defines the Azure Log Analytics and Application Insights resources:
param operationalInsightsName string
param appInsightsName string
param location string
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
name: appInsightsName
location: location
kind: 'web'
properties: {
Application_Type: 'web'
WorkspaceResourceId: logs.id
}
}
resource logs 'Microsoft.OperationalInsights/workspaces@2021-06-01' = {
name: operationalInsightsName
location: location
properties: {
retentionInDays: 30
features: {
searchVersion: 1
}
sku: {
name: 'PerGB2018'
}
}
}
output appInsightsInstrumentationKey string = appInsights.properties.InstrumentationKey
output appInsightsConnectionString string = appInsights.properties.ConnectionString
This bicep file defines the Azure Log Analytics and Application Insights resources. The appInsights
resource is a web
type, and the logs
resource is a PerGB2018
type. Both the appInsights
resource and the logs
resource are provisioned in the resource group's location. The appInsights
resource is linked to the logs
resource via the WorkspaceResourceId
property. There are two outputs defined in this bicep, used later by the App Service module
.
Finally, the app-service.bicep file defines the Azure App Service resource:
param appName string
param location string
param vnetSubnetId string
param stagingSubnetId string
param appInsightsInstrumentationKey string
param appInsightsConnectionString string
param storageConnectionString string
resource appServicePlan 'Microsoft.Web/serverfarms@2021-03-01' = {
name: '${appName}-plan'
location: location
kind: 'app'
sku: {
name: 'S1'
capacity: 1
}
}
resource appService 'Microsoft.Web/sites@2021-03-01' = {
name: appName
location: location
kind: 'app'
properties: {
serverFarmId: appServicePlan.id
virtualNetworkSubnetId: vnetSubnetId
httpsOnly: true
siteConfig: {
vnetPrivatePortsCount: 2
webSocketsEnabled: true
netFrameworkVersion: 'v8.0'
appSettings: [
{
name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
value: appInsightsInstrumentationKey
}
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: appInsightsConnectionString
}
{
name: 'ORLEANS_AZURE_STORAGE_CONNECTION_STRING'
value: storageConnectionString
}
{
name: 'ORLEANS_CLUSTER_ID'
value: 'Default'
}
]
alwaysOn: true
}
}
}
resource stagingSlot 'Microsoft.Web/sites/slots@2022-03-01' = {
name: '${appName}stg'
location: location
properties: {
serverFarmId: appServicePlan.id
virtualNetworkSubnetId: stagingSubnetId
siteConfig: {
http20Enabled: true
vnetPrivatePortsCount: 2
webSocketsEnabled: true
netFrameworkVersion: 'v8.0'
appSettings: [
{
name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
value: appInsightsInstrumentationKey
}
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: appInsightsConnectionString
}
{
name: 'ORLEANS_AZURE_STORAGE_CONNECTION_STRING'
value: storageConnectionString
}
{
name: 'ORLEANS_CLUSTER_ID'
value: 'Staging'
}
]
alwaysOn: true
}
}
}
resource slotConfig 'Microsoft.Web/sites/config@2021-03-01' = {
name: 'slotConfigNames'
parent: appService
properties: {
appSettingNames: [
'ORLEANS_CLUSTER_ID'
]
}
}
resource appServiceConfig 'Microsoft.Web/sites/config@2021-03-01' = {
parent: appService
name: 'metadata'
properties: {
CURRENT_STACK: 'dotnet'
}
}
This bicep file configures the Azure App Service as a .NET 8 application. Both the appServicePlan
resource and the appService
resource are provisioned in the resource group's location. The appService
resource is configured to use the S1
SKU, with a capacity of 1
. Additionally, the resource is configured to use the vnetSubnetId
subnet and to use HTTPS. It also configures the appInsightsInstrumentationKey
instrumentation key, the appInsightsConnectionString
connection string, and the storageConnectionString
connection string. These are used by the shopping cart app.
The aforementioned Visual Studio Code extension for Bicep includes a visualizer. All of these bicep files are visualized as follows:
Staging environments
The deployment infrastructure can deploy to staging environments, which are short-lived, test-centric, and immutable throwaway environments. These environments are very helpful for testing deployments before promoting them to production.
Note
If your App Service is running on Windows, each App Service must be on its own separate App Service Plan. Alternatively, to avoid such configuration, you could instead use App Service on Linux, and this problem would be resolved.
Summary
As you update the source code and push
changes to the main
branch of the repository, the deploy.yml workflow will run. It will provide the resources defined in the bicep files and deploy the application. The application can be expanded upon to include new features, such as authentication, or to support multiple instances of the application. The primary objective of this workflow is to demonstrate the ability to provision and deploy resources in a single step.
In addition to the visualizer from the bicep extension, the Azure portal resource group page would look similar to the following example after provisioning and deploying the application: