将 Orleans 部署到 Azure 应用服务

本教程介绍如何将 Orleans 购物车应用部署到 Azure 应用服务。 本指南逐步讲解支持以下功能的示例应用程序:

  • 购物车:一个简单的购物车应用程序,利用Orleans的跨平台框架支持和可扩展的分布式应用程序功能。

    • 库存管理:编辑和/或创建产品库存。
    • 商店库存:探索可购买的产品并将其添加到购物车。
    • 购物车:通过删除或更改每个商品的数量来查看购物车中所有项目的摘要并管理这些项目。

了解应用及其功能后,了解如何使用 GitHub Actions、.NET 和 Azure CLIs 和 Azure Bicep 将应用部署到 Azure 应用服务。 此外,了解如何在 Azure 中为应用配置虚拟网络。

本教程中,您将学习如何:

  • 将 Orleans 应用程序部署到 Azure 应用服务
  • 使用 GitHub Actions 和 Azure Bicep 自动部署
  • 在 Azure 中为应用配置虚拟网络

先决条件

在本地运行应用

若要在本地运行应用,请分叉 Azure 示例: Orleans Azure 应用服务存储库上的群集 并将其克隆到本地计算机。 克隆后,在所选的 IDE 中打开解决方案。 如果使用 Visual Studio,请右键单击 。OrleansShoppingCart.Silo 项目,选择“设为启动项目”,然后运行应用。 否则,请使用以下 .NET CLI 命令运行应用:

dotnet run --project Silo\Orleans.ShoppingCart.Silo.csproj

有关详细信息,请参阅 dotnet run。 运行应用后,导航并测试其功能。 在本地运行时的所有应用功能都依赖于内存中持久性和本地群集。 它还使用 Bogus NuGet 包生成假冒产品。 通过在 Visual Studio 中选择 “停止调试 ”选项或在 .NET CLI 中按 Ctrl+C 来停止应用。

在购物车应用中

Orleans 是用于生成分布式应用程序的可靠且可缩放的框架。 对于本教程,请部署一个简单的购物车应用,该应用是使用 Orleans Azure 应用服务生成的。 该应用公开了管理库存、在购物车中添加和删除商品以及购买可用产品的功能。 客户端是使用 Blazor 和服务器托管模型生成的。 应用架构如下:

Orleans:购物车示例应用体系结构。

上图显示客户端是服务器端 Blazor 应用。 它由使用相应 Orleans 粒度的多个服务组成。 每个服务与一个Orleans粒配对,如下所示:

  • InventoryService:消耗 IInventoryGrain 库存按产品类别进行分区的情况。
  • ProductService:消耗 IProductGrain,即单个产品通过 Id 连接到单个粒度实例。
  • ShoppingCartService:消耗 IShoppingCartGrain,在这种情况下,每个用户只拥有一个购物车实例,无论使用的客户端如何。

该解决方案包含三个项目:

  • Orleans.ShoppingCart.Abstractions:定义应用的模型和接口的类库。
  • Orleans.ShoppingCart.Grains:一个类库,定义实现应用业务逻辑的粒子。
  • Orleans.ShoppingCart.Silos:托管 Orleans silo 的服务器端 Blazor 应用。

客户端用户体验

购物车客户端应用有多个页面,每个页面都表示不同的用户体验。 应用的 UI 是使用 MudBlazor NuGet 包生成的。

主页

几个简单的短语有助于了解应用的用途,并将上下文添加到每个导航菜单项。

Orleans:购物车示例应用,主页。

商店库存页

显示所有可供购买的产品的页面。 可以从此页面将项目添加到购物车。

Orleans:购物车示例应用,商店库存页面。

空购物车页

如果购物车中未添加任何内容,页面将显示一条消息,指示购物车中没有项目。

Orleans:购物车示例应用,空购物车页面。

在商店库存页面上添加到购物车的项目

当商品添加到购物车时,在商店库存页面上,应用会显示一条消息,指示已添加该项目。

Orleans:购物车示例应用,在商店库存页面上添加到购物车的项目。

产品管理页

从此页面管理清单。 产品可从库存中添加、编辑和删除。

Orleans:购物车示例应用,产品管理页面。

“产品管理”页“新建”对话框

单击“ 创建新产品 ”按钮将显示允许创建新产品的对话框。

Orleans:购物车示例应用,产品管理页面 - 创建新产品对话框。

购物车页中的项目

当商品在购物车中时,查看它们、更改其数量,甚至将其删除。 显示购物车中商品和税前总成本的概览。

Orleans:购物车示例应用,购物车页面中的项目。

重要

