GoSuda

dotnet aspire içinde ölçeklenebilir bir şekilde Go sunucusu çalıştırmayı deneme

By snowmerak
views ...

dotnet aspire?

dotnet aspire, bulut tabanlı ortamların artmasıyla birlikte, geliştiricilerin bulut tabanlı geliştirme ve yapılandırmalarına yardımcı olmak amacıyla oluşturulmuş bir araçtır. Bu araç, .NET geliştiricilerinin .NET projelerini ve çeşitli bulut tabanlı altyapıları, ayrıca diğer dillerdeki hizmetleri veya container'ları kolayca dağıtmasını sağlar.

Elbette docker'dan k8s'ye kadar yayınlanır ve işletilir; mevcut şirket içi (on-premise) ortamlardan oldukça fazla sayıda sektör, endüstri ve geliştirici bulut tabanlı ortamlara geçiş yapmakta veya geçiş yapmış durumdadır. Artık olgunlaşmış bir alandır. Bu nedenle, host adları, port yapılandırması, güvenlik duvarı, metrik yönetimi gibi konularda mevcut rahatsızlıkları açıklamanın gereksiz olduğunu düşünüyorum.

Bu nedenle, yukarıdaki açıklamalardan bile dotnet aspire'ın ne olduğu konusunda bir fikir sahibi olmanız zor olabilir. Çünkü Microsoft da net bir tanım yapmamaktadır. Bu yüzden ben de ayrı bir tanım yapmayacağım. Ancak, bu yazıda anladığım kadarıyla dotnet aspire'ın temel işlevlerini kullanacağım, bu nedenle referans alarak kendi konumunuzu belirlemeniz iyi olacaktır.

Proje Yapılandırması

dotnet aspire Projesi Oluşturma

Eğer dotnet aspire şablonu yoksa, öncelikle şablonu yüklemeniz gerekir. Aşağıdaki komut ile şablonu yükleyin. Eğer .net yoksa, onu kendiniz kurmanız gerekir.

1dotnet new install Aspire.ProjectTemplates

Ardından uygun bir klasörde yeni bir çözüm oluşturun.

1dotnet new sln

Daha sonra, çözüm klasöründe aşağıdaki komutu çalıştırarak aspire-apphost şablonunun projesini oluşturun.

1dotnet new aspire-apphost -o AppHost

Bu, ayarlama için sadece basit kodların bulunduğu bir aspire-apphost projesi oluşturacaktır.

Valkey Ekleme

Şimdi basitçe Valkey ekleyelim.

Hemen eklemeden önce belirtmek gerekir ki, dotnet aspire, community hosting aracılığıyla çeşitli üçüncü taraf çözümler sunmaktadır. Elbette valkey de bu community hosting desteğinden yararlanabilir ve aşağıdaki nuget paketi aracılığıyla kolayca kullanılabilir.

1dotnet add package Aspire.Hosting.Valkey

Bunun dışında çeşitli entegre hosting seçenekleri de sunmaktadır; buradan kontrol edebilirsiniz. Tekrar valkey'e dönersek, AppHost projesindeki Program.cs dosyasını açıp aşağıdaki gibi düzenleyin.

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 hizmetini inşa edebilecek bilgilere sahip IResourceBuilder arayüzünün bir uygulamasıdır.WithDataVolume, önbellek verilerini saklayacak bir birim oluşturur ve WithPersistence, önbellek verilerinin sürekli olarak saklanmasını sağlar. Buraya kadar bakıldığında, docker-compose'daki volumes ile benzer bir rol üstlendiği görülmektedir. Elbette, bunu sizin de zorlanmadan yapabileceğiniz görülmektedir. Ancak bu yazının kapsamını aştığı için şimdilik bahsetmeyeceğim.

Go Dilinde Echo Sunucusu Oluşturma

Şimdi basit bir Go dili sunucusu ekleyelim. Öncelikle çözüm klasöründe go work init komutu ile bir çalışma alanı oluşturalım. .NET geliştiricileri için Go çalışma alanı, çözüme benzer bir şey olarak düşünülebilir.

