GoSuda

dotnet aspireにおけるGoサーバーの拡張可能な実行

By snowmerak
views ...

dotnet aspire?

dotnet aspireは、クラウドネイティブ環境の増加に伴い、開発者のクラウドネイティブ開発と設定を支援するために作成されたツールです。このツールは、.NET開発者が.NETプロジェクトや様々なクラウドネイティブインフラストラクチャ、さらに他の言語で書かれたサービスやコンテナなどを簡単にデプロイできるようにします。

当然のことながら、Dockerからk8sまでがリリース・運用され、既存のオンプレミス環境から非常に多くの分野、産業、開発者がクラウドネイティブ環境へ移行しており、移行済みです。今や成熟した分野です。そのため、ホスト名、ポート構成、ファイアウォール、メトリクス管理などにおける従来の不便さについて説明する必要はないと考えます。

したがって、上記の記述からでは、dotnet aspireが何であるか全く掴めないでしょう。なぜなら、これはMicrosoftも正確な定義を下していないからです。そのため、私も特別な定義はしません。ただし、この記事では私が理解したdotnet aspireの基本的な機能を使用しますので、参考にしてご自身の立ち位置を決められると良いでしょう。

プロジェクト構成

dotnet aspire プロジェクトの作成

もしdotnet aspireテンプレートがない場合、まずテンプレートをインストールする必要があります。次のコマンドでテンプレートをインストールします。.NETがない場合は、ご自身でインストールしてください。

1dotnet new install Aspire.ProjectTemplates

そして、適切なフォルダで新しいソリューションを作成します。

1dotnet new sln

その後、ソリューションフォルダで次のコマンドを実行して、aspire-apphostテンプレートのプロジェクトを作成します。

1dotnet new aspire-apphost -o AppHost

すると、設定のための簡単なコードのみが存在するaspire-apphostプロジェクトが作成されます。

Valkeyの追加

それでは、簡単にValkeyを追加してみましょう。

すぐに追加する前に、dotnet aspireはcommunity hostingを通じて様々なサードパーティソリューションを提供しています。当然、Valkeyもこのようなコミュニティホスティングのサポートを受けることができ、次のnugetパッケージを通じて簡単に利用できます。

1dotnet add package Aspire.Hosting.Valkey

その他、多様な統合ホスティングが提供されており、こちらで確認できます。 Valkeyに戻り、AppHostプロジェクトのProgram.csファイルを開き、次のように修正します。

1var builder = DistributedApplication.CreateBuilder(args);
2
3var cache = builder.AddValkey("cache")
4    .WithDataVolume(isReadOnly: false)
5    .WithPersistence(interval: TimeSpan.FromMinutes(5),
6        keysChangedThreshold: 100);
7
8builder.Build().Run();

cacheは、Valkeyサービスをビルドするための情報を持つIResourceBuilderインターフェースの実装です。WithDataVolumeはキャッシュデータを保存するボリュームを作成し、WithPersistenceはキャッシュデータを永続的に保存できるようにします。ここまでの説明を見ると、docker-composevolumesと似た役割を果たすように見えます。当然のことながら、これは皆様も難なく作成できるでしょう。しかし、この記事の範囲を超えるため、ここでは説明しません。

Go言語エコーサーバーの作成

それでは、簡単なGo言語サーバーを追加してみましょう。まず、ソリューションフォルダでgo work initを実行してワークスペースを作成します。 .NET開発者にとってGoワークスペースはソリューションと類似していると考えてください。

そして、EchoServerというフォルダを作成し、その中へ移動してgo mod init EchoServerを実行します。このコマンドによってGoモジュールが作成されます。モジュールは.NET開発者にとってプロジェクトと類似していると認識してください。そして、main.goファイルを作成し、次のように記述します。

 1package main
 2
 3import (
 4	"log"
 5	"net/http"
 6	"os"
 7)
 8
 9func main() {
10	addr := os.Getenv("PORT")
11	log.Printf("Server started on %s", addr)
12
13	http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
14		name := request.URL.Query().Get("name")
15		writer.Write([]byte("Hello, " + name))
16	})
17
18	http.ListenAndServe(":"+addr, nil)
19}

このサーバーは、Aspire AppHostが実行される際に、リッスンすべきPORT環境変数が注入されると、そのポートを読み取ってサーバーを起動します。簡単にnameクエリを受け取り、Hello, {name}を返すサーバーです。

次に、このサーバーをdotnet aspireに追加してみましょう。

エコーサーバーをaspireに追加する

