ROMANCE DAWN for the new world

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

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

GA 後の記事は、こちらを参照してください。
gooner.hateblo.jp

先日の 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>

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

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


結果確認

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

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

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

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

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

ContainerAppConsoleLogs_CL |
project TimeGenerated, ContainerAppName_s, RevisionName_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 を使ったほうがいいですが、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"
}]

Azure Container Apps Preview で Dapr sidecar を使ってバックエンドサービスを呼び出す

GA 後の記事は、こちらを参照してください。
gooner.hateblo.jp

先日の Microsoft Ignite で発表された Azure Container Apps で、Dapr sidecar を使ってバックエンドサービスを呼び出してみました。

Azure Container Apps の Dapr integration

マイクロサービスで構成されるシステムを構築する場合、分散されたサービス群を管理するためのアーキテクチャや運用の複雑さが増す傾向にあります。
分散アーキテクチャのメリットを活かしながら複雑さを最小限に抑える手法として、Dapr を使ったアプリケーション構築が注目されています。

Dapr はマイクロサービスの sidecar パターンを使っているため、分散アーキテクチャの非機能要件をアプリケーションとは疎結合に切り離して実装できます。

  • サービス呼び出し
  • 状態管理
  • 可観測性
  • シークレット管理
  • Pub / Sub メッセージング など

今回のサービス呼び出しでは、次のような非機能要件への対応が必要となります。

  • 他サービスが配置されている場所(URL)の管理
  • 一時的な障害が発生した場合のリトライ
  • 分散トレーシング

Azure Container Apps では、フルマネージドで管理される Dapr が統合されているため、シンプルに Dapr sidecar をリバース プロキシとして使うことができます。
docs.microsoft.com

Container Apps Environment を作成する

Bicep を使って、App Service Plan にあたる Environment を作成します。
詳細は、こちらの記事を参照してください。
gooner.hateblo.jp

バックエンドサービスを作成する

バックエンドサービスとして、ASP.NET Core Web API を作成します。プロジェクトテンプレートで作成される WeatherForecast API を使います。
Dapr sidecar では HTTPS 通信させることもできますが、今回は [HTTPS 用の構成] チェックボックスは OFF にしておきます。

Dockerfile を追加し、Ducker Hub に Image をプッシュしておきます。

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /code
COPY . .
RUN dotnet restore
RUN dotnet publish --output /output --configuration Release

FROM mcr.microsoft.com/dotnet/aspnet:6.0
COPY --from=build /output /app
WORKDIR /app
ENTRYPOINT ["dotnet", "WebApp.dll"]

フロントエンドサービスを作成する

フロントエンドサービスとして、ASP.NET Core Web アプリケーションを作成します。
Dapr sidecar でサービスを呼び出すため、Dapr の ASP.NET Core 用ライブラリをインストールします。
www.nuget.org

$ Install-Package Dapr.AspNetCore -Version 1.5.0

Program.cs で、DaprClient クラスのインスタンスを DI コンテナーに登録します。

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers().AddDapr();    // ←この行を追加
builder.Services.AddRazorPages();

バックエンドサービスの API レスポンスを受け取るモデルクラスを追加します。

public class WeatherForecast
{
    public DateTime Date { get; set; }

    public int TemperatureC { get; set; }

    public int TemperatureF { get; set; }

    public string Summary { get; set; }
}

Dapr sidecar でサービス呼び出すページを追加し、DaprClient クラスを使って HttpClient クラスを生成します。Method Invoke や gRPC で呼び出すこともできます。
Web API の呼び出しでは、Dapr に登録する app id の dapr-backend を使った URL を指定しています。Dapr sidecar をリバース プロキシに使うことで、バックエンドサービスの場所を管理する必要がなくなります。

public class DaprModel : PageModel
{
    private readonly DaprClient _daprClient;

    public DaprModel(DaprClient daprClient)
    {
        _daprClient = daprClient;
    }