当此应用在开发环境中运行时,它使用 localhost 群集、内存存储和本地孤立空间。 它还通过使用 Bogus NuGet 包自动生成的假数据来填充库存。 这是有意演示功能。

部署概述

Orleans 应用程序旨在高效地纵向扩展和横向扩展。 为此,应用程序实例直接通过 TCP 套接字进行通信。 因此, Orleans 需要在孤岛之间建立网络连接。 Azure 应用服务通过 虚拟网络集成 和其他配置支持此要求,指示应用服务为应用实例分配专用网络端口。

部署到 Orleans Azure 应用服务时,执行以下操作以确保主机可以通信:

使用 Azure CLI 配置专用端口计数

az webapp config set -g '<resource-group-name>' --subscription '<subscription-id>' -n '<app-service-app-name>' --generic-configurations '{\"vnetPrivatePortsCount\": "2"}'

配置主机网络

使用虚拟网络(VNet)集成配置 Azure 应用服务并将其设置为为每个应用程序实例提供至少两个专用端口后,向应用进程提供另外两个环境变量: WEBSITE_PRIVATE_IPWEBSITE_PRIVATE_PORTS 这些变量提供两个重要信息:

  • 在虚拟网络中,其他主机可以使用哪个 IP 地址来联系指定的应用实例。
  • 该 IP 地址上的哪些端口将路由到该应用实例

WEBSITE_PRIVATE_IP 变量指定从 VNet 路由的 IP,但不一定是应用实例可以直接绑定到的 IP 地址。 因此,请指示主机通过在ConfigureEndpoints方法调用中传递listenOnAnyHostAddress: true来绑定所有内部地址。 以下示例将 ISiloBuilder 实例配置为利用注入的环境变量,并监听正确的接口:

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)

上述代码也存在于 Azure 示例: Orleans Azure 应用服务存储库上的群集 中,允许在主机配置其余部分的上下文中查看它。

部署到 Azure 应用服务

典型的 Orleans 应用程序由一组服务器进程(孤岛)组成,其中包含谷物,以及一组客户端进程(通常是 Web 服务器),负责接收外部请求,将其转换为谷物方法调用,并返回结果。 因此,运行 Orleans 应用程序的第一步是启动孤岛群集。 出于测试目的,群集可以包含单个孤岛。

注释

对于可靠的生产部署,需要在群集内有多个存储孤岛,以提高容错能力和扩展性。

在部署应用之前,请创建 Azure 资源组(也可以使用现有资源组)。 若要创建新的 Azure 资源组,请使用以下文章之一:

记下所选的资源组名称;稍后需要它来部署应用。

创建服务主体

若要自动执行应用的部署,需要创建服务主体。 这是一个Microsoft帐户,有权代表你管理 Azure 资源。

az ad sp create-for-rbac --sdk-auth --role Contributor \
  --name "<display-name>"  --scopes /subscriptions/<your-subscription-id>

创建的 JSON 凭据类似于以下内容,但客户端、订阅和租户的实际值如下:

{
  "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"
}

将命令的输出复制到剪贴板,并继续执行下一步。

创建 GitHub 机密

GitHub 提供了用于创建加密机密的机制。 创建的机密可用于 GitHub Actions 工作流。 你将了解如何使用 GitHub Actions 配合 Azure Bicep 自动化应用的部署。 Bicep 是一种特定于域的语言 (DSL),使用声明性语法来部署 Azure 资源。 有关详细信息,请参阅什么是 Bicep?。 使用创建服务主体步骤的输出,你需要用 JSON 格式的凭据创建一个名为AZURE_CREDENTIALS的 GitHub 密钥。

在 GitHub 存储库中,选择 “设置>机密>创建新机密”。 输入名称 AZURE_CREDENTIALS 并将上一步中的 JSON 凭据粘贴到 “值 ”字段中。

GitHub 存储库:设置 > 机密

有关详细信息,请参阅 GitHub:加密的机密

准备 Azure 部署

打包应用以供部署。 在 Orleans.ShoppingCart.Silos 项目中,Target 元素被定义为在 Publish 步骤之后运行。 此目标将发布目录压缩到 silo.zip 文件中:

<Target Name="ZipPublishOutput" AfterTargets="Publish">
    <Delete Files="$(ProjectDir)\..\silo.zip" />
    <ZipDirectory SourceDirectory="$(PublishDir)" DestinationFile="$(ProjectDir)\..\silo.zip" />
</Target>

可通过多种方式将 .NET 应用部署到 Azure 应用服务。 在本教程中,请使用 GitHub Actions、Azure Bicep 和 .NET 和 Azure CLIs。 请考虑 GitHub 存储库根目录中的 ./github/workflows/deploy.yml 文件:

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

前面的 GitHub 工作流执行以下作:

