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

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:

Orleans: Shopping Cart sample app architecture.

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 the IInventoryGrain where inventory is partitioned by product category.
  • ProductService: Consumes the IProductGrain where a single product is tethered to a single grain instance by Id.
  • ShoppingCartService: Consumes the IShoppingCartGrain 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.

Orleans: Shopping Cart sample app, home page.

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.

Orleans: Shopping Cart sample app, shop inventory 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.

Orleans: Shopping Cart sample app, empty cart page.

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.

Orleans: Shopping Cart sample app, items added to cart while on shop inventory page.

Product management page

A user can manage inventory from this page. Products can be added, edited, and removed from the inventory.

Orleans: Shopping Cart sample app, product management page.

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.

Orleans: Shopping Cart sample app, product management page - create new product dialog.

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.

Orleans: Shopping Cart sample app, items in cart page.

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:

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.

GitHub Repository: Settings > Secrets

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:

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:

Orleans: Shopping cart sample app bicep provisioning visualizer rendering.

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:

Azure Portal: Orleans shopping cart sample app resources.

See also