Ardından EchoServer adında bir klasör oluşturup içine girdikten sonra go mod init EchoServer komutunu çalıştırın. Bu komut ile bir Go modülü oluşturulur. Modül, .NET geliştiricileri için projeye benzer bir şey olarak kabul edilebilir. Sonra main.go dosyasını oluşturup aşağıdaki gibi yazın.

 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}

Bu sunucu, Aspire AppHost çalıştırıldığında, dinlemesi gereken PORT ortam değişkenini enjekte eder ve bu portu okuyarak sunucuyu çalıştırır. Basitçe name sorgusunu alıp Hello, {name} döndüren bir sunucudur.

Şimdi bu sunucuyu dotnet aspire'a ekleyelim.

Echo Sunucusunu Aspire'a Ekleme

Tekrar Valkey'i eklediğimiz Aspire AppHost projesine dönerek Go dili için community hosting'i ekleyelim.

1dotnet add package CommunityToolkit.Aspire.Hosting.Golang

Ardından Program.cs dosyasını açarak aşağıdaki ifadeleri ekleyin.

 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();

Burada echoServer, Go dili sunucusunu inşa edebilecek bilgilere sahip IResourceBuilder arayüzünün bir uygulamasıdır. Az önce eklediğimiz AddGolangApp metodu, Go dili sunucusu eklemek için özel bir host'un genişletme metodudur. Sabit olarak 3000 portunu kullanır ve PORT ortam değişkenini enjekte ettiğini görebilirsiniz. Son olarak, WithExternalHttpEndpoints, dışarıdan erişilebilir olmasını sağlar.

Test için http://localhost:3000/?name=world adresine eriştiğinizde, Hello, world çıktısını göreceksiniz.

Ancak şu anda dotnet aspire'da, dotnet harici projelere uygulanan ağır bir ceza bulunmaktadır. O da şudur...

Proje Genişletme

Peki ya Yatay Ölçeklendirme Nasıl Yapılır?

Şu anda dotnet aspire, AddProject metoduyla eklenen .NET projelerinin builder'ları için yalnızca WithReplica seçeneğini sunmaktadır. Ancak Go dili host veya AddContainer gibi harici projeler için bu seçeneği sunmamaktadır.

Bu nedenle, ayrı bir load balancer veya reverse proxy kullanarak doğrudan uygulamanız gerekir. Ancak bu durumda, söz konusu reverse proxy bir SPOF (Single Point of Failure) olabileceği için, reverse proxy'nin WithReplica seçeneği sunması daha iyi olacaktır. Bu durumda, reverse proxy'nin zorunlu olarak bir .NET projesi olması gerekmektedir.

Şimdiye kadar bu sorun için nginx, trafik, doğrudan uygulama gibi yöntemler kullanılmış olsa da, .NET projesi gibi bir sınırlama getirildiğinde, şu anda benim elimde bir çözüm yoktu. Bu nedenle, .NET ile uygulanmış bir reverse proxy aradım ve şans eseri YARP adında bir seçenek buldum. YARP, .NET ile uygulanmış bir reverse proxy olup, load balancer görevi de görebiliyor ve çeşitli özellikler sunduğu için iyi bir seçenek olduğuna karar verdim.

O zaman şimdi YARP'ı ekleyelim.

YARP ile Reverse Proxy Yapılandırması

Öncelikle YARP'ı kullanmak için bir proje oluşturalım.

1dotnet new web -n ReverseProxy

Ardından projeye girerek YARP'ı yükleyin.

1dotnet add package Yarp.ReverseProxy --version 2.2.0

Kurulum bittikten sonra, Program.cs dosyasını açarak aşağıdaki gibi yazın.

 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"}");