    public async Task OnGet()
    {
        var url = "http://dapr-backend/WeatherForecast";
        var httpClient = DaprClient.CreateInvokeHttpClient();
        var json = await httpClient.GetStringAsync(url);
        var options = new JsonSerializerOptions()
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
        var forecasts = JsonSerializer.Deserialize<IEnumerable<WeatherForecast>>(json, options);

        ViewData["WeatherForecastData"] = forecasts;
    }
}

WeatherForecast API のレスポンスを View にバインドします。

@page
@using WebApp.Models
@model WebApp.Pages.DaprModel
@{
    ViewData["Title"] = "Dapr page";
}

<div class="text-center">
    <h1 class="display-4">Service to Service calls</h1>

    @foreach (var forecast in (IEnumerable<WeatherForecast>)ViewData["WeatherForecastData"])
    {
        <p>The forecast for @forecast.Date is @forecast.Summary!</p>
    }
</div>

ここまでのコードが書けたら、バックエンドサービスと同様に Dockerfile を追加し、Ducker Hub に Image をプッシュしておきます。

Container Apps をデプロイする

Azure CLI を使って、Container Apps をデプロイします。
まだ Preview 中なので、拡張モジュールをインストールする必要があります。

$ az extension add --source https://workerappscliextension.blob.core.windows.net/azure-cli-extension/containerapp-0.2.0-py2.py3-none-any.whl
$ az provider register --namespace Microsoft.Web

まずは、バックエンドサービスです。最終行のオプションで Dapr を有効化して app idport を指定します。
同一 Environment に配置されたサービスからのみ呼び出されることを想定しているので、ingress を internal にしています。

$ az containerapp create -n dapr-backend -g <ResourceGroup Name> \
     -e <Environment Name> -i thara0402/dapr-backend:0.1.0 \
     --ingress internal --target-port 80 \
     --revisions-mode single --scale-rules ./deploy/httpscaler.json \
     --max-replicas 10 --min-replicas 1 \
     --enable-dapr --dapr-app-id dapr-backend --dapr-app-port 80

httpscaler.json には、HTTP リクエスト数によるスケーリングルールを定義しました。

[{
    "name": "http-scaling-rule",
     "type": "http",
    "metadata": {
        "concurrentRequests": "10"
    }
}]

デプロイした結果は、このように Azure ポータルで確認できます。

続いて、フロントエンドサービスをデプロイします。最終行のオプションで Dapr を構成している部分は、バックエンドサービスと同じです。
ブラウザからアクセスされる Web アプリケーションなので、ingress を external にしています。

$ az containerapp create -n dapr-frontend -g <ResourceGroup Name> \
     -e <Environment Name> -i thara0402/dapr-frontend:0.9.0 \
     --ingress external --target-port 80 \
     --revisions-mode single --scale-rules ./deploy/httpscaler.json \
     --max-replicas 10 --min-replicas 1 \
     --enable-dapr --dapr-app-id dapr-frontend --dapr-app-port 80

結果確認

フロントエンドの Web アプリケーションを開いて、バックエンドの WeatherForecast API を呼び出します。

Container Apps Environment を作成する際に、Application Insights を関連付けているので、Application Map でこのような結果が表示されます。


まとめ

Azure Container Apps で Dapr sidecar を使ってバックエンドサービスを呼び出してみました。Kubernetes や Dapr のインフラ周りを気にせず、シンプルに Dapr sidecar をリバース プロキシとして使うことができます。
Azure 環境はシンプルになりましたが、ローカル開発環境では Dapr CLI と Docker Compose を使うなどの煩雑さが残ります。Azure Container Apps 向けの Project Tye とか出来ると良さそうですね。

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

Azure Container Apps Preview で Blue-Green Deployments を試してみた

GA 後の記事は、こちらを参照してください。
gooner.hateblo.jp

先日の Microsoft Ignite で発表された Azure Container Apps で、Blue-Green Deployments を試してみました。

Azure Container Apps とは

