ROMANCE DAWN for the new world

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

京都で紅葉狩り

今年の春に訪れた京都の桜が見事だったので紅葉も見てみたくなり、ふらっと錦秋の京都に行ってきました。
gooner.hateblo.jp

瑠璃光院

八瀬にある瑠璃光院は、春と秋の時期だけ公開される完全予約制となっています。書院の2階から望む写経机に映り込む紅葉が見事でした。

f:id:TonyTonyKun:20211204200125j:plain
f:id:TonyTonyKun:20211204200203j:plain
f:id:TonyTonyKun:20211204200142j:plain
f:id:TonyTonyKun:20211204201029j:plain
f:id:TonyTonyKun:20211204201046j:plain
f:id:TonyTonyKun:20211207212300j:plain
f:id:TonyTonyKun:20211207164002j:plain
f:id:TonyTonyKun:20211207164024j:plain
f:id:TonyTonyKun:20211207164054j:plain
f:id:TonyTonyKun:20211207164110j:plain
f:id:TonyTonyKun:20211207164124j:plain

常寂光寺

嵯峨嵐山の小倉山にある常寂光寺は、深紅や黄色に染まるモミジが楽しめました。

f:id:TonyTonyKun:20211204203707j:plain
f:id:TonyTonyKun:20211204203723j:plain
f:id:TonyTonyKun:20211204203739j:plain
f:id:TonyTonyKun:20211204203756j:plain
f:id:TonyTonyKun:20211204203814j:plain
f:id:TonyTonyKun:20211204203831j:plain
f:id:TonyTonyKun:20211204203846j:plain
f:id:TonyTonyKun:20211204203901j:plain
f:id:TonyTonyKun:20211204203917j:plain
f:id:TonyTonyKun:20211204203938j:plain
f:id:TonyTonyKun:20211204203954j:plain
f:id:TonyTonyKun:20211204204011j:plain
f:id:TonyTonyKun:20211204204029j:plain
f:id:TonyTonyKun:20211204204046j:plain
f:id:TonyTonyKun:20211204204101j:plain

醍醐寺

豊臣秀吉が催した「醍醐の花見」でも有名な醍醐寺は、世界文化遺産にも登録されています。

f:id:TonyTonyKun:20211208171045j:plain
f:id:TonyTonyKun:20211204211321j:plain
f:id:TonyTonyKun:20211204211407j:plain
f:id:TonyTonyKun:20211204211425j:plain
f:id:TonyTonyKun:20211204211444j:plain
f:id:TonyTonyKun:20211204211459j:plain
f:id:TonyTonyKun:20211204211516j:plain
f:id:TonyTonyKun:20211204211532j:plain
f:id:TonyTonyKun:20211207212424j:plain
f:id:TonyTonyKun:20211204211622j:plain
f:id:TonyTonyKun:20211207212539j:plain
f:id:TonyTonyKun:20211204211709j:plain
f:id:TonyTonyKun:20211204211724j:plain

醍醐寺(ライトアップ)

醍醐寺は、紅葉の時期に夜間拝観が行われており、照明に照らされた紅葉が楽しめました。

f:id:TonyTonyKun:20211205205344j:plain
f:id:TonyTonyKun:20211205205400j:plain
f:id:TonyTonyKun:20211205205415j:plain
f:id:TonyTonyKun:20211205205431j:plain
f:id:TonyTonyKun:20211205205504j:plain
f:id:TonyTonyKun:20211205205519j:plain

清水寺(ライトアップ)

清水寺の夜間特別拝観は、とても幻想的な雰囲気でした。

f:id:TonyTonyKun:20211207161154j:plain
f:id:TonyTonyKun:20211207161210j:plain
f:id:TonyTonyKun:20211207161239j:plain
f:id:TonyTonyKun:20211207161300j:plain
f:id:TonyTonyKun:20211207161316j:plain
f:id:TonyTonyKun:20211207161333j:plain
f:id:TonyTonyKun:20211207161348j:plain
f:id:TonyTonyKun:20211207161419j:plain
f:id:TonyTonyKun:20211207161434j:plain
f:id:TonyTonyKun:20211207161447j:plain

二尊院

常寂光寺と同じ小倉山にある二尊院は、入り口から続く参道の「紅葉の馬場」が有名です。

f:id:TonyTonyKun:20211204204923j:plain
f:id:TonyTonyKun:20211204204938j:plain
f:id:TonyTonyKun:20211204205001j:plain
f:id:TonyTonyKun:20211204205018j:plain
f:id:TonyTonyKun:20211204205040j:plain
f:id:TonyTonyKun:20211204205058j:plain
f:id:TonyTonyKun:20211204205113j:plain
f:id:TonyTonyKun:20211204205129j:plain

