概要
こんにちは!サイオステクノロジーの安藤 浩です。
Bicep を利用して、Flex Consumption のAzure Functions で Event Grid トリガー とバインドした Event Subscription を Deploy します。
Flex Consumptionはプレビュー版ですが、仮想ネットワークのサポートや常時起動などの機能が利用できることが特徴です。また、現状では利用できるリージョンも限られています。
利用できるリージョンは以下のコマンドで確認できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | $ 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | param hostingPlanName string param location string = resourceGroup().location param tags object = {} param sku object = {} param kind string = 'linux' 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | @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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | 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 } 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | { "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 コードを実行する。
1 2 3 4 5 6 7 | 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指定をしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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
1 2 3 4 5 6 7 8 9 10 | 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
1 2 3 4 5 6 7 8 9 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | @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 の作成する 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
1 2 3 4 5 6 7 8 | { "operationalInsightsWorkspaces": "log-", "insightsComponents": "appi-", "storageStorageAccounts": "st", "webServerFarms": "plan-", "webSitesFunctions": "func-", "eventGridSystemTopics": "egst-" } |
main.bicep
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | 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 を作成する。 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | { "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 コードを実行する。
1 2 3 4 5 6 7 | 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のハマりポイントを踏まえながら説明しました。