---
id: 7ecf5f1e3335910ea27e6beb68f04ae7
author: snowmerak
title: dotnet Aspireで拡張可能なGoサーバーを実行してみる
description: '.NET Aspire で Go サーバーをスケーラブルに実行する方法: YARP リバースプロキシを用いた水平スケーリングと自動化構成の例.'
language: ja
date: 2025-01-05T05:04:22.744778105Z
path: /blog/posts/exploring-the-scalable-execution-of-go-servers-in-net-aspire-z42f3b6ee
category: network
---

## dotnet aspire?

dotnet aspireは、クラウドネイティブ環境の普及に伴い、開発者のクラウドネイティブ開発および構成を支援するために作られたツールです。このツールは、.NET開発者が.NETプロジェクトやさまざまなクラウドネイティブインフラ、そして他の言語で書かれたサービスやコンテナなどを容易にデプロイできるようにします。

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

そのため、上記の説明だけでは、dotnet aspireが何なのか到底つかめないと思います。なぜなら、これはMicrosoftも正確な定義を示していないからです。ですので、私も特別な定義は下しません。ただし、この記事では私が理解したdotnet aspireの基本的な機能を用いますので、それを参考にして、ご自身なりの位置づけを決めていただければと思います。

## プロジェクト構成

### dotnet aspireプロジェクトの作成

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

```bash
dotnet new install Aspire.ProjectTemplates
```

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

```bash
dotnet new sln
```

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

```bash
dotnet new aspire-apphost -o AppHost
```

すると、セットアップのための簡単なコードだけが存在するaspire-apphostプロジェクトが作成されます。

### Valkeyの追加

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

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

```bash
dotnet add package Aspire.Hosting.Valkey
```