Bu kod, YARP kullanmak için temel bir koddur.routes, reverse proxy'nin kullanacağı rota bilgilerini, clusters ise reverse proxy'nin kullanacağı cluster bilgilerini içerir. Bu bilgiler, LoadFromMemory metoduyla reverse proxy'ye yüklenir. Son olarak, MapReverseProxy metodu kullanılarak reverse proxy eşleştirilir ve çalıştırılır.

Ve gerçek kullanım için aspire apphost projesinde reverse proxy projesini referans olarak ekleyip Program.cs dosyasına aşağıdaki ifadeleri ekleyip düzenleyin.

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();

Şimdi reverse proxy, echo sunucusuna referans verebilir. Dışarıdan gelen istekler reverse proxy'den alınacak ve echo sunucusuna yönlendirilecek şekilde yapı değiştirilmektedir.

Reverse Proxy'yi Düzenleme

Öncelikle reverse proxy'ye atanan projenin dinleme adresini değiştirmemiz gerekiyor.Properties/launchSettings.json dosyası içindeki applicationUrl'yi kaldırın. Ardından Program.cs dosyasını açarak aşağıdaki gibi kapsamlı bir şekilde düzenleyin.

 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"}");

Öncelikle routes ve clusters ile ilgili bilgileri düzenleyin. Her birine echo-route ve echo-cluster ekleyerek, isteği echo sunucusuna gönderecek şekilde ayarlayın. Ayrıca echo sunucusunun adresini ortam değişkeninden okuyarak kullanacak şekilde düzenleyin.

Bu adresin kuralı services__{service-name}__http__{index} şeklindedir. echo sunucusu durumunda, servis adı echo-server olup, tek örnek olduğu için indeks olarak 0 kullanılır. Eğer asp .net core sunucusu eklerseniz, WithReplica aracılığıyla birden fazla örnek oluşturulabileceği için indeksi artırarak kullanabilirsiniz. İstisna olarak ele alınan http://localhost:8080 anlamsız bir çöp değeridir.

Şimdi projeyi çalıştırıp, http://localhost:3000/?name=world adresine eriştiğinizde, yine Hello, world çıktısını göreceksiniz.

Genişleme Fikri

Artık dotnet aspire'a Go sunucusu ekleyip, reverse proxy aracılığıyla istekleri yönlendirdiğimizi doğruladık. O zaman şimdi bu süreci programatik olarak uygulayabilecek şekilde genişletebiliriz. Örneğin, echo sunucusu için servis adının sonuna numaralandırma ekleyerek birden fazla örnek oluşturabilir ve reverse proxy ayarlarını otomatik olarak ekleyebilirsiniz.

aspire apphost projesinin Program.cs dosyasında reverse proxy ve echo sunucusunu kullanan kodu aşağıdaki gibi düzenleyin.

 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}

Ve reverse proxy projesinin Program.cs dosyasını aşağıdaki gibi düzenleyin.

 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'e çıkarılan echo sunucu örneği için hedef ayarlarını ekliyoruz. Artık reverse proxy, artan echo sunucularına ilişkin hedef bilgilerine sahip ve istekleri iletebilecek duruma geldi. Mevcut http://localhost:3000/?name=world adresine eriştiğinizde, yine Hello, world çıktısını göreceksiniz.

Sonuç

Bu yazıda, dotnet aspire'a Go sunucusu ekleme ve reverse proxy aracılığıyla istekleri iletme sürecini açıkladık. Ancak genişleme konusunda henüz tamamını yazmadım ve ortam değişkenleri aracılığıyla daha programatik bir şekilde uygulanabilecek bir örneği ayrı bir repoda hazırladım. Ayrıntılı proje yapılandırması ve kod için snowmerak/AspireStartPack adresine bakabilirsiniz.

Ben şahsen dotnet aspire'ın docker compose'a bir alternatif olarak ve bir bulut dağıtım aracı olarak kendine özgü bir rol oynayabileceğini umuyorum. Zaten docker compose veya k8s manifest'i oluşturan bir jeneratör bulunduğu için, genel geliştiricinin altyapı araçlarına erişiminin daha iyi hale geldiğini düşünüyorum.