65.9K
CodeProject 正在变化。 阅读更多。
Home

Azure Functions 自动订阅到 Event Grid

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2018年11月19日

CPOL

4分钟阅读

viewsIcon

13557

downloadIcon

119

Azure 函数自动订阅到 Event Grid。

引言

随着 Event Grid 的引入,出现了更快地分发事件的新可能性,消除了像 Service Bus 中那样轮询事件的需要。现在,当消息满足订阅过滤器时,Event Grid 会自行执行简单的基于 HTTP 的事件传递,从而可以降低延迟和成本。到目前为止,一切都运行得很好,但要将函数连接到 Event Grid 需要大量的手动工作,需要在 Azure 门户中进行多次点击。考虑到在几个不同的环境中存在许多函数,这可能导致部署过程中的许多错误。但即使在部署之后,谁来维护它呢?你需要进行多少次点击才能在你的订阅中引入更改?

背景

“让我们项目中使用 Event Grid”——我提议道。事情就是这样开始的。

回想一下我们当时已经存在的 Service Bus 订阅的解决方案——每一个都手动创建、手动维护,没有变更历史。对我来说,这是“不可行”的。我希望有一个尽可能接近消费者的消费者定义(订阅定义),并且能够清楚地了解订阅在我们的代码存储库中是如何随时间变化的。

这个想法背后很简单

  1. 将消费者函数创建为 Azure FunctionApp
  2. 在同一个 Azure FunctionApp 中创建消费者订阅定义。
  3. 在 CI/CD 过程中使用 PowerShell 脚本将函数连接到 Event Grid。

将会有许多带有 FunctionApp 的组件。每个组件都将被包含在一个资源组中。每个 FunctionApp 可以有许多函数,并且这些函数中的每一个都可以有许多订阅定义。

-- Resource Group
|-- Function App
   |-- function
     | -- subscription filter 
     | -- ...
   | -- ...
| -- ...

Using the Code

我在我的 FunctionApp 2.x 项目中创建了两个函数。第一个是标准的 EventGridTrigger,负责消费传入的事件。第二个函数则包含订阅的定义。

消费者函数

using Microsoft.Azure.EventGrid.Models;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.EventGrid;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

namespace EventGridSubscription2xFunctionApp
{
    public class MyFunction
    {
        [FunctionName("MyFunction")]
        public static async 
               Task Run([EventGridTrigger] EventGridEvent eventGridEvent, ILogger log)
        {
            ...
        }
    }
}

关于 FunctionApp 1.x 的说明

请注意,为了让它适用于 Azure Functions 1.x,您必须使用特定的 NuGet 包版本。

绑定也不同。

    [FunctionName("MyFunction")]
    public static async Task Run
    ([EventGridTrigger] JObject eventGridEvent, TraceWriter log)
    {
        ...       
    }

订阅定义函数

订阅函数必须遵循命名约定,以便以后能够轻松地发现并连接到 EventGrid 组件。在我们的例子中,我们选择使用“_Subscription”作为包含 EventGrid 订阅定义的函数名称的后缀。

 [FunctionName("MyFunction_Subscription")]
        public static async Task<HttpResponseMessage> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req,
            ILogger log)
        {
            var subscription1 = Create("subscription-name-1", 
            "my-event-type-11", "my-event-type-12");
            var subscription2 = Create("subscription-name-2", 
            "my-event-type-2");

            var subscriptions = new[] { subscription1, subscription2 };

            return new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(JsonConvert.SerializeObject
                  (subscriptions, Formatting.Indented), 
                   Encoding.UTF8, "application/json")
            };
        }

该函数将由 HTTP 请求触发,并返回 JSON 格式的订阅定义。由于一个函数可以由不同的订阅触发,因此将为每个订阅创建单独的定义。在上面的示例中,它们是 subscription1subscription2

   private static EventGridSubscription Create
           (string subscriptionName, params string[] eventTypes)
   {
       var subscription = new EventGridSubscription
       {
           Name = subscriptionName,
         Prefix = "prefix",   // Subject Begins With
      Postfix = "postfix",    // Subject Ends With
           EventTypes = eventTypes
       };

       return subscription;
   }

包含订阅定义的响应模型如下。响应是 JSON 格式的。

    public class EventGridSubscription
    {
        private readonly List<string> _eventTypes;

        public EventGridSubscription()
        {
            _eventTypes = new List<string>();
        }

        [JsonProperty("name")]
        public string Name { get; set; }

        [JsonProperty("eventTypes")]
        public string[] EventTypes { get; set; }

        [JsonProperty("subjectBeginsWith")]
        public string Prefix { get; set; }

        [JsonProperty("subjectEndsWith")]
        public string Postfix { get; set; }
    }

PowerShell

此解决方案的核心组件是 PowerShell 脚本,该脚本从“_Subscription”函数获取订阅定义,并在 EventGrid 上挂载 WebHook 来创建或更新订阅。在我们当前的解决方案中,我们有许多带有 FunctionApp 的组件。每个组件都在一个单独的资源组中。因此,脚本需要组件资源组名称以及包含 EventGrid 的资源组作为输入参数。

param (
    [string][Parameter(Mandatory=$true)]$EventGridTopicName,
    [string][Parameter(Mandatory=$true)]$EventGridTopicResourceGroupName,
    [string][Parameter(Mandatory=$true)]$FunctionAppsResourceGroupName
)

逻辑很简单

  1. 查找所有 FunctionApp
  2. 查找所有带有“_Subscription”后缀的函数
  3. 获取函数密钥和订阅定义
  4. EventGrid 上注册函数

