ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

Azure Container Apps で KEDA を使って Azure Queue Storage のバックグラウンド処理を構築する

先日の Microsoft Ignite で発表された Azure Container Apps で、KEDA を使って Azure Queue Storage のバックグラウンド処理を構築してみました。

Azure Container Apps の KEDA サポート

Container Apps では、コンテナのインスタンス(Replicas)の水平オートスケーリングに対応しており、4種類のスケーリング トリガーがあります。

  • HTTP Request
  • Event-driven
  • CPU
  • Memory

Event-driven については、KEDA でサポートされているすべてのイベントに対応しています。今回は、Azure Queue Storage のスケーリング トリガーを試してみます。
Replicas をゼロにスケーリングすると課金されることはありませんが、CPU と Memory はゼロにスケーリングできないので注意が必要です。
docs.microsoft.com

バックグラウンド処理を作成する

バックグラウンド処理を C# で作る場合、2つの方法があります。

  • Azure Functions を使う
  • BackgroundService クラスを使って実装する

今回は、Azure Functions の Queue Trigger テンプレートをそのまま使います。

public class Function
{
    [FunctionName(nameof(Function))]
    public void Run([QueueTrigger("myqueue-items", Connection = "AzureWebJobsStorage")]string myQueueItem, ILogger log)
    {
        log.LogInformation($"C# Queue trigger function processed: {myQueueItem}");
    }
}

Azure Functions Core Tools で func init コマンドに --docker-only を指定すると、既存のプロジェクトに Dockerfile を追加できます。

$ func init --docker-only --dotnet
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS installer-env

# Build requires 3.1 SDK
COPY --from=mcr.microsoft.com/dotnet/core/sdk:3.1 /usr/share/dotnet /usr/share/dotnet

COPY . /src/dotnet-function-app
RUN cd /src/dotnet-function-app && \
    mkdir -p /home/site/wwwroot && \
    dotnet publish *.csproj --output /home/site/wwwroot

# To enable ssh & remote debugging on app service change the base image to the one below
# FROM mcr.microsoft.com/azure-functions/dotnet:4-appservice
FROM mcr.microsoft.com/azure-functions/dotnet:4
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
    AzureFunctionsJobHost__Logging__Console__IsEnabled=true

COPY --from=installer-env ["/home/site/wwwroot", "/home/site/wwwroot"]

Dockerfile が追加されたので、Ducker Hub に Image をプッシュしておきます。

Container Apps をデプロイする

過去の記事では Container Apps Environment のみ Bicep を使ってデプロイしていましたが、今回は Container Apps も合わせてデプロイします。詳細は後述しますが、現時点では Azure CLI を使って KEDA の Scaling rules が設定できないためです。

environment.bicep では、Environment、LogAnalytics、Application Insights のリソースを定義します。

param environmentName string
param logAnalyticsWorkspaceName string = 'logs-${environmentName}'
param appInsightsName string = 'appins-${environmentName}'
param location string = resourceGroup().location

resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2020-08-01' = {
  name: logAnalyticsWorkspaceName
  location: location
  properties: any({
    retentionInDays: 30
    features: {
      searchVersion: 1
      legacy: 0
      enableLogAccessUsingOnlyResourcePermissions: true
    }
    sku: {
      name: 'PerGB2018'
    }
  })
}

resource appInsights 'Microsoft.Insights/components@2020-02-02-preview' = {
  name: appInsightsName
  location: location
  kind: 'web'
  properties: { 
    ApplicationId: appInsightsName
    Application_Type: 'web'
    Flow_Type: 'Redfield'
    Request_Source: 'CustomDeployment'
  }
}

resource environment 'Microsoft.Web/kubeEnvironments@2021-02-01' = {
  name: environmentName
  location: location
  properties: {
    type: 'managed'
    internalLoadBalancerEnabled: false
    appLogsConfiguration: {
      destination: 'log-analytics'
      logAnalyticsConfiguration: {
        customerId: logAnalyticsWorkspace.properties.customerId
        sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey
      }
    }
    containerAppsConfiguration: {
      daprAIInstrumentationKey: appInsights.properties.InstrumentationKey
    }
  }
}

output location string = location
output environmentId string = environment.id

storage.bicep では、Storage Accounts のリソースを定義します。ConnectionString を作っている部分がポイントです。

param storageAccountName string
param location string = resourceGroup().location

var strageSku = 'Standard_LRS'

resource stg 'Microsoft.Storage/storageAccounts@2019-06-01' = {
  name: storageAccountName
  location: location
  kind: 'Storage'
  sku:{
    name: strageSku
  }
}

output storageId string = stg.id
output storageConnectionString string = 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(stg.id, stg.apiVersion).keys[0].value}'

containerapp.bicep では、バックグラウンド処理を実行する Functions の Docker Image を指定して Container Apps のリソースを定義します。
バックグラウンド処理なので Ingress は有効化せず、Functions の構成に必要となる環境変数とシークレットに Storage の ConnectionString を指定します。
myqueue-items の Queue 内のメッセージが 3 個を超えると新しい Replicas を作成して、最大 10 個までオートスケーリングさせます。