Valkeyを追加したAspire AppHostプロジェクトに戻り、Go言語用のコミュニティホスティングを追加します。

1dotnet add package CommunityToolkit.Aspire.Hosting.Golang

そして、Program.csファイルを開き、次の構文を追加します。

 1var builder = DistributedApplication.CreateBuilder(args);
 2
 3var cache = builder.AddValkey("cache")
 4    .WithDataVolume(isReadOnly: false)
 5    .WithPersistence(interval: TimeSpan.FromMinutes(5),
 6        keysChangedThreshold: 100);
 7
 8var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
 9    .WithHttpEndpoint(port: 3000, env: "PORT")
10    .WithExternalHttpEndpoints();
11
12builder.Build().Run();

ここでechoServerは、Go言語サーバーをビルドできる情報を持つIResourceBuilderインターフェースの実装です。先ほど追加したAddGolangAppメソッドは、Go言語サーバーを追加するためのカスタムホストの拡張メソッドです。固定で3000ポートを使用し、PORT環境変数を注入していることが確認できます。最後にWithExternalHttpEndpointsは、外部からアクセスできるようにするものです。

テストのためにhttp://localhost:3000/?name=worldにアクセスすると、Hello, worldが出力されるのを確認できるでしょう。

しかし、現在のdotnet aspireには、non-dotnetプロジェクトに与えられる重いペナルティがあります。それは、まさに…

プロジェクトの拡張

水平スケーリングはどうすればいいのか?

現在のdotnet aspireは、AddProjectメソッドで追加された.NETプロジェクトのビルダーに対してのみWithReplicaオプションを提供します。しかし、Go言語ホストやAddContainerのような外部プロジェクトに対しては、このオプションを提供しません。

そのため、別途ロードバランサーやリバースプロキシを使用して直接実装する必要があります。しかし、これでは当該リバースプロキシがSPOFになる可能性があるため、リバースプロキシはWithReplicaオプションを提供することが望ましいです。そうなると、必然的にリバースプロキシは.NETプロジェクトである必要があります。

これまで、このような問題に対してnginx、trafik、自作などの方法を用いてきましたが、.NETプロジェクトという制約がかかると、私の手元にはすぐに解決策がありませんでした。そこで、.NETで実装されたリバースプロキシを探したところ、幸いにもYARPという選択肢がありました。 YARPは.NETで実装されたリバースプロキシで、ロードバランサーの役割も果たせ、様々な機能を提供していたため、良い選択だと判断しました。

それでは、YARPを追加してみましょう。

YARPによるリバースプロキシの構成

まず、YARPを使用するためのプロジェクトを作成します。

1dotnet new web -n ReverseProxy

そして、プロジェクトへ移動してYARPをインストールします。

1dotnet add package Yarp.ReverseProxy --version 2.2.0

インストールが完了したら、Program.csファイルを開き、次のように記述します。

 1using Yarp.ReverseProxy.Configuration;
 2
 3var builder = WebApplication.CreateBuilder(args);
 4
 5var routes = new List<RouteConfig>();
 6var clusters = new List<ClusterConfig>();
 7
 8builder.Services.AddReverseProxy()
 9    .LoadFromMemory(routes, clusters);
10
11var app = builder.Build();
12
13app.MapReverseProxy();
14app.Run(url: $"http://0.0.0.0:{Environment.GetEnvironmentVariable("PORT") ?? "5000"}");

このコードはYARPを使用するための基本的なコードです。routesはリバースプロキシが使用するルート情報を、clustersはリバースプロキシが使用するクラスタ情報を格納しています。これらの情報はLoadFromMemoryメソッドによってリバースプロキシにロードされます。最後にMapReverseProxyメソッドを使用してリバースプロキシをマッピングし、実行します。

そして実運用のために、aspire apphostプロジェクトでreverse proxyプロジェクトを参照として追加し、Program.csファイルに次の構文を追加・修正します。

1dotnet add reference ../ReverseProxy
1var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
2    .WithHttpEndpoint(env: "PORT");
3
4var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
5    .WithReference(echoServer)
6    .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
7    .WithExternalHttpEndpoints();

これでリバースプロキシがエコーサーバーを参照できるようになりました。外部からのリクエストはリバースプロキシで受け取り、エコーサーバーに転送する構造に変更されています。

リバースプロキシの修正

