Bicepによる Event Grid トリガーとバインドした Event Subscription の作成 (Flex Consumption のAzure Functionsの利用)

概要

こんにちは!サイオステクノロジーの安藤 浩です。

Bicep を利用して、Flex Consumption のAzure Functions で Event Grid トリガー とバインドした Event Subscription を Deploy します。

Flex Consumptionはプレビュー版ですが、仮想ネットワークのサポートや常時起動などの機能が利用できることが特徴です。また、現状では利用できるリージョンも限られています。

利用できるリージョンは以下のコマンドで確認できます。

$ az functionapp list-flexconsumption-locations --output table
northeurope
southeastasia
eastasia
eastus2
southcentralus
australiaeast
eastus
northcentralus(stage)
westus2
uksouth
eastus2euap
westus3
swedencentral

オペレーティング システムのサポート や Function App タイムアウト などは以下を参照してください。

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-scale

また、Flex Consumptionでは以下のURLに記載のパラメータは非推奨です。後ほどAzure Function のDeploy の際に出てくる ENABLE_ORYX_BUILD , SCM_DO_BUILD_DURING_DEPLOYMENT は非推奨です。

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-app-settings#flex-consumption-plan-deprecations

Blob 上のファイル更新があった際に Event Subscription とバインドしたEvent Grid Trigger が実行されるようにしたいときに利用できます。

Storage AccountのContainer にファイルを配置したら、Event Subscription がEventが発火して、Azure Function の関数が実行されます。 以下のイメージです。

前提条件

以下が利用できることを前提としています。

ツールVersion
Visual Studio CodeVersion: 1.94.2
Visual Studio Code 用の Bicep 拡張機能0.30.23
最新の Azure CLI ツールまたは最新の Azure PowerShell バージョンAzure CLI : 2.65.0 ここではAzure CLI を利用します。

Deploy 手順

Flex Consumption の場合は通常のConsumption とはBicep の記法が異なるので、注意です。 また、Event Grid Subscription を Deploy する際には既にEvent Grid Trigger の関数が存在しなければならないので、以下の手順でDeploy する必要があります。

手順1. AppService Plan: Flex Consumption での Azure Function をDeployする。

手順2. Event Grid Subscription と紐づける Azure Function にEvent Grid Trigger の関数をDeployする。

手順3. Event Grid(System Topic) と Event Grid Subscription をDeployする。

手順1. AppService Plan: Flex Consumption での Azure Function をDeployする

Flex Consumption のAzure Function をDeployするコードは以下です。 ※Application Insights などは省いています。

Flex Consumption の Bicep コード

ポイント:

  1. serverfarms の sku をFlex Consumptionを指定
  2. modules/func/func.bicep の serverFarmId で PlanId を指定
  3. 従量課金のSKUのAzure Function とは記述が若干異なる
  4. Function から Storage Accountへのアクセス権限(ストレージ BLOB データ所有者)を付与する (Bicep コード実行時に Role Based Access Control Administrator または、 User Access Administrator の権限が必要)

modules/func/func.bicep

param hostingPlanName string
param functionName string
param location string = resourceGroup().location
param storageAccountName string
param deploymentStorageContainerName string
param applicationInsightsName string
param tags object = {}
param functionAppRuntime string = 'python'
param functionAppRuntimeVersion string = '3.11'
param maximumInstanceCount int = 100
param instanceMemoryMB int = 2048
param eventGridStorageAccountName string

resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' existing = {
  name: storageAccountName
}

resource eventGridStorage 'Microsoft.Storage/storageAccounts@2023-01-01' existing = {
  name: eventGridStorageAccountName
}

resource appInsights 'Microsoft.Insights/components@2020-02-02' existing = {
  name: applicationInsightsName
}


resource flexFuncPlan 'Microsoft.Web/serverfarms@2023-12-01' existing = {
  name: hostingPlanName
}

