概要
こんにちは!サイオステクノロジーの安藤 浩です。
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 は非推奨です。
Blob 上のファイル更新があった際に Event Subscription とバインドしたEvent Grid Trigger が実行されるようにしたいときに利用できます。
Storage AccountのContainer にファイルを配置したら、Event Subscription がEventが発火して、Azure Function の関数が実行されます。 以下のイメージです。
前提条件
以下が利用できることを前提としています。
ツール | Version |
---|---|
Visual Studio Code | Version: 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 コード
ポイント:
- serverfarms の sku をFlex Consumptionを指定
- modules/func/func.bicep の serverFarmId で PlanId を指定
- 従量課金のSKUのAzure Function とは記述が若干異なる
- 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 と指定が必要です。
- 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 でコメントされている通り、同様に困惑しているようです。
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する
ポイント:
- 事前にAzure Functions にEvent Grid Trigger の関数がDeployされていること。
- Event Grid System Topic 自体はAzure Function に Event Grid Trigger の関数がDeployされていなくとも作成できるが、Event subscription は関数名を指定する必要がある。
- Event subscription のリソース作成では関数名の数分でループして作成。main.parameters.dev.json の箇所の eventGridFunctionsInJson でJson形式で関数をParameter に渡している。
- 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のハマりポイントを踏まえながら説明しました。