工作流在推送到 main 分支时触发。 有关详细信息,请参阅 GitHub Actions 和 .NET

小窍门

如果在运行工作流时遇到问题,可能需要验证服务主体是否已注册所有必需的提供程序命名空间。 需要以下提供程序命名空间:

  • Microsoft.Web
  • Microsoft.Network
  • Microsoft.OperationalInsights
  • Microsoft.Insights
  • Microsoft.Storage

有关详细信息,请参阅解决资源提供程序注册错误

Azure 对资源施加命名限制和约定。 更新以下环境变量的 deploy.yml 文件中的值:

  • UNIQUE_APP_NAME
  • AZURE_RESOURCE_GROUP_NAME
  • AZURE_RESOURCE_GROUP_LOCATION

将这些值设置为唯一的应用名称和 Azure 资源组名称和位置。

有关详细信息,请参阅 Azure 资源的命名规则和限制

浏览 Bicep 模板

az deployment group create命令运行时,它会评估 main.bicep 文件。 此文件包含要部署的 Azure 资源。 将此步骤视为 预配 用于部署的所有资源。

重要

如果使用 Visual Studio Code,则 使用 Bicep 扩展时,bicep 创作体验会得到改进。

有许多 Bicep 文件,每个文件都包含资源或模块(资源集合)。 main.bicep 文件是入口文件,主要由module定义组成。

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
  }
}

前面的 Bicep 文件定义以下内容:

  • 资源组名称和应用名称的两个参数。
  • 定义 storageModule 存储帐户的定义。
  • 定义 logsModule,用于定义 Azure Log Analytics 和 Application Insights 资源。
  • 资源 vnet ,定义虚拟网络。
  • 定义的 siloModule,用于定义 Azure 应用服务。

一个非常重要的resource是虚拟网络。 该 vnet 资源使 Azure 应用服务能够与 Orleans 群集通信。

每当 Bicep 文件中遇到 a module 时,都会通过包含资源定义的另一个 Bicep 文件对其进行评估。 遇到的第一个模块是在 storageModulestorage.bicep 文件中定义的:

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 文件接受使用 param 关键字声明的参数。 同样,他们也可以使用关键字声明输出 output 。 存储 resource 依赖于 Microsoft.Storage/storageAccounts@2021-08-01 类型和版本。 它在资源组的位置中,作为StorageV2Standard_LRS SKU 进行预配。 存储 Bicep 文件将其连接字符串定义为 output。 稍后,筒仓 Bicep 文件将使用 connectionString 连接到存储帐户。

接下来, logs-and-insights.bicep 文件定义 Azure Log Analytics 和 Application Insights 资源:

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

此 Bicep 文件定义 Azure Log Analytics 和 Application Insights 资源。 资源 appInsights 是一种 web 类型, logs 资源是一种 PerGB2018 类型。 appInsightslogs资源在资源组的位置中被预配。 资源appInsights通过WorkspaceResourceId属性链接到logs资源。 此 Bicep 文件定义应用服务 module稍后使用的两个输出。

最后, app-service.bicep 文件定义 Azure 应用服务资源:

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'
  }
}

此 Bicep 文件将 Azure 应用服务配置为 .NET 8 应用程序。 appServicePlanappService资源在资源组的位置中被预配。 资源 appService 配置为将 S1 SKU 与容量 1一起使用。 此外,资源配置为使用 vnetSubnetId 子网和 HTTPS。 它还配置 appInsightsInstrumentationKey 检测密钥、 appInsightsConnectionString 连接字符串和 storageConnectionString 连接字符串。 购物车应用使用这些值。

上述适用于 Bicep 的 Visual Studio Code 扩展包括可视化工具。 这些 Bicep 文件都可按如下方式可视化:

Orleans:购物车示例应用 bicep 预配可视化工具呈现。

过渡环境

部署基础结构可以部署到测试环境。 短暂存在、不可变且一次性使用的测试中心环境在将部署提升到生产环境之前对测试部署非常有用。

注释

如果应用服务在 Windows 上运行,则每个应用服务必须位于其自己的独立应用服务计划中。 或者,若要避免此类配置,请改用 Linux 上的应用服务,并解决此问题。

概要

随着源代码的更新和存储库分支的更改pushmaindeploy.yml工作流运行。 它配置 Bicep 文件中定义的资源并部署应用程序。 可以扩展应用程序以包括新功能,例如身份验证,或支持多个实例。 此工作流的主要目标是演示在单个步骤中预配和部署资源的能力。

除了 Bicep 扩展中的可视化工具外,Azure 门户资源组页面在预配和部署应用程序后看起来与以下示例类似:

Azure 门户: Orleans 购物车示例应用资源。

另请参阅