毘沙門堂

京都七福神の一つ毘沙門天を本尊とする毘沙門堂は、勅使坂の敷モミジが有名です。

f:id:TonyTonyKun:20211207212644j:plain
f:id:TonyTonyKun:20211204210254j:plain
f:id:TonyTonyKun:20211204210310j:plain
f:id:TonyTonyKun:20211204210342j:plain
f:id:TonyTonyKun:20211207212721j:plain
f:id:TonyTonyKun:20211204210430j:plain

渡月橋

渡月橋のある嵐山は、常寂光寺や二尊院へと続く通りの紅葉も綺麗でした。

f:id:TonyTonyKun:20211207161818j:plain
f:id:TonyTonyKun:20211207161832j:plain
f:id:TonyTonyKun:20211207161907j:plain
f:id:TonyTonyKun:20211207161923j:plain

もみじのトンネル

叡山電鉄の市原駅~二ノ瀬駅間にある「もみじのトンネル」は、すでに見頃を過ぎていました。もう少し早く来れていれば、ライトアップも見てみたかったです。

f:id:TonyTonyKun:20211207162252j:plain
f:id:TonyTonyKun:20211207162306j:plain
f:id:TonyTonyKun:20211207162320j:plain
f:id:TonyTonyKun:20211207162336j:plain
f:id:TonyTonyKun:20211207162352j:plain
f:id:TonyTonyKun:20211207162410j:plain
f:id:TonyTonyKun:20211207162427j:plain

八瀬もみじの小径

叡山ケーブルのケーブル八瀬駅の横にある八瀬もみじの小径は、回遊路を散策しながら紅葉を楽しめました。

f:id:TonyTonyKun:20211207162915j:plain
f:id:TonyTonyKun:20211207162931j:plain
f:id:TonyTonyKun:20211207162949j:plain
f:id:TonyTonyKun:20211207163028j:plain
f:id:TonyTonyKun:20211207163102j:plain
f:id:TonyTonyKun:20211207163133j:plain

おまけ

祇園四条にあるロシア料理のキエフが美味しかった。お勧めはピロシキとボルシチ。新宿にある姉妹店のスンガリーにも行ってみたい。

f:id:TonyTonyKun:20211207163338j:plainf:id:TonyTonyKun:20211207163353j:plain
f:id:TonyTonyKun:20211207163408j:plainf:id:TonyTonyKun:20211207163601j:plain
f:id:TonyTonyKun:20211207163431j:plainf:id:TonyTonyKun:20211207163446j:plain

Azure Container Apps Preview の Reverse Proxy に YARP を使ってみた

この記事は、NEXTSCAPE Advent Calendar 2021 の 1 日目 の記事です。
qiita.com

先日の Microsoft Ignite で発表された Azure Container Apps で、Reverse Proxy に YARP を使ってみました。
ショッピングサイトのマイクロサービスを想定して、Item Service、Pos Service、Stock Service を API Gateway パターンで公開するシナリオで試したいと思います。

Azure Container Apps における Reverse Proxy の必要性

Azure Container Apps は、サーバーレスなプラットフォームであり、複数のコンテナアプリケーションで構成されるマイクロサービスの構築を支援するサービスです。
HTTP エンドポイントを公開するすべてのマイクロサービスで external な Ingress を構成することは考えにくく、一般的には external な Reverse Proxy を経由させてバックエンドにある internal なマイクロサービスを公開する API Gateway パターンが多いのではないかと思います。

先日の Hack Azure にお邪魔して、このほかにも Container Apps の使いドコロをいろいろと話してきましたので、よかったらご覧ください。

www.youtube.com

YARP とは

YARP ( Yet Another Reverse Proxy ) は、.NET で作られた Reverse Proxy です。
Reverse Proxy や API Gateway には Nginx や Ocelot などのたくさんのソリューションがあります。YARP は ASP.NET Core に統合されているため、C# を使うエンジニアが要件に合わせたカスタマイズや調整を簡単にできるところが嬉しいです。Envoy よりもパフォーマンスが速いという測定結果もでています。
devblogs.microsoft.com

Container Apps Environment を作成する

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

マイクロサービスを作成する

マイクロサービスを ASP.NET Core Web API で作成します。Item Service の API を実装します。API の実装は今回の本題ではないので割愛し、ポイントを絞って説明します。

Reverse Proxy を経由して呼び出すため、https://localhost:xxx/item/api/items のように URL に PathBase (item) が含まれることへの対応が必要です。おもに、Swagger UI への対応となりますが、X-Forwarded-Host ヘッダーと PathBase を使って構成を変更しています。

var pathBase = builder.Configuration.GetValue<string>("PathBase");
if (!String.IsNullOrEmpty(pathBase))
{
    app.UsePathBase(pathBase);
}