resource flexFuncApp 'Microsoft.Web/sites@2023-12-01' = {
  name: functionName
  location: location
  tags: tags
  kind: 'functionapp,linux'
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: flexFuncPlan.id
    siteConfig: {
      appSettings: [
        {
          name: 'AzureWebJobsStorage__accountName'
          value: storage.name
        }
        {
          name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
          value: appInsights.properties.ConnectionString
        }
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: '~4'
        }
        {
          name: 'BLOB_CONNECTION_STRING'
          value: 'DefaultEndpointsProtocol=https;AccountName=${eventGridStorage.name};AccountKey=${eventGridStorage.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}'
        }
      ]
    }
    functionAppConfig: {
      deployment: {
        storage: {
          type: 'blobContainer'
          value: '${storage.properties.primaryEndpoints.blob}${deploymentStorageContainerName}'
          authentication: {
            type: 'SystemAssignedIdentity'
          }
        }
      }
      scaleAndConcurrency: {
        maximumInstanceCount: maximumInstanceCount
        instanceMemoryMB: instanceMemoryMB
      }
      runtime: { 
        name: functionAppRuntime
        version: functionAppRuntimeVersion
      }
    }
  }
  dependsOn:[
    appInsights
    storage
    eventGridStorage
  ]
}

var storageRoleDefinitionId  = 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' //Storage Blob Data Owner role

// Allow access from function app to storage account using a managed identity
resource storageRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
  name: guid(storage.id, storageRoleDefinitionId)
  scope: storage
  properties: {
    roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', storageRoleDefinitionId)
    principalId: flexFuncApp.identity.principalId
    principalType: 'ServicePrincipal'
  }
}

// resource function 'Microsoft.Web/sites/functions@2020-12-01' = {
//     parent: flexFuncApp 
//     name: functionNameComputed
//     properties: {
//       config: {
//         disabled: false
//         bindings: [
//           {
//             type: 'eventGridTrigger'
//             name: 'event'
//             direction: 'in'
//           }
//         ]
//       }
//       files: {
//         '__init__.py': loadTextContent('__init__.py')
//       }
//     }
// }

※コメントアウトの箇所は従量課金用のSKUでは動作しましたが、Flex Consumptionでは動作しません。

modules/host/asp.bicep

param hostingPlanName string
param location string = resourceGroup().location
param tags object = {}
param sku object = {}

param kind string = 'linux'


//https://github.com/Azure/azure-quickstart-templates/blob/master/quickstarts/microsoft.web/app-function/main.bicep
resource hostingPlan 'Microsoft.Web/serverfarms@2023-12-01' = {
  name: hostingPlanName
  location: location
  tags: tags
  kind: kind
  properties: {
    reserved: true
  }
  sku: sku
}

output id string = hostingPlan.id
output name string = hostingPlan.name

modules/storage/storage.bicep

@description('Storage Account type')
@allowed([
  'Premium_LRS'
  'Premium_ZRS'
  'Standard_GRS'
  'Standard_GZRS'
  'Standard_LRS'
  'Standard_RAGRS'
  'Standard_RAGZRS'
  'Standard_ZRS'
])
param storageAccountType string = 'Standard_LRS'

@description('The storage account location.')
param location string = resourceGroup().location

param tags object = {}

@description('The name of the storage account')
param storageAccountName string

param containerNames array

resource sa 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  tags: tags
  sku: {
    name: storageAccountType
  }
  kind: 'StorageV2'
  properties: {}
}

resource blobServices 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
  parent: sa
  name: 'default'
}

resource containers 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = [for containerName in containerNames: {
  parent: blobServices
  name: containerName
}]


output storageAccountName string = storageAccountName
output storageAccountId string = sa.id

main.bicep

targetScope = 'resourceGroup'

param location string = resourceGroup().location
@description('The environment designator for the deployment. Replaces {env} in namingConvention.')
@allowed([
  'dev' //Develop
  'stg' //Staging
  'prd' //Production
])
param enviromentName string = 'dev'
var enviromentResourceNameWithoutHyphen = replace(enviromentName, '-', '')

@allowed(['northeurope', 'southeastasia', 'eastasia', 'eastus2', 'southcentralus', 'australiaeast', 'eastus', 'westus2', 'uksouth', 'eastus2euap', 'westus3', 'swedencentral'])
param hostingPlanLocation string = 'eastus2'

@description('The workload name. Replaces {workloadName} in namingConvention.')
param workloadName string = 'pj'

