YARP İle Microservice’ler de API Gateway Implementasyonu
Merhaba,
Microserve mimarisinde yapılan çalışmalarda client’lar açısından en önemli konulardan biri servislere istekte bulunabilmek için tüm servislerin bilgilerine sahip olması gerekliliğidir ki böylece hedef servis odaklı bir istek süreci yürütebilsin. Amma velakin microservice gibi büyük sistemlerin söz konusu olduğu çalışmalarda bu hiçte kolay bir çözüm olmayacak, hatta bir client’ın tüm servislerle ilgili bilgilere sahip olması hem güvenlik riski hem de sürekli artan karmaşıklık gibi zorluklar eşliğinde çığ gibi büyüyen sorunlarla devam edecektir. İşte bu tarz durumlara karşın bizler API Gateway‘ler eşliğinde çözümler getirerek hem güçlü bir güvenlik sunmakta hem de ölçeklenebilirlik eşliğinde yüksek kullanılabilirlik sağlayabilmekteyiz. Bizler önceki içeriklerimizde API Gateway olarak Ocelot aracını teferruatlı inceleyerek konuya dair teknik bir tecrübeyi kaleme almıştık. Bu içeriğimizde ise araç olarak YARP kütüphanesini kullanacak ve böylece reverse proxy davranışa sahip API Gateway çalışmasını da tecrübe etmiş olacağız. O halde buyurun başlayalım…
API Gateway Nedir?
API Gateway, bir yazılım mimarisi bileşenidir ve genellikle microservice mimarilerinde kullanılan ve temel olarak client ile servisler arasındaki iletişimi kolaylaştıran bir giriş noktası yahut yönlendiricidir. API Gateway sayesinde, sorumluluğu birden fazla servise dağıtılmış olan uygulamanın tüm servislerine tek bir erişim noktası sağlanabilmekte ve bu güvenli bir şekilde gerçekleştirilebilmektedir. Ayrıca servislerin farklı protokoller kullanma ihtimaline karşın gelen istekleri doğru servislere yönlendirdikten sonra istekleri ilgili servisin kullandığı protokollere de dönüştürebilmektedir. Bunların dışında API Gateway sayesinde rahatlıkla load balancing sağlanabilmekte ve istekler trace edilerek analiz edilebilmektedir.
YARP Nedir?
YARP(Yet Another Reverse Proxy), Microsoft tarafından geliştirilen open source bir API Gateway ve reverse proxy kütüphanesidir. YARP sayesinde; yüksek performanslı, üretime hazır ve son derece özelleştirilebilir reverse proxy sunucuları oluşturulabilmektedir.
YARP, her bir deployment senaryosunun özel ihtiyaçlarına göre kolayca özelleştirilebilecek şekilde tasarlanmıştır.
API Gateway, API’leri yönetmek için tasarlanmış olan özel bir reverse proxy‘dir diyebiliriz.
YARP Kurulumu ve Konfigürasyonu
YARP’ı kullanabilmek için ilgili projeye Yarp.ReverseProxy kütüphanesinin yüklenmesi ve ardından aşağıdaki gibi yapılandırılması gerekmektedir.
var builder = WebApplication.CreateBuilder(args); builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); var app = builder.Build(); app.MapReverseProxy(); app.Run();
Burada görüldüğü üzere AddReverseProxy
metoduyla YARP için gerekli servisi ekliyor, MapReverseProxy
middleware’i ile de reverse proxy özelliğini uygulamada aktifleştiriyoruz. LoadFromConfig
metoduyla ise YARP’ın konfigürasyonunu ‘appsettings.json’ dosyası üzerinden yapılandırıyoruz. Bu yapılandırma için kullanacağımız şablon aşağıdaki gibidir;
{ "ReverseProxy": { "Routes": { "..." }, "Clusters": { "..." } } }
Görüldüğü üzere YARP’ın yapılandırması Clusters ve Routes ayarlarına göre temellendirilmiştir.
Clusters ile reverse proxy tarafından yönlendirilen isteklerin hedeflerini belirleyebilmek için birden fazla hedef sunucu veya hizmet gruplandırılmaktadır. Misal olarak bir uygulamadaki farklı yükseklikteki sunucuları veya servisleri bir küme olarak cluster ile tanımlayabiliriz. Her bir cluster, içerisinde hedef sunucunun ya da servisin adresini belirtilen destination barındırmaktadır ve bunların sayısı bir veya birden fazla olabilmektedir. Böylece istekler belirli bir küme içindeki farklı hedeflere yönlendirilebilir, bu da hedef sunucularda çeşitlilik sunabileceği gibi load balancing vs. gibi imkanlarda ekstradan avantajlar sağlayabilmektedir.
Bir cluster şablonu aşağıdaki gibi olabilir;
"Clusters": { "Example-Cluster": { "Destinations": { "destination1": { "Address": "https://..../server1" }, "destination2": { "Address": "https://..../server2" } } } }
Routes ise isteklerin reverse proxy tarafından yönlendirilmesinin hangi kurala, cluster’a ve hedefe göre yönlendirileceği tanımlarını barındırır. Misal olarak;
"Routes": { "Example-Route": { "ClusterId": "Example-Cluster", "Match": { "Path": "..." }, "Transforms": [ { "RequestHeader": "..." }, { "ResponseHeader": "..." } ] } }
Bütünsel olarak gerçek bir YARP yapılandırması için aşağıdaki örneği inceleyebilirsiniz;
{ . . . "ReverseProxy": { "Routes": { "API1-Route": { "ClusterId": "API1-Cluster", "Match": { "Path": "/api1/{**catch-all}" }, "Transforms": [ { "RequestHeader": "api1-request-header", "Append": "api1 request" }, { "ResponseHeader": "api1-response-header", "Append": "api1 response", "When": "Always" } ] }, "API2-Route": { "ClusterId": "API2-Cluster", "Match": { "Path": "/api2/{**catch-all}" }, "Transforms": [ { "RequestHeader": "api2-request-header", "Append": "api2 request" }, { "ResponseHeader": "api2-response-header", "Append": "api2 response", "When": "Always" } ] }, "API3-Route": { "ClusterId": "API3-Cluster", "Match": { "Path": "/api3/{**catch-all}" }, "Transforms": [ { "RequestHeader": "api3-request-header", "Append": "api3 request" }, { "ResponseHeader": "api3-response-header", "Append": "api3 response", "When": "Always" } ] } }, "Clusters": { "API1-Cluster": { "Destinations": { "destination1": { "Address": "https://localhost:7018" } } }, "API2-Cluster": { "Destinations": { "destination1": { "Address": "https://localhost:7210" } } }, "API3-Cluster": { "Destinations": { "destination1": { "Address": "https://localhost:7144" } } } } } }
Tabi YARP yapılandırmasını LoadFromConfig
metodunun dışında LoadFromMemory
metodu eşliğinde in-memory üzerinden de aşağıdaki gibi konfigüre edebilirsiniz.
using Yarp.ReverseProxy.Configuration; var builder = WebApplication.CreateBuilder(args); builder.Services.AddReverseProxy() //.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); .LoadFromMemory(new List<RouteConfig> { new RouteConfig() { RouteId = "API1-Route", ClusterId = "API1-Cluster", Match = new(){ Path = "/api1/{**catch-all}" }, Transforms = new List<Dictionary<string, string>>() { new() { { "RequestHeader", "api1-request-header" }, { "Append", "api1 request" } }, new() { { "ResponseHeader", "api1-response-header" }, { "Append", "api1 response" }, { "When", "Always" }, } } }, new RouteConfig() { RouteId = "API2-Route", ClusterId = "API2-Cluster", Match = new(){ Path = "/api2/{**catch-all}" }, Transforms = new List<Dictionary<string, string>>() { new() { { "RequestHeader", "api2-request-header" }, { "Append", "api2 request" } }, new() { { "ResponseHeader", "api2-response-header" }, { "Append", "api2 response" }, { "When", "Always" }, } } }, new RouteConfig() { RouteId = "API3-Route", ClusterId = "API3-Cluster", Match = new(){ Path = "/api3/{**catch-all}" }, Transforms = new List<Dictionary<string, string>>() { new() { { "RequestHeader", "api3-request-header" }, { "Append", "api3 request" } }, new() { { "ResponseHeader", "api3-response-header" }, { "Append", "api3 response" }, { "When", "Always" }, } } } }, new List<ClusterConfig> { new ClusterConfig(){ ClusterId = "API1-Cluster", Destinations = new Dictionary<string, DestinationConfig>() { { "destination1", new(){ Address = "https://localhost:7018" } } } }, new ClusterConfig(){ ClusterId = "API2-Cluster", Destinations = new Dictionary<string, DestinationConfig>() { { "destination2", new(){ Address = "https://localhost:7210" } } } }, new ClusterConfig(){ ClusterId = "API3-Cluster", Destinations = new Dictionary<string, DestinationConfig>() { { "destination3", new(){ Address = "https://localhost:7144" } } } } }); var app = builder.Build(); app.MapReverseProxy(); app.Run();
Artık karar sizin… 🙂
Authentication & Authorization Yapılandırması
Bir API Gateway’den bekleneceği üzere YARP ile de istekleri hedef servislere yönlendirirken öncelikle kimlik doğrulama ve yetki kontrolü zorunlu kılınabilmektedir. Bunu örneklendirebilmek için aşağıdaki gibi güvenlik protokolü uygulamış olan ‘API1’ servisine API Gateway üzerinden erişim gösterilmeye çalışıldığını varsayalım;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.Text; var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])) }; }); builder.Services.AddAuthorization(options => { options.AddPolicy("Authenticated", policy => policy.RequireAuthenticatedUser()); }); var app = builder.Build(); app.MapGet("/api1", (HttpContext httpContext) => { return "API 1"; }).RequireAuthorization("Authenticated"); app.UseAuthentication(); app.UseAuthorization(); app.Run();
Görüldüğü üzere ilgili servisin ‘/api1’ endpoint’ine erişim gösterilebilmesi için ‘Authenticated’ politikası gereği sadece kimliği doğrulanmış bir kullanıcı olmak yeterlidir. Bunu kontrol edebilmek için http://jwtbuilder.jamiekurtz.com/ adresi üzerinden pekte güvenli olmasa da şimdilik test amaçlı token validation parametrelerine uygun bir JWT üretip deneyde bulunabiliriz. İlgili parametrelerin değerlerini içeriğimizin sonunda paylaşılan github linkinden edinebilecek olsanız da işinizi kolaylaştırmak için aşağıda paylaşmakta fayda görmekteyim(tabi sizler kendinize göre bu değerleri özelleştirebilirsiniz);
{ . . . "Jwt": { "Issuer": "www.gencayyildiz.com", "Audience": "www.filanca.com", "Key": "laylaylom galiba sana göre sevmeler..." } }
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.Text; var builder = WebApplication.CreateBuilder(args); builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])) }; }); builder.Services.AddAuthorization(options => { options.AddPolicy("Authenticated", policy => policy.RequireAuthenticatedUser()); }); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapReverseProxy(); app.Run();
Görüldüğü üzere yukarıdaki gibi YARP üzerinden de bir yetkilendirme politikası tanımlanmalıdır ve bu politika hedef servistekiyle birebir aynı olmalıdır. Ayrıca artık kimlik doğrulama ve yetkilendirme işlemleri yapılacağı için MapReverseProxy
middleware’inden önce UseAuthentication
ve UseAuthorization
middleware’lerinin çağrılması gerekmektedir.
Ve son olarak tanımlanmış olan bu politikanın hangi hedef servisle ilişkili olduğunu ifade edebilmek için bunu yapılandırmada aşağıdaki gibi AuthorizationPolicy
eşliğinde bildirmek gerekmektedir;
"ReverseProxy": { "Routes": { "API1-Route": { "ClusterId": "API1-Cluster", "AuthorizationPolicy": "Authenticated", "Match": { "Path": "/api1/{**catch-all}" }, "Transforms": [ { "RequestHeader": "api1-request-header", "Append": "api1 request" }, { "ResponseHeader": "api1-response-header", "Append": "api1 response", "When": "Always" } ] } }, . . . }
Bu bildirim neticesinde ilgili politikayı uygulayan hedef servis, yukarıda oluşturduğumuz JWT eşliğinde yapılacak olan istekte API Gateway tarafından tetiklenebilecektir. İşte YARP, mevcut authentication ve authorization middleware mekanizmalarına bu şekilde entegre olabilmektedir.
Rate Limiting Yapılandırması
YARP ile geliştirilen sisteme, güvenliği artırmak ve sunucular üzerindeki yükü azaltmak için rate limiting’de uygulanabilmektedir. Rate limiting sayesinde, hedef API’lere gelen istek sayılarını sınırlandırabilmekte ve ölçeklendirebilmekteyiz. YARP tarafından .NET mimarisinde dahili olarak varlık gösteren rate limiting davranışı desteklenmektedir. .NET 7 ile gelmiş olan build-in rate limiting mekanizmasına dair çekmiş olduğum ders videosunu izlemek için tıklayınız.
Bunun için tek yapılması gereken bir rate limiting politikası belirlemektir;
builder.Services.AddRateLimiter(rateLimiterOptions => { rateLimiterOptions.AddFixedWindowLimiter("fixed", options => { options.Window = TimeSpan.FromSeconds(10); options.PermitLimit = 5; }); });
Ve ardından UseRateLimiter
middleware’i MapReverseProxy
‘den önce çağrılmalıdır.
app.UseRateLimiter(); app.MapReverseProxy();
Bu aşamadan sonra aşağıdaki gibi yapılandırma üzerinden RateLimiterPolicy
ile istenilen route’a tanımlanan politikaya uygun istek sınırlandırması uygulanabilir;
"ReverseProxy": { "Routes": { "API1-Route": { "ClusterId": "API1-Cluster", "AuthorizationPolicy": "Authenticated", "RateLimiterPolicy": "fixed", "Match": { "Path": "/api1/{**catch-all}" }, "Transforms": [ { "RequestHeader": "api1-request-header", "Append": "api1 request" }, { "ResponseHeader": "api1-response-header", "Append": "api1 response", "When": "Always" } ] } }, . . . }
Microservice mimarisinde olmazsa olmaz diyebileceğimiz API Gateway yapılanması için oldukça etkili bir araç olan YARP neredeyse mükemmel bir seçenektir diyebiliriz. Bu içeriğimizde; temel yapılandırma, kimlik doğrulama, yetkilendirme ve rate limiting gibi YARP’ın sıklıkla kullanılacak birçok özelliğini ele almış bulunuyoruz. Haliyle böylece sonraki yazılarımızdan birinde klavyeye alacağımız YARP ile load balancing davranışına temel atmış bulunuyoruz.
İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…
Not : Örnek çalışmaya aşağıdaki github adresinden erişebilirsiniz.
https://github.com/gncyyldz/Microservice_API_Gateway_With_YARP
Merhaba Hocam,
Araştırdığımda Ocelotta bulunan DelegatingHandler özelliğini YARP’ta bulamadım. Bir bilginiz varsa benimle paylaşır mısınız?
Özelliği arama sebebim gelen tokenı exchange edip değiştirerek işleme devam etmektir.
İyi çalışmalar.
çoğu konularda bir makalenize rastlamak güzel oluyor ve faydalanıyoruz. Emeğinize sağlık. Bir de yakın zamana kadar gencay yıldız değil de genç ayyildiz diye okuyordum ikisi de güzel 🙂
🙂