ROMANCE DAWN for the new world

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

Azure Container Apps の Revision Label を使って Staging の URL を固定する

Azure Container Apps の Revision は、Web Apps の Deployment Slot とは異なり、新しい Docker Image をデプロイするたびに Revision が追加されます。
Revision Label を使うことで、Staging の URL を固定できるかどうか試してみました。
docs.microsoft.com

Container Apps をデプロイする

前回の記事と同様の手順で、Bicep で Container Apps をデプロイします。
gooner.hateblo.jp

Staging へのデプロイを想定し、新しいバージョンをデプロイしました。dapr-frontend--v2 という Revision が追加されました。

新しい Revision には、Container Apps とは別の Revison URL が割り当てられるので、リリース前のテストができます。

この Revision 毎に割り当てられる URL を固定させるため、Revison Label に staging をセットします。

Revision URL とは別に、Label URL が割り当てられます。


Revision を Swap する

Revision の traffic パーセンテージを Swap します。

$ az containerapp ingress traffic set -n dapr-frontend -g <ResourceGroup Name> \
     --revision-weight dapr-frontend--v1=0 latest=100

Swap が完了したら、Revison Label を dapr-frontend--v2 から dapr-frontend--v1 に付け替えます。

最後に、旧バージョンの Revision をシャットダウンします。

$ az containerapp revision deactivate -n dapr-frontend -g <ResourceGroup Name> --revision dapr-frontend--v1

あとは、この繰り返しで。

まとめ

Azure Container Apps の Revision Label を使って Staging の URL を固定してみました。
ひと手間かかりますが、Web Apps の Deployment Slot に近い運用を実現できそうです。

本当は、Azure CLI で Revision Label を追加削除したかったのですが、挙動が怪しかったので Azure Portal を使いました。
具体的には、traffic パーセンテージが 0% の Revison に対して、Revison Label を追加できません。

$ az containerapp revision label add -n dapr-frontend -g <ResourceGroup Name> --label staging --revision dapr-frontend--v2
Please specify a revision name with an associated traffic weight.

GitHub の Issues に報告したところ、次のリリースで修正されるようです。
github.com

// 追記:2022/06/15
Azure CLI の Ver.0.3.6 で修正が完了しました。--no-prompt オプションを指定することで、Revision Label を削除追加せずとも、強制的に付け替えできます。

// Revision Label(staging) を新バージョンにセット
az containerapp revision label add -n dapr-frontend -g <ResourceGroup Name> --label staging --revision dapr-frontend--v2

// スワップ
az containerapp ingress traffic set -n dapr-frontend -g <ResourceGroup Name> --revision-weight dapr-frontend--v1=0 latest=100

// Revision Label(staging) を付け替え
az containerapp revision label add -n dapr-frontend -g <ResourceGroup Name> --label staging --revision dapr-frontend--v1 --no-prompt

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

Microsoft Build 2022 で Azure Container Apps が GA されました。東日本リージョンでも使えるようになりましたので、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 / Replica の構成要素を理解しておけば OK です。

Microsoft Docs から引用

Container Apps Environment を作成する

Azure Portal では App Service Plan にあたる Environment だけを作成できないため、Bicep を使って 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' = {
  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

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

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

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

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

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

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


Container Apps をデプロイする

Container Apps についても、Azure Portal では細かい構成を指定できないので、Bicep を使って作成します。
あらかじめ ASP.NET Core Web アプリケーションの Docker Image を作っておいたので、apps.bicep でデプロイします。

param containerAppName string
param location string = resourceGroup().location
param environmentId string
param containerImage string
param revisionSuffix string
param oldRevisionSuffix string
param isExternalIngress bool

@allowed([
  'multiple'
  'single'
])
param revisionMode string = 'single'

resource containerApp 'Microsoft.App/containerApps@2022-03-01' = {
  name: containerAppName
  location: location
  properties: {
    managedEnvironmentId: environmentId
    configuration: {
      activeRevisionsMode: revisionMode
      ingress: {
        external: isExternalIngress
        targetPort: 80
        transport: 'auto'
        allowInsecure: false
        traffic: ((contains(revisionSuffix, oldRevisionSuffix)) ? [
          {
            weight: 100
            latestRevision: true
          }
        ] : [
          {
            weight: 0
            latestRevision: true
          }
          {
            weight: 100
            revisionName: '${containerAppName}--${oldRevisionSuffix}'
          }
        ])
      }
      dapr:{
        enabled:false
      }
    }
    template: {
      revisionSuffix: revisionSuffix
      containers: [
        {
          image: containerImage
          name: containerAppName
          resources: {
            cpu: '0.25'
            memory: '0.5Gi'
          }
        }
      ]
      scale: {
        minReplicas: 1
        maxReplicas: 10
        rules: [
          {
            name: 'http-scaling-rule'
            http: {
              metadata: {
                concurrentRequests: '10'
              }
            }
          }
        ]
      }
    }
  }
}

output fqdn string = containerApp.properties.configuration.ingress.fqdn

main.bicep では、Blue-Green Deployments を行うため、revisions-mode に multiple を指定します。
新旧 RevisionSuffix に v1 を指定することで、デプロイしたアプリケーションの traffic を 100% にします。

param environmentName string = 'env-${resourceGroup().name}'
param containerAppName string = 'dapr-frontend'
param location string = resourceGroup().location
param containerImage string = 'thara0402/dapr-frontend:0.1.0'
param revisionSuffix string = 'v1'
param oldRevisionSuffix string = 'v1'
param isExternalIngress bool = true
param revisionMode string = 'multiple'

resource environment 'Microsoft.App/managedEnvironments@2022-03-01' existing = {
  name: environmentName
}

module apps 'apps.bicep' = {
  name: 'container-apps'
  params: {
    containerAppName: containerAppName
    location: location
    environmentId: environment.id
    containerImage: containerImage
    revisionSuffix: revisionSuffix
    oldRevisionSuffix: oldRevisionSuffix
    revisionMode: revisionMode
    isExternalIngress: isExternalIngress
  }
}

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

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

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

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


Blue-Green Deployments を行う

Staging へのデプロイを想定し、新しいバージョンをデプロイします。
main.bicep では、containerImage のタグを 0.2.0、revisionSuffix のバージョンを v2 に更新することで、先ほどデプロイした Revision( dapr-frontend--v1)へのトラフィックを 100% のまま、新しい Revision をデプロイします。

param containerImage string = 'thara0402/dapr-frontend:0.2.0'
param revisionSuffix string = 'v2'

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

traffic が「0」の新しい Revision には、ユーザーに公開される FQDN とは異なる FQDN が割り当てられるので、リリース前のテストができます。
dapr-frontend--v2.bravewave-d4e6d1bd.japaneast.azurecontainerapps.io/ という Revision が含まれた FQDN が自動生成されました。

リリース前のテストが完了したので、Azure CLI を使って Swap します。
拡張モジュールをインストールし、名前空間を登録します。Preview のときは Microsoft.Web 名前空間でしたが、Microsoft.App 名前空間に移行されています。

$ az extension add --name containerapp --upgrade
$ az provider register --namespace Microsoft.App
$ az containerapp ingress traffic set -n dapr-frontend -g <ResourceGroup Name> \
     --revision-weight dapr-frontend--v1=0 latest=100

traffic のパーセンテージが入れ替わったことを確認できます。

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

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

$ az containerapp revision deactivate -n dapr-frontend -g <ResourceGroup Name> --revision dapr-frontend--v1

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


まとめ

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

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

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