ROMANCE DAWN for the new world

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

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

Microsoft Build 2022 で Azure Container Apps が GA されました。東日本リージョンでも使えるようになりましたので、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 をデプロイする

Bicep を使って、Container Apps をデプロイします。
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' = {
  name: appInsightsName
  location: location
  kind: 'web'
  properties: { 
    Application_Type: 'web'
    WorkspaceResourceId:logAnalyticsWorkspace.id
  }
}

resource environment 'Microsoft.App/managedEnvironments@2022-03-01' = {
  name: environmentName
  location: location
  properties: {
    appLogsConfiguration: {
      destination: 'log-analytics'
      logAnalyticsConfiguration: {
        customerId: logAnalyticsWorkspace.properties.customerId
        sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey
      }
    }
    daprAIInstrumentationKey: appInsights.properties.InstrumentationKey
    zoneRedundant: false
  }
}

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 containerAppName string
param location string = resourceGroup().location
param environmentId string
param containerImage string
param revisionSuffix string
@allowed([
  'multiple'
  'single'
])
param revisionMode string = 'single'
@secure()
param storageConnectionString string

resource containerApp 'Microsoft.App/containerApps@2022-03-01' = {
  name: containerAppName
  location: location
  properties: {
    managedEnvironmentId: environmentId
    configuration: {
      activeRevisionsMode: revisionMode
      dapr:{
        enabled:false
      }
      secrets: [
        {
          name: 'storage-connection'
          value: storageConnectionString
        }
      ]   
    }
    template: {
      revisionSuffix: revisionSuffix
      containers: [
        {
          image: containerImage
          name: containerAppName
          resources: {
            cpu: '0.25'
            memory: '0.5Gi'
          }
          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
param location string = resourceGroup().location
param containerAppName string = 'queue-reader-function'
param containerImage string = 'thara0402/queue-reader-function:0.1.0'
param revisionSuffix string = ''
@allowed([
  'multiple'
  'single'
])
param revisionMode string = 'single'


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

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

module containerapp 'containerapp.bicep' = {
  name: 'container-app'
  params: {
    environmentId: environment.outputs.environmentId
    containerAppName: containerAppName
    containerImage: containerImage
    revisionSuffix: revisionSuffix
    revisionMode: revisionMode
    storageConnectionString: storage.outputs.storageConnectionString
    location: location
  }
}

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

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

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

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


結果確認

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

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

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

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

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

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


まとめ

Azure Container Apps で、KEDA を使って Azure Queue Storage のバックグラウンド処理を構築してみました。
今回のように C# で Queue Trigger のケースは素直に Azure Functions を使ったほうがいいですが、従量課金プランにおける 1 回の実行が 10 分までなどの制限が要件にマッチしないケースでは Container Apps が選択肢になると思います。
Azure Functions Core Tools には func kubernetes install コマンドがあるので、いずれ Container Apps にも対応する可能性があるので、さらに簡単にデプロイできそうです。

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