现在一步一步来。

获取资源组中的所有 FunctionApp

只需获取 Kind 为“functionapp”的所有资源。

{
    [CmdletBinding()]
    param
    (
        [string] [Parameter(Mandatory=$true)] $ResourceGroupName
    )

    $azureRmResourceParams = @{
        ResourceGroupName = $ResourceGroupName
        ResourceType      = 'Microsoft.Web/sites'
        ApiVersion        = '2018-09-01'
    }
    $resources = Get-AzureRmResource @azureRmResourceParams

    $functionApps = @()
    
    foreach ($resource in $resources) {
        if($resource.Kind -eq "functionapp"){
            $name = $resource.Name
            $functionApps += $name
        }
    }
    return $functionApps
}

然后我们遍历 FunctionApp 并获取所有带有“_Subscription”前缀的函数。只有这些函数会被处理。

    $azureRmResourceParams = @{
        ResourceGroupName = $ResourceGroupName
        ResourceType      = 'Microsoft.Web/sites/functions'
        ResourceName      = $FunctionApp
        ApiVersion        = '2015-08-01'
    }
    $functions = Get-AzureRmResource @azureRmResourceParams

    foreach ($function in $functions) {
        $functionName = $function.ResourceName
        if ($functionName.endswith("_Subscription") )
        {
            ...
        }
    }

这是核心逻辑,获取函数的主密钥,获取订阅,最后将它们作为元数据分组。

           if($masterKey -eq $null){
                $functionAppMasterKeyParams = @{
                    ResourceGroup = $ResourceGroupName
                    FunctionApp = $FunctionApp
                }       
                $masterKey = Get-FunctionAppMasterKey @functionAppMasterKeyParams
            }
        
            ## get subscription parameters
            $subscriptionFunctionName =  $functionName -replace "$FunctionApp/", ""
            $subscriptionApiUrl = 
            'http://'+$FunctionApp+'.azurewebsites.net/api/'+$subscriptionFunctionName +
            '?code='+$masterKey
            $subscriptionResult = Invoke-WebRequest $subscriptionApiUrl | 
                                  ConvertFrom-Json
            
            ## get function endpoint
            $functionAppName = $subscriptionFunctionName -replace "_Subscription", ""
            $eventGridApiUrl = 'http://'+$FunctionApp+'.azurewebsites.net/
                               admin/host/systemkeys/
                               eventgrid_extension?code='+$masterKey
            
            $sysKeyResult = Invoke-WebRequest $eventGridApiUrl | 
                            select -Expand Content | ConvertFrom-Json | select value
            $sysKey = $sysKeyResult.value
             
            $eventGridSubscriptionEndpoint = 'https://'+$FunctionApp+
                                             '.azurewebsites.net/runtime/webhooks/
            eventgrid?functionName='+$functionAppName+'&code='+$sysKey
            foreach($result in $subscriptionResult)
            {
                $subscription = @{
                    name = $result.name
                    endpoint = $eventGridSubscriptionEndpoint
                    eventTypes = $result.eventTypes
                    subjectEndsWith = $result.subjectEndsWith
                    subjectBeginsWith = $result.subjectBeginsWith
                }
                $subscription
            }

关于 FunctionApp 1.x 与 2.x 的说明

FunctionApp 1.x 和 2.x 的 API 存在差异。与所述情况相关的差异如下。

SystemKey API

1.x 'https://'+$FunctionApp+'.azurewebsites.net/admin/host/systemkeys/
eventgridextensionconfig_extension?code='+$MasterKey
2.x 'http://'+$FunctionApp+'.azurewebsites.net/admin/host/systemkeys/
eventgrid_extension?code='+$masterKey

Azure 函数 WebHook 用于 EventGrid URL

1.x 'https://'+$FunctionApp+'.azurewebsites.net/admin/extensions/
EventGridExtensionConfig?functionName='+$functionAppName+'&code='+$sysKey
2.x 'https://'+$FunctionApp+'.azurewebsites.net/runtime/webhooks/
eventgrid?functionName='+$functionAppName+'&code='+$sysKey

关于 FunctionApp 2.x 的说明

2.x 版本中 FunctionApp 的密钥必须存储在存储文件中,而不是 blob 中。由于 blob 是默认值,因此应显式设置。PowerShell 脚本需要访问 FunctionApp 密钥。这可以通过 Azure 门户手动设置,或在 ARM 模板中自动化。我强烈建议您不要手动进行任何操作。

在 Azure DevOps 中整合

Azure DevOps 的标准发布管道可用于部署 FunctionApp 和 PowerShell 脚本。只需部署 FunctionApp,然后运行 Azure PowerShell 任务,它将完成神奇的工作——将 EventGrid 连接到函数。

关注点

ARM 模板方法

还有其他方法可以做到这一点。您可以使用 ARM 模板。但是…… ARM 模板的流程是这样的:

  1. 部署 FunctionApp
  2. 使用 PowerShell 获取 FunctionAppSystemKey
  3. 使用 SystemKey 部署 ARM

这是一个先有鸡还是先有蛋的问题。在部署 FunctionApp 之前,您需要 SystemKey。您无法在 FunctionApp 部署期间将其提取并传递给同一个 ARM 中的 EventGrid 模板。第二个缺点(在我看来)是订阅定义保存在 ARM 模板中。我希望将过滤器定义保留在靠近消费者代码的地方。

历史

  • 2018 年 11 月 19 日:初始版本
  • 2018 年 11 月 20 日:添加了 EventGridSubscription.zip
© . All rights reserved.