Mencoba menjalankan server Go secara skalabel dengan dotnet aspire

dotnet aspire?
dotnet aspire adalah alat yang dibuat untuk membantu pengembang dalam pengembangan dan konfigurasi cloud native, seiring dengan semakin meluasnya lingkungan cloud native. Alat ini memungkinkan pengembang .NET untuk dengan mudah men-deploy proyek .NET, berbagai infrastruktur cloud native, serta layanan atau kontainer yang ditulis dalam bahasa lain.
Tentu saja, mulai dari docker hingga k8s telah dirilis dan dioperasikan, dan cukup banyak bidang, industri, serta pengembang yang sebelumnya berada di lingkungan on-premise kini sedang bermigrasi, bahkan sudah bermigrasi, ke lingkungan cloud native. Ini sudah menjadi bidang yang matang. Oleh karena itu, saya rasa tidak perlu lagi menjelaskan ketidaknyamanan yang ada sebelumnya terkait nama host, konfigurasi port, firewall, pengelolaan metrik, dan sebagainya.
Karena itu, hanya dengan penjelasan di atas, Anda pasti belum bisa membayangkan dengan jelas apa itu dotnet aspire. Alasannya, Microsoft sendiri belum memberikan definisi yang pasti mengenai hal ini. Oleh karena itu, saya pun tidak akan memberikan definisi tersendiri. Namun, dalam tulisan ini saya akan menggunakan fitur dasar dotnet aspire sebagaimana yang saya pahami, sehingga Anda dapat merujuknya dan menentukan posisi Anda sendiri.
Konfigurasi Proyek
Membuat proyek dotnet aspire
Jika template dotnet aspire belum tersedia, Anda perlu menginstal template tersebut terlebih dahulu. Instal template dengan perintah berikut. Jika .NET belum terpasang, silakan instal sendiri.
1dotnet new install Aspire.ProjectTemplates
Lalu buat solusi baru di folder yang sesuai.
1dotnet new sln
Setelah itu, jalankan perintah berikut di dalam folder solusi untuk membuat proyek dengan template aspire-apphost.
1dotnet new aspire-apphost -o AppHost
Maka akan dibuat proyek aspire-apphost yang hanya berisi kode sederhana untuk konfigurasi.
Menambahkan Valkey
Sekarang kita akan menambahkan Valkey secara sederhana.
Sebelum menambahkannya, dotnet aspire menyediakan berbagai solusi pihak ketiga melalui community hosting.
Tentu saja Valkey juga mendapat dukungan dari community hosting ini, dan dapat digunakan dengan mudah melalui paket NuGet berikut.
1dotnet add package Aspire.Hosting.Valkey
Selain itu, tersedia berbagai hosting terintegrasi lainnya, yang dapat Anda lihat di sini.
Kembali ke Valkey, buka file Program.cs pada proyek AppHost dan ubah seperti berikut.
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 adalah implementasi dari interface IResourceBuilder yang memuat informasi untuk membangun layanan Valkey.WithDataVolume membuat volume untuk menyimpan data cache, sedangkan WithPersistence memastikan data cache disimpan secara berkelanjutan.
Sejauh ini, tampaknya fungsinya mirip dengan volumes pada docker-compose.
Tentu saja, Anda juga dapat membuatnya dengan mudah.
Namun, hal tersebut berada di luar cakupan tulisan ini, sehingga tidak akan dibahas sekarang.
Membuat server echo dengan bahasa Go
Sekarang kita akan menambahkan server sederhana dengan bahasa Go.
Pertama, buat workspace dengan menjalankan go work init di folder solusi.
Bagi pengembang .NET, workspace Go dapat dipahami mirip dengan solusi.
Kemudian buat folder bernama EchoServer, masuk ke dalamnya, dan jalankan go mod init EchoServer.
Perintah ini membuat modul Go. Bagi pengembang .NET, modul dapat dipahami mirip dengan proyek.
Lalu buat file main.go dan tulis seperti berikut.
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}
Server ini membaca port dari variabel lingkungan PORT yang disuntikkan oleh Aspire AppHost saat dijalankan, lalu menjalankan server pada port tersebut.
Server ini secara sederhana menerima query name dan mengembalikan Hello, {name}.
Sekarang mari kita tambahkan server ini ke dotnet aspire.
Menambahkan server echo ke aspire
Kembali ke proyek Aspire AppHost tempat Valkey ditambahkan, lalu tambahkan community hosting untuk Go.
1dotnet add package CommunityToolkit.Aspire.Hosting.Golang
Kemudian buka file Program.cs dan tambahkan kode berikut.
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();
Di sini, echoServer adalah implementasi dari interface IResourceBuilder yang memuat informasi untuk membangun server Go.
Metode AddGolangApp yang baru saja ditambahkan merupakan metode ekstensi dari host kustom untuk menambahkan server Go.
Server ini menggunakan port 3000 secara tetap, dan dapat dilihat bahwa variabel lingkungan PORT disuntikkan.
Terakhir, WithExternalHttpEndpoints berfungsi agar server dapat diakses dari luar.
Untuk pengujian, akses http://localhost:3000/?name=world, dan Anda akan melihat keluaran Hello, world.
Namun, saat ini dotnet aspire memiliki penalti berat yang diberikan kepada proyek non-dotnet.
Penalti tersebut adalah...
Memperluas Proyek
Bagaimana dengan penskalaan horizontal?
Saat ini dotnet aspire hanya menyediakan opsi WithReplica untuk builder proyek .NET yang ditambahkan dengan metode AddProject.
Namun, untuk host bahasa Go atau proyek eksternal seperti AddContainer, opsi ini tidak tersedia.
Oleh karena itu, perlu diimplementasikan secara langsung dengan menggunakan load balancer atau reverse proxy terpisah.
Akan tetapi, jika demikian, reverse proxy tersebut dapat menjadi SPOF, sehingga reverse proxy sebaiknya juga menyediakan opsi WithReplica.
Dengan demikian, reverse proxy secara mutlak harus berupa proyek .NET.
Selama ini, saya telah mencoba berbagai pendekatan untuk masalah ini, seperti nginx, traefik, dan implementasi langsung, tetapi ketika terdapat batasan harus berupa proyek .NET, saya tidak memiliki solusi yang dapat segera diterapkan.
Maka, saya mencari reverse proxy yang diimplementasikan dengan .NET, dan untungnya ada pilihan YARP.
YARP adalah reverse proxy yang diimplementasikan dengan .NET, dapat berfungsi sebagai load balancer, dan menyediakan berbagai fitur, sehingga saya menilai ini adalah pilihan yang baik.
Sekarang mari kita tambahkan YARP.
Mengonfigurasi reverse proxy dengan YARP
Pertama, buat proyek untuk menggunakan YARP.
1dotnet new web -n ReverseProxy
Lalu masuk ke proyek tersebut dan instal YARP.
1dotnet add package Yarp.ReverseProxy --version 2.2.0
Setelah instalasi selesai, buka file Program.cs dan tulis seperti berikut.
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"}");
Kode ini adalah kode dasar untuk menggunakan YARP.routes berisi informasi rute yang akan digunakan reverse proxy, sedangkan clusters berisi informasi cluster yang akan digunakan reverse proxy.
Informasi tersebut dimuat ke reverse proxy melalui metode LoadFromMemory.
Terakhir, metode MapReverseProxy digunakan untuk memetakan dan menjalankan reverse proxy.
Selanjutnya, untuk penggunaan sebenarnya, tambahkan referensi proyek reverse proxy pada proyek aspire apphost, lalu tambahkan dan ubah kode berikut pada file 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();
Sekarang reverse proxy dapat mereferensikan echo server.
Struktur telah berubah sehingga permintaan dari luar diterima oleh reverse proxy dan kemudian diteruskan ke echo server.
Memodifikasi reverse proxy
Pertama, kita perlu mengubah alamat listening dari proyek yang dijadikan reverse proxy.
Hapus applicationUrl di dalam file Properties/launchSettings.json.
Lalu buka file Program.cs dan ubah secara menyeluruh seperti berikut.
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"}");
Pertama, informasi untuk routes dan clusters diubah.
Masing-masing ditambahkan sebagai echo-route dan echo-cluster, sehingga permintaan dikirim ke echo server.
Selain itu, alamat echo server dimodifikasi agar dibaca dari variabel lingkungan.
Aturan alamat ini adalah services__{service-name}__http__{index}.
Untuk echo server, nama layanannya adalah echo-server dan karena merupakan instans tunggal, indeks yang digunakan adalah 0.
Jika server ASP.NET Core ditambahkan, beberapa instans dapat dibuat melalui WithReplica, sehingga indeks cukup ditingkatkan untuk digunakan.
Nilai http://localhost:8080 yang berada pada penanganan pengecualian adalah nilai tidak bermakna.
Sekarang jalankan proyek dan akses http://localhost:3000/?name=world, Anda akan tetap melihat keluaran Hello, world.
Gagasan perluasan
Sejauh ini, kita telah memastikan bahwa server Go ditambahkan ke dotnet aspire dan permintaan diteruskan melalui reverse proxy.
Selanjutnya, proses ini dapat diperluas agar diimplementasikan secara programmatic.
Sebagai contoh, dengan menambahkan nomor urut di belakang nama layanan echo server untuk membuat beberapa instans, konfigurasi reverse proxy dapat ditambahkan secara otomatis.
Ubah kode berikut pada file Program.cs proyek aspire apphost untuk menggunakan reverse proxy dan echo server.
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}
Lalu ubah file Program.cs pada proyek reverse proxy seperti berikut.
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};
Konfigurasi destinasi ditambahkan untuk delapan instans echo server.
Sekarang reverse proxy memiliki informasi destinasi untuk seluruh echo server yang bertambah, sehingga dapat meneruskan permintaan.
Jika Anda mengakses http://localhost:3000/?name=world seperti sebelumnya, Anda akan tetap melihat keluaran Hello, world.
Penutup
Dalam tulisan ini, saya telah menjelaskan proses menambahkan server Go ke dotnet aspire dan meneruskan permintaan melalui reverse proxy.
Namun, bagian perluasan belum sepenuhnya saya tuliskan, dan contoh yang dapat diimplementasikan secara lebih programmatic melalui variabel lingkungan telah saya tulis di repositori terpisah.
Untuk konfigurasi proyek dan kode yang lebih lengkap, silakan lihat snowmerak/AspireStartPack.
Secara pribadi, saya berharap dotnet aspire dapat memainkan perannya sendiri sebagai alternatif docker compose sekaligus sebagai alat deployment cloud. Saat ini sudah ada generator yang menghasilkan manifest docker compose atau k8s, sehingga saya rasa aksesibilitas alat infrastruktur bagi pengembang umum telah meningkat.