その他にも、さまざまな統合ホスティングが提供されていますので、[こちら](https://learn.microsoft.com/ko-kr/dotnet/aspire/fundamentals/integrations-overview)で確認できます。
それではValkeyに戻り、AppHostプロジェクトのProgram.csファイルを開いて、次のように修正します。

```csharp
// 組み込みの設定を用いてビルダーを作成する
var builder = DistributedApplication.CreateBuilder(args);

// Valkeyサービスを追加し、データボリュームと永続化を設定する
var cache = builder.AddValkey("cache")
    .WithDataVolume(isReadOnly: false)
    .WithPersistence(interval: TimeSpan.FromMinutes(5),
        keysChangedThreshold: 100);

builder.Build().Run();
```

`cache`は、valkeyサービスをビルドするための情報を持つ`IResourceBuilder`インターフェースの実装体です。
`WithDataVolume`はキャッシュデータを保存するためのボリュームを作成し、`WithPersistence`はキャッシュデータを継続的に保存できるようにします。
ここまで見ると、`docker-compose`の`volumes`と似た役割を果たしているように見えます。
当然のことながら、これは皆さまも難なく作成できるでしょう。
ただし、この記事の範囲を超えるため、今は取り上げません。

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

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

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

```go
// パッケージmainを宣言する
package main

import (
	"log"
	"net/http"
	"os"
)

func main() {
	// PORT環境変数からリッスンするポートを取得する
	addr := os.Getenv("PORT")
	log.Printf("Server started on %s", addr)

	// ルートパスへのリクエストに対してnameクエリを読み取り、応答を返す
	http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
		name := request.URL.Query().Get("name")
		writer.Write([]byte("Hello, " + name))
	})

	// 指定されたポートでHTTPサーバーを起動する
	http.ListenAndServe(":"+addr, nil)
}
```

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

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

### エコーサーバーをAspireに追加する

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

```bash
dotnet add package CommunityToolkit.Aspire.Hosting.Golang
```

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

```csharp
// 組み込みの設定を用いてビルダーを作成する
var builder = DistributedApplication.CreateBuilder(args);

// Valkeyサービスを追加し、データボリュームと永続化を設定する
var cache = builder.AddValkey("cache")
    .WithDataVolume(isReadOnly: false)
    .WithPersistence(interval: TimeSpan.FromMinutes(5),
        keysChangedThreshold: 100);

// Goアプリケーションを追加し、PORT環境変数を注入して外部からアクセス可能にする
var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
    .WithHttpEndpoint(port: 3000, env: "PORT")
    .WithExternalHttpEndpoints();

builder.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`のような外部プロジェクトについては、このオプションを提供していません。

そのため、別途load balancerやreverse proxyを使用して直接実装しなければなりません。
しかし、そうすると当該reverse proxyがSPOFになりうるため、reverse proxyには`WithReplica`オプションを提供してもらうのが望ましいです。
そうなると、必然的にreverse proxyは.NETプロジェクトでなければなりません。

これまで、このような問題に対してnginx、traefik、直接実装などの方法を使ってきましたが、.NETプロジェクトという制約がかかると、私の手元にはすぐに使える方法がありませんでした。
そこで、.NETで実装されたreverse proxyを探したところ、幸いにも[YARP](https://microsoft.github.io/reverse-proxy/)という選択肢がありました。
YARPは.NETで実装されたreverse proxyで、load balancerの役割も果たすことができ、さまざまな機能を提供しているため、良い選択だと判断しました。

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

### YARPによるreverse proxyの構成

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

```bash
dotnet new web -n ReverseProxy
```

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

```bash
dotnet add package Yarp.ReverseProxy --version 2.2.0
```

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

```csharp
// YARPの構成型を参照する
using Yarp.ReverseProxy.Configuration;

// 組み込みの設定を用いてWebアプリケーションのビルダーを作成する
var builder = WebApplication.CreateBuilder(args);

// ルートとクラスターの情報を格納するリストを作成する
var routes = new List<RouteConfig>();
var clusters = new List<ClusterConfig>();

// メモリ上の設定からreverse proxyを読み込む
builder.Services.AddReverseProxy()
    .LoadFromMemory(routes, clusters);

var app = builder.Build();

// reverse proxyをマッピングし、PORT環境変数で指定されたポートで実行する
app.MapReverseProxy();
app.Run(url: $"http://0.0.0.0:{Environment.GetEnvironmentVariable("PORT") ?? "5000"}");
```

このコードは、YARPを使用するための基本的なコードです。
`routes`はreverse proxyが使用するルート情報を、`clusters`はreverse proxyが使用するクラスター情報を保持しています。
これらの情報は`LoadFromMemory`メソッドによってreverse proxyに読み込まれます。
最後に、`MapReverseProxy`メソッドを使用してreverse proxyをマッピングし、実行します。

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

```bash
dotnet add reference ../ReverseProxy
```

```csharp
// Goアプリケーションを追加し、PORT環境変数を注入する
var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
    .WithHttpEndpoint(env: "PORT");

// reverse proxyプロジェクトを追加し、echoServerを参照させ、外部からアクセス可能にする
var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
    .WithReference(echoServer)
    .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
    .WithExternalHttpEndpoints();
```

これで、reverse proxyがecho serverを参照できるようになりました。
外部から入ってくるリクエストはreverse proxyで受け取り、echo serverへ受け渡す構造に変わりつつあります。

### reverse proxyの修正

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

```csharp
// YARPの構成型を参照する
using Yarp.ReverseProxy.Configuration;

// 組み込みの設定を用いてWebアプリケーションのビルダーを作成する
var builder = WebApplication.CreateBuilder(args);

// echo serverへリクエストを送るためのルート設定を作成する
var routes = new List<RouteConfig>
{
    new RouteConfig
    {
        ClusterId = "cluster-echo",
        RouteId = "route-echo",
        Match = new RouteMatch
        {
            Path = "/"
        }
    }
};

// 環境変数からecho serverのアドレスを読み取る（存在しない場合は既定値を使用する）
var echoServerAddr = Environment.GetEnvironmentVariable("services__echo-server__http__0") ?? "http://localhost:8080";

// echo serverを宛先とするクラスター設定を作成する
var clusters = new List<ClusterConfig>
{
    new ClusterConfig
    {
        ClusterId = "cluster-echo",
        Destinations = new Dictionary<string, DestinationConfig>
        {
            { "destination-echo", new DestinationConfig { Address = echoServerAddr } }
        }
    }
};

// メモリ上の設定からreverse proxyを読み込む
builder.Services.AddReverseProxy()
    .LoadFromMemory(routes, clusters);

var app = builder.Build();

// reverse proxyをマッピングし、PORT環境変数で指定されたポートで実行する
app.MapReverseProxy();
app.Run(url: $"http://0.0.0.0:{Environment.GetEnvironmentVariable("PORT") ?? "5000"}");
```

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

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

それでは、プロジェクトを実行して`http://localhost:3000/?name=world`にアクセスすると、相変わらず`Hello, world`が出力されることを確認できるでしょう。

### 拡張のアイデア

これで、dotnet aspireにGoサーバーを追加し、reverse proxyを通じてリクエストを転送することを確認しました。
では、この過程をprogrammaticに実装できるように拡張することができるでしょう。
例えば、echo serverについてサービス名の後ろに番号を付けて複数のインスタンスを作成し、reverse proxyの設定を自動的に追加することができます。

aspire apphostプロジェクトのProgram.csファイルで、reverse proxyとecho serverを利用するコードを次のように修正します。

```csharp
// reverse proxyプロジェクトを追加し、外部からアクセス可能にする
var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
    .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
    .WithExternalHttpEndpoints();

// 8つのecho serverインスタンスを作成し、それぞれをreverse proxyに参照させる
for (var i = 0; i < 8; i++)
{
    var echoServer = builder.AddGolangApp($"echo-server-{i}", "../EchoServer")
        .WithHttpEndpoint(env: "PORT");
    reverseProxy.WithReference(echoServer);
}
```

そして、reverse proxyプロジェクトのProgram.csファイルを次のように修正します。

```csharp
// 8つのecho serverインスタンスそれぞれの宛先設定を格納する辞書を作成する
var echoServerDestinations = new Dictionary<string, DestinationConfig>();
for (var i = 0; i < 8; i++)
{
    // 環境変数から各インスタンスのアドレスを読み取って宛先として登録する（存在しない場合は既定値を使用する）
    echoServerDestinations[$"destination-{i}"] = new DestinationConfig
    {
        Address = Environment.GetEnvironmentVariable($"services__echo-server-{i}__http__0") ?? "http://localhost:8080"
    };
}

// 8つの宛先をまとめたクラスター設定を作成する
var clusters = new List<ClusterConfig>
{
    new ClusterConfig
    {
        ClusterId = "cluster-echo",
        Destinations = echoServerDestinations
    }
};
```

8つに増えたecho serverインスタンスについて、宛先の設定を追加しました。
これで、reverse proxyは増加したecho serverの宛先情報を保持し、リクエストを転送できるようになりました。
従来の`http://localhost:3000/?name=world`にアクセスすると、相変わらず`Hello, world`が出力されることを確認できるでしょう。

## おわりに

この記事では、dotnet aspireにGoサーバーを追加し、reverse proxyを通じてリクエストを転送する過程を説明しました。
ただし、拡張についてはまだすべてを書いてはおらず、環境変数を通じてよりprogrammaticに実装できる例を別のリポジトリに作成しておきました。
詳しいプロジェクト構成とコードについては、[snowmerak/AspireStartPack](https://github.com/snowmerak/AspireStarterPack)をご参照ください。

私個人としては、dotnet aspireがdocker composeの代替として、またクラウドデプロイツールとして、独自の役割を果たせるものと期待しています。
すでにdocker composeやk8s manifestを生成する[ジェネレーター](https://prom3theu5.github.io/aspirational-manifests/generate-command.html)も存在しており、一般の開発者がインフラストラクチャツールにより容易にアクセスできるようになったのではないかと考えています。