まず、リバースプロキシに割り当てられたプロジェクトのリッスンアドレスを変更する必要があります。Properties/launchSettings.jsonファイル内のapplicationUrlを削除します。そしてProgram.csファイルを開き、以下のように大幅に修正します。

 1using Yarp.ReverseProxy.Configuration;
 2
 3var builder = WebApplication.CreateBuilder(args);
 4
 5var routes = new List<RouteConfig>
 6{
 7    new RouteConfig
 8    {
 9        ClusterId = "cluster-echo",
10        RouteId = "route-echo",
11        Match = new RouteMatch
12        {
13            Path = "/"
14        }
15    }
16};
17
18var echoServerAddr = Environment.GetEnvironmentVariable("services__echo-server__http__0") ?? "http://localhost:8080";
19
20var clusters = new List<ClusterConfig>
21{
22    new ClusterConfig
23    {
24        ClusterId = "cluster-echo",
25        Destinations = new Dictionary<string, DestinationConfig>
26        {
27            { "destination-echo", new DestinationConfig { Address = echoServerAddr } }
28        }
29    }
30};
31
32builder.Services.AddReverseProxy()
33    .LoadFromMemory(routes, clusters);
34
35var app = builder.Build();
36
37app.MapReverseProxy();
38app.Run(url: $"http://0.0.0.0:{Environment.GetEnvironmentVariable("PORT") ?? "5000"}");

まず、routesclustersに関する情報を修正します。それぞれecho-routeecho-clusterを追加し、エコーサーバーへリクエストを送信するように設定します。そして、エコーサーバーのアドレスを環境変数から読み取って使用するように修正します。

このアドレスの規則はservices__{service-name}__http__{index}です。エコーサーバーの場合、サービス名がecho-serverであり、単一インスタンスであるため、インデックスとして0を使用します。もしasp .net coreサーバーを追加する場合、WithReplicaを通じて複数のインスタンスが生成されうるため、インデックスを増やして使用することができます。例外処理されているhttp://localhost:8080は意味のないダミー値です。

これでプロジェクトを実行し、http://localhost:3000/?name=worldにアクセスすると、依然としてHello, worldが出力されるのを確認できるでしょう。

拡張アイデア

これで、dotnet aspireにGoサーバーを追加し、リバースプロキシを通じてリクエストを転送することを確認しました。それでは、このプロセスをプログラム的に実装できるように拡張できるでしょう。例えば、エコーサーバーに対してサービス名の後にナンバリングを追加して複数のインスタンスを生成し、リバースプロキシの設定を自動的に追加することができます。

aspire apphostプロジェクトのProgram.csファイルで、リバースプロキシとエコーサーバーを使用するコードを次のように修正します。

 1var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
 2    .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
 3    .WithExternalHttpEndpoints();
 4
 5for (var i = 0; i < 8; i++)
 6{
 7    var echoServer = builder.AddGolangApp($"echo-server-{i}", "../EchoServer")
 8        .WithHttpEndpoint(env: "PORT");
 9    reverseProxy.WithReference(echoServer);
10}

そして、リバースプロキシプロジェクトのProgram.csファイルを次のように修正します。

 1var echoServerDestinations = new Dictionary<string, DestinationConfig>();
 2for (var i = 0; i < 8; i++)
 3{
 4    echoServerDestinations[$"destination-{i}"] = new DestinationConfig
 5    {
 6        Address = Environment.GetEnvironmentVariable($"services__echo-server-{i}__http__0") ?? "http://localhost:8080"
 7    };
 8}
 9
10var clusters = new List<ClusterConfig>
11{
12    new ClusterConfig
13    {
14        ClusterId = "cluster-echo",
15        Destinations = echoServerDestinations
16    }
17};

8つに増えたエコーサーバーインスタンスに対して、送信先設定を追加します。これでリバースプロキシは、増えたエコーサーバーに関する送信先情報を持ち、リクエストを転送できるようになりました。既存のhttp://localhost:3000/?name=worldにアクセスすると、依然としてHello, worldが出力されるのを確認できるでしょう。

終わりに

この記事では、dotnet aspireにGoサーバーを追加し、リバースプロキシを通じてリクエストを転送するプロセスを説明しました。ただし、拡張についてはまだ全てを記述しておらず、環境変数を通じてよりプログラム的に実装できる例を別途リポジトリに作成してあります。詳細なプロジェクト構成とコードは、snowmerak/AspireStartPackを参照してください。

私は個人的に、dotnet aspireがDocker Composeの代替として、そしてクラウドデプロイツールとして、独自の役割を果たすことができると期待しています。すでにDocker Composeやk8sマニフェストを生成するジェネレーターが存在し、一般の開発者がインフラツールへのアクセス性が向上したのではないかと考えています。