param deploymentStorageContainerName string = 'app-pkg-func'
param eventGridContainerName string = 'eventcontainer'


var suffixResourceName = '-${workloadName}'
var suffixResourceNameWithoutHyphen = replace(suffixResourceName, '-', '')

param convertedEpoch int = dateTimeToEpoch(dateTimeAdd(utcNow(), 'P1Y'))

var abbrs = json(loadTextContent('abbreviations.json'))

var tags = {
  workload: workloadName
  environment: enviromentName
}

//https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.azure.management.operationalinsights.models.workspacesku?view=azure-dotnet-legacy
////https://github.com/mspnp/microservices-reference-implementation/blob/e903447c7b8b6af6302a5f5a7c672572471d01e4/azuredeploy.bicep#L77
module logAnalyticsWorkspace 'modules/monitor/logAnayticsWorkspace.bicep' = {
  name: 'logAnalyticsWorkspace'
  params:{
    logAnalyticsName: '${abbrs.operationalInsightsWorkspaces}${enviromentName}${suffixResourceName}'
    location: location
  }
}

module appInsights 'modules/monitor/appInsights.bicep' = {
  name: 'appInsights'
  params:{
    name: '${abbrs.insightsComponents}${enviromentName}${suffixResourceName}'
    location: location
    tags: tags
    logAnalyticsWorkspaceId: logAnalyticsWorkspace.outputs.id
  }
}

module storage 'modules/storage/storage.bicep' = {
  name: 'storage'
  params: {
    storageAccountName: '${abbrs.storageStorageAccounts}${enviromentResourceNameWithoutHyphen}${suffixResourceNameWithoutHyphen}'
    location: location
    tags: tags
    storageAccountType: 'Standard_LRS'
    containerNames: [deploymentStorageContainerName]
  }
}


module evetGridStorage 'modules/storage/storage.bicep' = {
  name: 'evetGridStorage'
  params: {
    storageAccountName: '${abbrs.storageStorageAccounts}${enviromentResourceNameWithoutHyphen}egst${suffixResourceNameWithoutHyphen}'
    location: location
    tags: tags
    storageAccountType: 'Standard_LRS'
    containerNames: [eventGridContainerName]
  }
}

var aspSku = {
  tier: 'FlexConsumption'
  name: 'FC1'
}


module hostingPlan 'modules/host/asp.bicep' = {
  name: 'hostingPlan'
  params:{
    hostingPlanName: '${abbrs.webServerFarms}${enviromentName}${suffixResourceName}'
    location: !empty(hostingPlanLocation) ? hostingPlanLocation : location
    tags: tags
    sku: aspSku
    kind: 'functionapp,linux'
  }
}

module flexFunction 'modules/function/flexFunction.bicep' = if(aspSku.tier == 'FlexConsumption'){
  name: 'flexFunction'
  params:{
    functionName: '${abbrs.webSitesFunctions}${enviromentName}-flex${suffixResourceName}'
    location: hostingPlanLocation
    tags: tags
    hostingPlanName: hostingPlan.outputs.name
    storageAccountName: storage.outputs.storageAccountName
    applicationInsightsName: appInsights.outputs.appInsightsName
    functionAppRuntime: 'python'
    functionAppRuntimeVersion: '3.11'
    deploymentStorageContainerName: deploymentStorageContainerName
    eventGridStorageAccountName: evetGridStorage.outputs.storageAccountName
  }
}

main.parameters.dev.json

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "location": {
      "value": "eastus2"
    },
    "enviromentName": {
      "value": "dev"
    },
    "hostingPlanLocation": {
      "value": "eastus2"
    },
    "workloadName": {
      "value": "pj-bicep"
    },
    "deploymentStorageContainerName": {
      "value": "app-pkg-func-flex"
    }
  }
}

Complete モードでBicep コードを実行する。

az deployment group create \
  --name {Resource Group 名}-deploy \
  --mode Complete \
  --resource-group {Resource Group 名} \
  --confirm-with-what-if \
  --template-file ./main.bicep \
  --parameters ./main.parameters.dev.json

手順2.Event Grid Subscription と紐づける Azure Function にEvent Grid Trigger の関数をDeployする。

