API Gateway با YARP
به جای اینکه کاربر با ده سرویس جدا حرف بزند، همه درخواستها از یک در ورودی میگذرند. این در ورودی مسیر را پیدا میکند و کارهای مشترک مثل احراز هویت و محدودیت درخواست را یک جا انجام میدهد. در .NET این کار را با کتابخانه YARP میسازیم.
نویسنده: bezzad
مشکل: اپ موبایل با سه سرویس
فروشگاه ما سه سرویس دارد: کاتالوگ، سفارش و پرداخت. اپ موبایل باید با هر سه حرف بزند. اگر اپ مستقیم به سرویسها وصل شود:
- اپ باید آدرس همه سرویسها را بداند. اگر فردا سرویس سفارش را دو تکه کنیم، باید نسخه جدید اپ منتشر کنیم و منتظر بمانیم کاربرها آن را نصب کنند.
- کارهای مشترک تکرار میشوند. هر سرویس باید جداگانه token را چک کند، محدودیت درخواست بگذارد و تنظیمات CORS داشته باشد. هر تیم کمی متفاوت این کار را میکند.
- همه سرویسها از اینترنت پیدا هستند. هر سرویس یک در ورودی برای حمله است.
ایده: یک در ورودی
یک API Gateway جلوی همه سرویسها قرار میگیرد. کاربر فقط یک آدرس میشناسد. سرویسها فقط در شبکه داخلی هستند.
کارهایی که Gateway انجام میدهد:
- مسیریابی. آدرسهای شروعشده با «/api/orders» به سرویس سفارش میروند.
- احراز هویت. token را یک بار چک میکند. درخواست بدون token معتبر به سرویسها نمیرسد.
- محدودیت درخواست. جلوی کاربری را میگیرد که در یک دقیقه هزار درخواست میفرستد.
- پخش بار. اگر سرویس سفارش سه نسخه دارد، درخواستها را بین آنها پخش میکند و نسخه ناسالم را کنار میگذارد.
- تغییر درخواست. مثلاً پیشوند «/api» را از آدرس برمیدارد یا یک header اضافه میکند.
مسیر یک درخواست
بیا یک درخواست را دنبال کنیم. مشتری سفارش شماره ۴۲ را باز میکند:
- اگر token نامعتبر باشد، Gateway همان اول کد 401 برمیگرداند.
- اگر کاربر از سهمیهاش گذشته باشد، کد 429 میگیرد.
- بعد Gateway مسیری را پیدا میکند که با آدرس جور است.
- آدرس را برای سرویس مقصد تغییر میدهد.
- یکی از نسخههای سالم سرویس سفارش را انتخاب میکند و درخواست را میفرستد.
پس سرویس سفارش فقط درخواستهای درست را میبیند و روی کار اصلی خودش تمرکز میکند.
کتابخانه YARP
اسم YARP کوتاهشده Yet Another Reverse Proxy است. یک کتابخانه متنباز از Microsoft که روی ASP.NET Core ساخته شده است. پس Gateway ما یک برنامه معمولی ASP.NET Core است و همه چیزهایی که میشناسیم (Middleware، احراز هویت، Rate Limiting، لاگ) در آن کار میکند.
در YARP سه مفهوم اصلی داریم:
- مفهوم Route. یک الگوی آدرس، مثل «/api/orders/» و هر چیزی بعد از آن. هر Route به یک Cluster اشاره میکند.
- مفهوم Cluster. گروه نسخههای یک سرویس، به همراه روش پخش بار و چک سلامت.
- مفهوم Destination. آدرس واقعی هر نسخه.
کد
برنامه Gateway
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
// JWT settings come from configuration (Authentication:Schemes:Bearer).
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization(o =>
o.AddPolicy("customer", p => p.RequireAuthenticatedUser()));
builder.Services.AddRateLimiter(o =>
{
o.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
o.AddPolicy("per-user", context => RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.User.Identity?.Name ?? "anonymous",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
});
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.UseAuthentication(); // first: who is the user?
app.UseAuthorization();
app.UseRateLimiter(); // must run before the proxy
app.MapReverseProxy();
app.Run();
تنظیمات مسیرها
{
"ReverseProxy": {
"Routes": {
"catalog": {
"ClusterId": "catalog",
"Match": { "Path": "/api/catalog/{**rest}" },
"Transforms": [ { "PathRemovePrefix": "/api" } ]
},
"orders": {
"ClusterId": "orders",
"AuthorizationPolicy": "customer",
"RateLimiterPolicy": "per-user",
"Match": { "Path": "/api/orders/{**rest}" },
"Transforms": [ { "PathRemovePrefix": "/api" } ]
}
},
"Clusters": {
"catalog": {
"Destinations": {
"c1": { "Address": "http://catalog-1:8080/" },
"c2": { "Address": "http://catalog-2:8080/" }
}
},
"orders": {
"LoadBalancingPolicy": "RoundRobin",
"HealthCheck": {
"Active": {
"Enabled": true,
"Interval": "00:00:10",
"Timeout": "00:00:05",
"Policy": "ConsecutiveFailures",
"Path": "/health/ready"
}
},
"Destinations": {
"o1": { "Address": "http://orders-1:8080/" },
"o2": { "Address": "http://orders-2:8080/" }
}
}
}
}
}
این تنظیمات میگوید:
- کاتالوگ برای همه باز است. سفارش فقط برای کاربر واردشده است و هر کاربر در دقیقه صد درخواست سهمیه دارد.
- آدرس «/api/orders/42» به شکل «/orders/42» به سرویس سفارش میرسد.
- هر ده ثانیه سلامت نسخههای سفارش چک میشود. نسخهای که پشت سر هم جواب نداد، کنار گذاشته میشود.
قانونهای مهم
- منطق کسبوکار در Gateway نگذار. قیمتگذاری یا قانون سفارش جایش در سرویس است. اگر Gateway کسبوکار بداند، هر تغییر کوچک باید در دو جا انجام شود.
- سرویسها هم مجوز را چک کنند. گیتوی فقط میگوید «این کاربر واردشده است». این که «آیا این کاربر اجازه دیدن سفارش ۴۲ را دارد» را سرویس سفارش باید چک کند. اگر یک روز کسی از داخل شبکه مستقیم به سرویس برسد، نباید بیدفاع باشد.
- از Gateway چند نسخه داشته باش. همه ترافیک از آن میگذرد. اگر فقط یک نسخه باشد و بیفتد، کل فروشگاه میخوابد.
- سبک نگهش دار. هر درخواست یک قدم شبکه اضافه دارد. کار سنگین در Gateway همه سرویسها را کند میکند.
- برای هر نوع کاربر، Gateway جدا را در نظر بگیر. اگر اپ موبایل و سایت نیازهای خیلی متفاوتی دارند، الگوی BFF (Backend for Frontend) یعنی یک Gateway برای هر کدام. اینطور تیم موبایل منتظر تیم وب نمیماند.
- شناسه درخواست را از همینجا بساز. یک شناسه ردیابی که از Gateway شروع شود، دنبال کردن یک درخواست در همه سرویسها را ممکن میکند.
اشتباههای رایج
| اشتباه | نتیجه | راه درست |
|---|---|---|
| منطق کسبوکار در Gateway | یک «سرویس خدا» که همه تیمها باید تغییرش دهند. | Gateway فقط کارهای مشترک و مسیریابی. |
| فقط Gateway مجوز را چک کند | دسترسی مستقیم از داخل شبکه بدون هیچ چکی. | سرویسها هم مجوز را چک کنند. |
| یک نسخه از Gateway | با افتادن آن، همه چیز میافتد. | چند نسخه پشت Load Balancer. |
| یک Gateway مشترک برای همه کلاینتها با کد زیاد | تیمها منتظر هم میمانند و Gateway بزرگ میشود. | الگوی BFF برای هر نوع کلاینت. |
| Rate Limiter قبل از احراز هویت | سهمیه بر اساس کاربر کار نمیکند. | اول احراز هویت، بعد محدودیت. |
چه وقت Gateway؟
مناسب
- چند سرویس که کلاینتهای بیرونی (موبایل، وب، شرکا) به آنها نیاز دارند.
- کارهای مشترک مثل احراز هویت و محدودیت درخواست که نمیخواهی در هر سرویس تکرار شود.
- نیاز به جابجا کردن یا تکه کردن سرویسها بدون تغییر کلاینت.
نامناسب
- یک Monolith با یک برنامه. یک لایه اضافه فقط پیچیدگی و تأخیر میآورد.
- وقتی زیرساخت تو همین حالا یک Ingress یا Gateway مدیریتشده دارد که همین کارها را میکند.
خلاصه در شش خط
- یک API Gateway در ورودی همه سرویسها است. کلاینت فقط یک آدرس میشناسد.
- کارهای مشترک یک جا: مسیریابی، احراز هویت، محدودیت درخواست، پخش بار.
- در YARP هر Route به یک Cluster میرسد و هر Cluster چند مقصد دارد.
- چون YARP روی ASP.NET Core است، همه Middleware های آشنا در آن کار میکنند.
- منطق کسبوکار در Gateway نیست و سرویسها هم خودشان مجوز را چک میکنند.
- از Gateway چند نسخه داشته باش و برای کلاینتهای خیلی متفاوت، الگوی BFF را در نظر بگیر.