app.UseSwagger(c =>
{
    c.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
    {
        if (httpReq.Headers.TryGetValue("X-Forwarded-Host", out var host))
        {
            swaggerDoc.Servers = new List<OpenApiServer> { new OpenApiServer { Url = $"https://{host}{pathBase}" } };
        }
    });
});

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint($"{pathBase}/swagger/v1/swagger.json", "Item Service API V1");
    c.RoutePrefix = string.Empty;
});

PathBase は、appsettings.json で定義しておきます。ローカル開発では PathBase を空にしておき、Azure 環境では環境変数で設定するようにします。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "PathBase": ""
}

詳細は割愛しますが、サービス間の呼び出し履歴を確認したいので、Application Insights の SDK も入れました。最後に、Dockerfile を追加し、Ducker Hub に Image をプッシュしておきます。
同じ要領で、Pos Service と Stock Service も作っておきます。

マイクロサービスをデプロイする

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

まずは、Item Service です。環境変数で、PathBase と Application Insights の InstrumentationKey を指定しています。
同一 Environment に配置された Reverse Proxy からのみ呼び出されることを想定しているので、ingress を internal にしています。

$ az containerapp create -n item-service -g <ResourceGroup Name> \
  -e <Environment Name> -i thara0402/item-service:0.6.0 \
  -v PathBase=/item,APPINSIGHTS_INSTRUMENTATIONKEY=<InstrumentationKey> \
  --ingress internal --target-port 80 \
  --revisions-mode single --scale-rules ./deploy/httpscaler.json \
  --max-replicas 10 --min-replicas 1

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

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

同じ要領で、Pos Service と Stock Service もデプロイしておきます。

Reverse Proxy を作成する

ASP.NET Core の空のプロジェクトを作成し、YARP の NuGet ライブラリをインストールします。

$ Install-Package Yarp.ReverseProxy -Version 1.0.0

www.nuget.org

Program.cs で、YARP の初期化処理を実装します。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
builder.Services.AddApplicationInsightsTelemetry();
var app = builder.Build();
app.MapReverseProxy();
app.Run();

appsettings.json において、YARP のルーティングの構成を定義しておきます。RouteConfig と ClusterConfig に分かれており、HTTP ヘッダーの書き換えや認証など詳細にカスタマイズした構成を組むことができます。
今回は、 Reverse Proxy からバックエンドのマイクロサービスにルーティングするだけのシンプルな構成にしました。

{
  "ReverseProxy": {
    "Routes": {
      "minimumroute": {
        "ClusterId": "minimumcluster",
        "Match": {
          "Path": "{**catch-all}"
        }
      },
      "item-route": {
        "ClusterId": "item-cluster",
        "Match": {
          "Path": "/item/{**catch-all}"
        },
        "Transforms": [
          {
            "PathRemovePrefix": "/item"
          }
        ]
      }
    },
    "Clusters": {
      "minimumcluster": {
        "Destinations": {
          "example.com": {
            "Address": "https://example.com/"
          }
        }
      },
      "item-cluster": {
        "Destinations": {
          "item-service": {
            "Address": "https://item-service.internal.livelysea-b3bd75f6.canadacentral.azurecontainerapps.io"
          }
        }
      }
    }
  }
}

Reverse Proxy にも、Application Insights の SDK を入れました。最後に、Dockerfile を追加し、Ducker Hub に Image をプッシュしておきます。

Reverse Proxy をデプロイする

Azure CLI を使って、Item Service と同じ要領で Reverse Proxy もデプロイします。

$ az containerapp create -n reverse-proxy -g <ResourceGroup Name> \
  -e <Environment Name> -i thara0402/reverse-proxy:0.3.0 \
  -v APPINSIGHTS_INSTRUMENTATIONKEY=<InstrumentationKey> \
  --ingress external --target-port 80 \
  --revisions-mode single --scale-rules ./deploy/httpscaler.json \
  --max-replicas 10 --min-replicas 1

環境変数で、Application Insights の InstrumentationKey を指定しています。
外部からアクセスされる Reverse Proxy なので、ingress を external にしています。

結果確認

ブラウザで Reverse Proxy の URL で Swagger UI を開いて、マイクロサービスの API を呼び出すことができます。

Application Insights の SDK を入れたので、Application Map でこのような結果が表示されます。


まとめ

Azure Container Apps の Reverse Proxy に YARP を使ってみました。このアーキテクチャでの API Gateway パターンはありだと思います。
そうなると、Dapr sidecar を使って YARP からバックエンドのマイクロサービスを呼び出したくなりますが、現段階では非対応のようです。とりあえず、フィードバックしておきました。
github.com

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

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"
}]