Levelwise
فارسی
زیرساخت و DevOps

API Gateway با YARP

به جای اینکه کاربر با ده سرویس جدا حرف بزند، همه درخواست‌ها از یک در ورودی می‌گذرند. این در ورودی مسیر را پیدا می‌کند و کارهای مشترک مثل احراز هویت و محدودیت درخواست را یک جا انجام می‌دهد. در .NET این کار را با کتابخانه YARP می‌سازیم.

بازبینی نشدهبا کمک AI نوشته شدهزمان خواندن: ۱۳ دقیقهمثال اپ موبایل فروشگاه اینترنتیکد C# با YARP و .NET 10

نویسنده: bezzad

مشکل: اپ موبایل با سه سرویس

فروشگاه ما سه سرویس دارد: کاتالوگ، سفارش و پرداخت. اپ موبایل باید با هر سه حرف بزند. اگر اپ مستقیم به سرویس‌ها وصل شود:

  1. اپ باید آدرس همه سرویس‌ها را بداند. اگر فردا سرویس سفارش را دو تکه کنیم، باید نسخه جدید اپ منتشر کنیم و منتظر بمانیم کاربرها آن را نصب کنند.
  2. کارهای مشترک تکرار می‌شوند. هر سرویس باید جداگانه token را چک کند، محدودیت درخواست بگذارد و تنظیمات CORS داشته باشد. هر تیم کمی متفاوت این کار را می‌کند.
  3. همه سرویس‌ها از اینترنت پیدا هستند. هر سرویس یک در ورودی برای حمله است.

ایده: یک در ورودی

یک API Gateway جلوی همه سرویس‌ها قرار می‌گیرد. کاربر فقط یک آدرس می‌شناسد. سرویس‌ها فقط در شبکه داخلی هستند.

بدون در ورودی مشترکبا یک در ورودیاپ موبایلکاتالوگسفارشپرداختسه آدرس، سه بار احراز هویتهمه سرویس‌ها از اینترنت پیدا هستنداپ موبایلAPI Gatewayکاتالوگسفارشپرداختیک آدرس، کارهای مشترک یک جاسرویس‌ها فقط در شبکه داخلی
با Gateway، اپ موبایل فقط یک آدرس می‌شناسد و سرویس‌ها پشت آن پنهان هستند.

کارهایی که Gateway انجام می‌دهد:

  1. مسیریابی. آدرس‌های شروع‌شده با «/api/orders» به سرویس سفارش می‌روند.
  2. احراز هویت. token را یک بار چک می‌کند. درخواست بدون token معتبر به سرویس‌ها نمی‌رسد.
  3. محدودیت درخواست. جلوی کاربری را می‌گیرد که در یک دقیقه هزار درخواست می‌فرستد.
  4. پخش بار. اگر سرویس سفارش سه نسخه دارد، درخواست‌ها را بین آن‌ها پخش می‌کند و نسخه ناسالم را کنار می‌گذارد.
  5. تغییر درخواست. مثلاً پیشوند «/api» را از آدرس برمی‌دارد یا یک header اضافه می‌کند.

مسیر یک درخواست

بیا یک درخواست را دنبال کنیم. مشتری سفارش شماره ۴۲ را باز می‌کند:

GET /api/orders/42احراز هویتJWTمحدودیتRate Limitپیدا کردن مسیرRouteتغییر آدرسTransformانتخاب نسخهLoad Balanceسرویس سفارش/orders/42401429درخواست بد همین‌جا رد می‌شود و به سرویس نمی‌رسد
درخواست بد زود رد می‌شود. فقط درخواست درست به سرویس می‌رسد.
  1. اگر token نامعتبر باشد، Gateway همان اول کد 401 برمی‌گرداند.
  2. اگر کاربر از سهمیه‌اش گذشته باشد، کد 429 می‌گیرد.
  3. بعد Gateway مسیری را پیدا می‌کند که با آدرس جور است.
  4. آدرس را برای سرویس مقصد تغییر می‌دهد.
  5. یکی از نسخه‌های سالم سرویس سفارش را انتخاب می‌کند و درخواست را می‌فرستد.

پس سرویس سفارش فقط درخواست‌های درست را می‌بیند و روی کار اصلی خودش تمرکز می‌کند.

کتابخانه 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. آدرس واقعی هر نسخه.
RoutesClustersDestinations/api/catalog/{**rest}/api/orders/{**rest}catalogorderscatalog-1:8080catalog-2:8080orders-1:8080orders-2:8080پاسخ سلامت نداد: کنار گذاشته شدکدام آدرس به کدام سرویس برودگروه نسخه‌های یک سرویسهمه این‌ها فقط با تنظیمات تعریف می‌شوند، بدون کد اضافه
هر Route به یک Cluster می‌رسد. هر Cluster چند مقصد دارد و مقصد ناسالم کنار گذاشته می‌شود.