Github actions のWorkflowの一部ですが、Flex Consumptionの場合、sku と remote-build を以下のように指定する必要がありました。

v1.5.2 でFlex Consumptionに対応したので厳密なVersion指定をしています。

  deploy:
    runs-on: ubuntu-22.04
    environment: 'dev'
    steps:
      - name: Run Azure Functions Action.
        uses: Azure/functions-action@v1.5.2
        id: deploy-to-function
        with:
          app-name: ${{ vars.AZURE_FUNCTIONAPP_NAME }}
          package: ${{ env.DOWNLOAD_ARTIFACT_DIRPATH }}
          publish-profile: ${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
          sku: flexconsumption
          remote-build: false

  • sku については以下に記載の通り、flexconsumption と指定が必要です。

https://github.com/Azure/functions-action/blob/067064f06ab9a7aeb62be38d35d45287dfa39443/action.yml#L49

  • remote-build については 以下のようにtrue にする必要があると書いてありますが、false としてRun Azure Functions Action. が実行される前にrequirements.txt からインストールすることで、Deployに成功しました。’scm-do-build-during-deployment’ and ‘enable-oryx-build’ は利用できないのでDefault値のままfalseです。 https://github.com/Azure/functions-action/issues/245 でコメントされている通り、同様に困惑しているようです。

https://github.com/Azure/functions-action/blob/067064f06ab9a7aeb62be38d35d45287dfa39443/action.yml#L54

Event Trigger は以下のようにログ出力するだけの関数を用意しています。

function_app.py

import logging
import azure.functions as func

app = func.FunctionApp()

@app.function_name(name="eventGridTrigger01")
@app.event_grid_trigger(arg_name="event")
def event_grid_trigger(event: func.EventGridEvent):
    """Process an event grid trigger for a new blob in the container."""
    logging.info("Processing event %s", event.id)

手順3. Event Grid(System Topic) と Event Grid Subscription をDeployする

ポイント:

  1. 事前にAzure Functions にEvent Grid Trigger の関数がDeployされていること。
  2. Event Grid System Topic 自体はAzure Function に Event Grid Trigger の関数がDeployされていなくとも作成できるが、Event subscription は関数名を指定する必要がある。
  3. Event subscription のリソース作成では関数名の数分でループして作成。main.parameters.dev.json の箇所の eventGridFunctionsInJson でJson形式で関数をParameter に渡している。
  4. eventGridStorageName と functionAppName はそれぞれ手順1でDeployしたStorage Account名とAzure Function 名を指定する。

Event Grid 用のBicep コード

modules/storage/storage.bicep

param name string

resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' existing = {
  name: name
}

output id string = storage.id
output name string = storage.name
output location string = storage.location

modules/event/systemTopic.bicep

@description('The name of the Event Grid custom topic.')
param eventGridSystemTopicName string = 'topic-${uniqueString(resourceGroup().id)}'

@description('The name of the Event Grid custom topic\'s subscription.')
param eventGridSystemTopicSubscriptionName string = 'sub-${uniqueString(resourceGroup().id)}'

param location string = resourceGroup().location

param storageAccountId string

param tags object = {}

param functionAppName string

param functions array

// Microsoft.EventGrid/systemTopics の作成する
// https://github.com/Azure/azure-quickstart-templates/blob/master/quickstarts/microsoft.eventgrid/event-grid-subscription-and-storage/main.bicep
resource systemTopic 'Microsoft.EventGrid/systemTopics@2023-12-15-preview' = {
  name: eventGridSystemTopicName
  location: location
  identity: {
    type: 'SystemAssigned'
  }
  tags: tags
  properties: {
    source: storageAccountId
    topicType: 'Microsoft.Storage.StorageAccounts'
  }
}

// 関数の数分、Event Grid Subscription を作成する
resource eventSubscription 'Microsoft.EventGrid/systemTopics/eventSubscriptions@2023-12-15-preview' = [for function in functions: {
  parent: systemTopic
  name: '${eventGridSystemTopicSubscriptionName}-${function.name}'
  properties: {
    destination: {
      endpointType: 'AzureFunction'
      properties: {
        resourceId: resourceId('Microsoft.Web/sites/functions', functionAppName, function.name)
      }
    }
    eventDeliverySchema: 'EventGridSchema'
    filter: {
      includedEventTypes: function.includedEventTypes
    }
  }
}]