param environmentId string
@secure()
param storageConnectionString string
param location string = resourceGroup().location

resource containerApp 'Microsoft.Web/containerApps@2021-03-01' = {
  name: 'queue-reader-function'
  kind: 'containerapp'
  location: location
  properties: {
    kubeEnvironmentId: environmentId
    configuration: {
      activeRevisionsMode: 'single'
      secrets: [
        {
          name: 'storage-connection'
          value: storageConnectionString
        }
      ]   
    }
    template: {
      containers: [
        {
          image: 'thara0402/queue-reader-function:0.1.0'
          name: 'queue-reader-function'
          env: [
            {
              name: 'AzureWebJobsStorage'
              secretref: 'storage-connection'
            }
            {
              name: 'FUNCTIONS_WORKER_RUNTIME'
              value: 'dotnet'
            }
          ]
        }
      ]
      scale: {
        minReplicas: 0
        maxReplicas: 10
        rules: [
          {
            name: 'queue-scaling-rule'
            azureQueue: {
              queueName: 'myqueue-items'
              queueLength: 3
              auth: [
                {
                  secretRef: 'storage-connection'
                  triggerParameter: 'connection'
                }
              ]
            }
          }
        ]
      }
    }
  }
}

main.bicep では、それぞれの bicep ファイルをモジュールとして定義します。

param environmentName string = 'env-${resourceGroup().name}'
param storageAccountName string = resourceGroup().name

module environment 'environment.bicep' = {
  name: 'container-app-environment'
  params: {
    environmentName: environmentName
  }
}

module storage 'storage.bicep' = {
  name: 'storage'
  params: {
    storageAccountName: storageAccountName
  }
}

module containerapp 'containerapp.bicep' = {
  name: 'container-app'
  params: {
    environmentId: environment.outputs.environmentId
    storageConnectionString: storage.outputs.storageConnectionString
  }
}

Azure CLI を使って、デプロイします。

$ az group create -n <ResourceGroup Name> -l canadacentral
$ az deployment group create -f ./deploy/main.bicep -g <ResourceGroup Name>

作られたリソースは、こんな感じです。

f:id:TonyTonyKun:20211116110213p:plain

Container Apps の Revision を確認すると、オートスケーリングが設定されたことが分かります。

f:id:TonyTonyKun:20211116110534p:plain

結果確認

Azure CLI を使って、Revision の Replicas 数を確認しておきます。

$ az containerapp revision show  --app queue-reader-function -n <Revision Name> -g <ResourceGroup Name> -o table

サーバーレスの設定を行ったので、Replicas 数が 0 であることを確認できます。

f:id:TonyTonyKun:20211116111555p:plain

myqueue-items の Queue を作ってメッセージを 10 個ほど送信すると、Replicas 数が 4 に増えたことを確認できます。

f:id:TonyTonyKun:20211116112149p:plain

Log Analytics でクエリを実行すると、Functions が実行されたログを確認できます。

ContainerAppConsoleLogs_CL |
project TimeGenerated, ContainerAppName_s, RevisionName_s, Log_s |
where ContainerAppName_s contains "queue-reader-function" |
order by TimeGenerated desc 

f:id:TonyTonyKun:20211116112157p:plain

まとめ

Azure Container Apps で、KEDA を使って Azure Queue Storage のバックグラウンド処理を構築してみました。
今回のように C# で Queue Trigger のケースは素直に Azure Functions を使ったほうがいいですが、Functions で非対応の開発言語でカスタムハンドラを作らざるを得ないケースでは Container Apps が選択肢になると思います。
Azure Functions Core Tools には func kubernetes install コマンドがあるので、いずれ Container Apps にも対応する可能性があるので、さらに簡単にデプロイできそうです。

今回のソースコードは、こちらのリポジトリで公開しています。
github.com

【補足】Azure CLI で Scaling rules が設定できない件

まだ Preview ということもあり、現時点では Azure CLI を使って KEDA の Scaling rules が設定できないです。(HTTP の Scaling rules は設定できる)
Container Apps はデプロイされますが、Scaling rules が null となります。

$ az containerapp create -n queue-reader-function -g <ResourceGroup Name> \
    -e <Environment Name> -i thara0402/queue-reader-function:0.1.0 \
    --ingress internal --target-port 80 --revisions-mode single \
    --scale-rules ./deploy/queuescaler.json --max-replicas 10 --min-replicas 0 \
    -s storage-connection="<Storage Connection>" \
    -v FUNCTIONS_WORKER_RUNTIME=dotnet,AzureWebJobsStorage=secretref:storage-connection

queuescaler.json のフォーマットが間違っているのか、Azure CLI のバグなのか、フィードバックしています。

[{
    "name": "queue-scaling-rule",
    "type": "azureQueue",
    "auth": [
        {
          "secretRef": "storage-connection",
          "triggerParameter": "connection"
        }
    ],
    "queueLength": 3,
    "queueName": "myqueue-items"
}]