کد

برنامه 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» به سرویس سفارش می‌رسد.
  • هر ده ثانیه سلامت نسخه‌های سفارش چک می‌شود. نسخه‌ای که پشت سر هم جواب نداد، کنار گذاشته می‌شود.
ترتیب Middleware مهم است: اول احراز هویت، بعد محدودیت درخواست. وگرنه Rate Limiter نمی‌داند کاربر کیست و همه را با هم یکی حساب می‌کند.

قانون‌های مهم

  1. منطق کسب‌وکار در Gateway نگذار. قیمت‌گذاری یا قانون سفارش جایش در سرویس است. اگر Gateway کسب‌وکار بداند، هر تغییر کوچک باید در دو جا انجام شود.
  2. سرویس‌ها هم مجوز را چک کنند. گیت‌وی فقط می‌گوید «این کاربر واردشده است». این که «آیا این کاربر اجازه دیدن سفارش ۴۲ را دارد» را سرویس سفارش باید چک کند. اگر یک روز کسی از داخل شبکه مستقیم به سرویس برسد، نباید بی‌دفاع باشد.
  3. از Gateway چند نسخه داشته باش. همه ترافیک از آن می‌گذرد. اگر فقط یک نسخه باشد و بیفتد، کل فروشگاه می‌خوابد.
  4. سبک نگهش دار. هر درخواست یک قدم شبکه اضافه دارد. کار سنگین در Gateway همه سرویس‌ها را کند می‌کند.
  5. برای هر نوع کاربر، Gateway جدا را در نظر بگیر. اگر اپ موبایل و سایت نیازهای خیلی متفاوتی دارند، الگوی BFF (Backend for Frontend) یعنی یک Gateway برای هر کدام. این‌طور تیم موبایل منتظر تیم وب نمی‌ماند.
  6. شناسه درخواست را از همین‌جا بساز. یک شناسه ردیابی که از Gateway شروع شود، دنبال کردن یک درخواست در همه سرویس‌ها را ممکن می‌کند.
ترکیب پاسخ‌ها: کتابخانه YARP یک reverse proxy است. هر درخواست را به یک مقصد می‌فرستد. اگر یک صفحه به داده سه سرویس نیاز دارد و می‌خواهی یک جواب ترکیبی بدهی، باید خودت یک endpoint معمولی در Gateway یا BFF بنویسی.

اشتباه‌های رایج

اشتباه نتیجه راه درست
منطق کسب‌وکار در Gateway یک «سرویس خدا» که همه تیم‌ها باید تغییرش دهند. Gateway فقط کارهای مشترک و مسیریابی.
فقط Gateway مجوز را چک کند دسترسی مستقیم از داخل شبکه بدون هیچ چکی. سرویس‌ها هم مجوز را چک کنند.
یک نسخه از Gateway با افتادن آن، همه چیز می‌افتد. چند نسخه پشت Load Balancer.
یک Gateway مشترک برای همه کلاینت‌ها با کد زیاد تیم‌ها منتظر هم می‌مانند و Gateway بزرگ می‌شود. الگوی BFF برای هر نوع کلاینت.
Rate Limiter قبل از احراز هویت سهمیه بر اساس کاربر کار نمی‌کند. اول احراز هویت، بعد محدودیت.

چه وقت Gateway؟

مناسب

  • چند سرویس که کلاینت‌های بیرونی (موبایل، وب، شرکا) به آن‌ها نیاز دارند.
  • کارهای مشترک مثل احراز هویت و محدودیت درخواست که نمی‌خواهی در هر سرویس تکرار شود.
  • نیاز به جابجا کردن یا تکه کردن سرویس‌ها بدون تغییر کلاینت.

نامناسب

  • یک Monolith با یک برنامه. یک لایه اضافه فقط پیچیدگی و تأخیر می‌آورد.
  • وقتی زیرساخت تو همین حالا یک Ingress یا Gateway مدیریت‌شده دارد که همین کارها را می‌کند.

خلاصه در شش خط

  1. یک API Gateway در ورودی همه سرویس‌ها است. کلاینت فقط یک آدرس می‌شناسد.
  2. کارهای مشترک یک جا: مسیریابی، احراز هویت، محدودیت درخواست، پخش بار.
  3. در YARP هر Route به یک Cluster می‌رسد و هر Cluster چند مقصد دارد.
  4. چون YARP روی ASP.NET Core است، همه Middleware های آشنا در آن کار می‌کنند.
  5. منطق کسب‌وکار در Gateway نیست و سرویس‌ها هم خودشان مجوز را چک می‌کنند.
  6. از Gateway چند نسخه داشته باش و برای کلاینت‌های خیلی متفاوت، الگوی BFF را در نظر بگیر.