abbreviations.json

{
    "operationalInsightsWorkspaces": "log-",
    "insightsComponents": "appi-",
    "storageStorageAccounts": "st",
    "webServerFarms": "plan-",
    "webSitesFunctions": "func-",
    "eventGridSystemTopics": "egst-"
  }

main.bicep

targetScope = 'resourceGroup'

@minLength(1)
@maxLength(16)
@description('Name of the the environment which is used to generate a short unique hash used in all resources.')
param environmentName string = 'test'

@description('The workload name. Replaces {workloadName} in namingConvention.')
param workloadName string = 'pj'

var suffixResourceName = '-${workloadName}'
var suffixResourceNameWithoutHyphen = replace(suffixResourceName, '-', '')

param eventGridFunctionsInJson object
var eventGridFunctions = eventGridFunctionsInJson.eventGridFunctions

param eventGridStorageName string

param functionAppName string

var tags = { 'workload': workloadName, 'environment': environmentName }

var abbrs = loadJsonContent('./abbreviations.json')

// Event Grid 用のストレージアカウントを参照する。
module eventGridStorage './modules/storage/storage.bicep' = {
  name: 'eventGridStorage'
  params: {
    name: eventGridStorageName
  }
}

// NOTE: Function App に Event Grid Trigger を追加後に、Event Grid System Topic と Event Grid Subscription を作成する。
// Event Grid (System Topic) , Event Grid Subscription を作成する。
// https://learn.microsoft.com/ja-jp/azure/templates/microsoft.storage/storageaccounts/blobservices/containers?pivots=deployment-language-bicep
// https://learn.microsoft.com/ja-jp/azure/templates/microsoft.eventgrid/eventsubscriptions?pivots=deployment-language-bicep
module eventGridSystemTopic './modules/event/systemTopic.bicep' = {
  name: 'eventGridSystemTopic'
  params: {
    eventGridSystemTopicName: '${abbrs.eventGridSystemTopics}${environmentName}${suffixResourceName}'
    eventGridSystemTopicSubscriptionName: '${abbrs.eventGridEventSubscriptions}${environmentName}${suffixResourceName}'
    location: eventGridStorage.outputs.location  //NOTE: eventGrid Storage と同じ Region にする
    storageAccountId: eventGridStorage.outputs.id
    functionAppName: functionAppName
    functions: eventGridFunctions
    tags: tags
  }
}

main.parameters.dev.json

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "environmentName": {
      "value": "dev"
    },
    "workloadName": {
      "value": "pj-bicep"
    },
    "eventGridStorageName": {
      "value": "stdevegstpjbicep"
    },
    "functionAppName": {
      "value": "func-dev-flex-pj-bicep"
    },
    "eventGridFunctionsInJson": {
      "value": {"eventGridFunctions":[{"name": "eventGridTrigger01", "includedEventTypes": ["Microsoft.Storage.BlobCreated"]}]}
    }
  }
}

Incremental モードでBicep コードを実行する。

az deployment group create \
  --name {Resource Group 名}-deploy-eventgrid \
  --mode Incremental \
  --resource-group {Resource Group 名} \
  --confirm-with-what-if \
  --template-file ./main.bicep \
  --parameters ./main.parameters.dev.json

確認方法

手順1で作成したEvent Grid 用のStorage Accountのコンテナに任意のファイルをアップロードすることでAzure Function のEvent Grid Trigger が実行されることを確認します。 ここでは Azure Function にDeployした関数: eventGridTrigger01 の呼び出しタブから実行結果を確認しました。 

まとめ

Flex Consumptionはまだ Preview 版のため本番運用では非推奨ですが、常時使用可能でタイムアウトの上限がないなどメリットがあります。今回はFlex ConsumptionのBicep コードによるDeployとGithub actionsでのDeployのハマりポイントを踏まえながら説明しました。

参考URL

Azure Event Grid とは

Azure Functions のホスティング オプション

Azure Functions の Flex 従量課金プラン ホスティング

Flex 従量課金プランの非推奨

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です