Azure Container Apps は、複数のコンテナアプリで構成されるシステム向けのサービスです。これまでは Web Apps for Containers か AKS の2択でしたが、新たに Container Apps が追加されたことになります。
docs.microsoft.com

Container Apps は Kubernetes を基盤に構成されています。とりあえずは、下記のような Environment / Container App / Revision / Pod の構成要素を理解しておけば OK です。

Microsoft Docs から引用

Container Apps Environment を作成する

Bicep を使って、App Service Plan にあたる Environment を作成します。
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

main.bicep では、Resource Group 名にプレフィックスをつけて Environment 名を定義します。

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

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

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

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

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


Container Apps をデプロイする

Azure CLI を使って、Container Apps をデプロイします。
まだ Preview 中なので、拡張モジュールをインストールする必要があります。

$ az extension add --source https://workerappscliextension.blob.core.windows.net/azure-cli-extension/containerapp-0.2.0-py2.py3-none-any.whl
$ az provider register --namespace Microsoft.Web

あらかじめ ASP.NET Core Web アプリケーションの Docker Image を作っておいたので、Container Apps にデプロイします。
オプションの revisions-mode に multiple を指定する部分がポイントです。ちなみに In-place upgrade を行う場合は、single を指定します。

$ az containerapp create -n dapr-frontend -g <ResourceGroup Name> \
     -e <Environment Name> -i thara0402/dapr-frontend:0.1.0 \
     --ingress external --target-port 80 \
     --revisions-mode multiple --scale-rules ./deploy/httpscaler.json \
     --max-replicas 10 --min-replicas 1

httpscaler.json には、HTTP リクエスト数によるスケーリングルールを定義しました。

[{
    "name": "http-scaling-rule",
     "type": "http",
    "metadata": {
        "concurrentRequests": "10"
    }
}]

デプロイすると、dapr-frontend--h2qwxun という Revision が作成されました。

dapr-frontend.blackcliff-bbde75ab.canadacentral.azurecontainerapps.io という FQDN が自動生成されました。
ASP.NET Core Web アプリケーションがブラウザで表示されることを確認できます。


Blue-Green Deployments を行う

新しいバージョンをデプロイします。Staging へのデプロイを想定し、トラフィック制御では先ほど作成された Revision( dapr-frontend--h2qwxun)に「100」、latestに「0」を指定します。

$ az containerapp update -n dapr-frontend -g <ResourceGroup Name> \
     -i thara0402/dapr-frontend:0.2.0 \
     --traffic-weight dapr-frontend--h2qwxun=100,latest=0

デプロイすると、dapr-frontend--ugfijqv という Revision が追加されました。

トラフィック制御が「0」の新しい Revision には、ユーザーに公開される FQDN とは異なる FQDN が割り当てられるので、リリース前のテストができます。
dapr-frontend--ugfijqv.blackcliff-bbde75ab.canadacentral.azurecontainerapps.io という Revision が含まれた FQDN が自動生成されました。

リリース前のテストが完了したので、Swap します。

$ az containerapp update -n dapr-frontend -g <ResourceGroup Name> \
     --traffic-weight dapr-frontend--h2qwxun=0,latest=100

トラフィック制御で、「100」と「0」の指定が入れ替わったことを確認できます。

ユーザーに公開される FQDN にアクセスすると、V2 に更新された ASP.NET Core Web アプリケーションが表示されます。

最後に、旧バージョンの Revision をシャットダウンします。アクティブのままだと、課金対象となってしまいます。

$ az containerapp revision deactivate --app dapr-frontend -g <ResourceGroup Name> \
     --name dapr-frontend--h2qwxun

新バージョンの Revision のみになったことを確認できます。


まとめ

Azure Container Apps で Blue-Green Deployments を試してみました。トラフィック制御の比率を変えることで AB テストやカナリアリリースもできます。GA までにはもう少し簡単にできるようになるといいですね。
Envoy によるトラフィック制御以外にも、KEDA によるスケーリングや Dapr によるサイドカーなどの面白い機能があるので、引き続き試していきたいと思います。