Xây dựng Microservices bằng ASP.NET Core – Xây dựng API Gateway với Ocelot

Xây dựng Microservices bằng ASP.NET Core – Xây dựng API Gateway với Ocelot

Danh sách các bài viết:

  • Phần 1: Lập kế hoạch.
  • Phần 2: Định hình kiến ​​trúc Microservices với CQRS và MediatR.
  • Phần 3: Khám phá dịch vụ với Eureka.
  • Phần 4: Xây dựng API Gateway với Ocelot (bài viết này).
  • Phần 5: Thao tác với database PostgreSQL bằng Marten.
  • Phần 6: Giao tiếp máy chủ thời gian thực với SignalR và RabbitMQ.
  • Phần 7: Transaction outbox với RabbitMQ.

Trong bài viết này, chúng ta sẽ tập trung vào một khái niệm cơ bản khác của kiến ​​trúc dựa trên microservices – API Gateway.

Mã nguồn cho giải pháp hoàn chỉnh có thể được tìm thấy trên GitHub của chúng tôi.

API Gateway là gì?

Một trong những lợi thế của phương pháp tiếp cận dựa trên microservices là bạn có thể tạo hệ thống lớn từ các dịch vụ nhỏ hơn, mỗi dịch vụ chịu trách nhiệm về một nghiệp vụ kinh doanh.

Cách tiếp cận này, khi được áp dụng cho các lĩnh vực lớn và phức tạp như thương mại điện tử, bảo hiểm hoặc tài chính, dẫn đến giải pháp bao gồm rất nhiều dịch vụ nhỏ.

Với cách tiếp cận này, các dịch vụ mới được thêm vào, một số dịch vụ được chia thành nhiều dịch vụ, bạn có thể tưởng tượng sẽ khó khăn như thế nào nếu bạn muốn truy cập trực tiếp vào từng dịch vụ từ ứng dụng client của bạn.

Mẫu API Gateway cố gắng giải quyết vấn đề truy cập các dịch vụ riêng lẻ từ các ứng dụng client, bằng cách thêm một điểm tương tác duy nhất giữa ứng dụng client và các dịch vụ backend trong kiến trúc microservices. API Gateway hoạt động như một mặt tiền che giấu sự phức tạp của hệ thống cơ bản khỏi các client của nó.

API Gateway là một dịch vụ chạy trước các dịch vụ backend của bạn và chỉ đưa ra các hoạt động cần thiết cho ứng dụng client nhất định.

API Gateway trong kiến trúc hệ thống

API Gateway có thể làm được nhiều việc hơn là chỉ định tuyến các yêu cầu từ ứng dụng client đến các dịch vụ backend thích hợp. Tuy nhiên, bạn nên cẩn thận không đưa ra logic nghiệp vụ và quy trình có thể dẫn đến vấn đề API Gateway quá mức.

Ngoài định tuyến, API Gateway thường chịu trách nhiệm về bảo mật. Chúng tôi thường không cho phép các cuộc gọi không được xác thực đi qua API Gateway. Do đó, API Gateway có trách nhiệm kiểm tra xem các token bảo mật được cung cấp, hợp lệ và chứa các yêu cầu bắt buộc hay không.

Việc tiếp theo là xử lý CORS. API Gateway phải được chuẩn bị để được truy cập từ các trình duyệt web chạy các ứng dụng SPA từ nguồn gốc khác với API Gateway.

API Gateway thường chịu trách nhiệm chuyển đổi yêu cầu và phản hồi như thêm tiêu đề, thay đổi định dạng yêu cầu để chuyển đổi giữa các định dạng dữ liệu được sử dụng bởi máy khách và máy chủ.

Cuối cùng nhưng không kém phần quan trọng, API Gateway có thể được sử dụng để thay đổi các giao thức truyền thông. Ví dụ: bạn có thể hiển thị các dịch vụ của mình dưới dạng HTTP REST trên API Gateway, trong khi các lệnh gọi này được API Gateway dịch thành gRPC chẳng hạn.

Một best practice là xây dựng các API Gateway riêng biệt cho từng loại ứng dụng client. Ví dụ: nếu chúng tôi có hệ thống dựa trên microservices cho bảo hiểm, chúng tôi sẽ xây dựng: một cổng riêng cho cổng đại lý bảo hiểm, cổng riêng cho ứng dụng văn phòng, cổng riêng để tích hợp bảo hiểm ngân hàng, cổng riêng cho ứng dụng di động của khách hàng cuối.

Xây dựng API Gateway với Ocelot

Có rất nhiều giải pháp để xây dựng API Gateway trên nền tảng Java, nhưng khi tôi tìm kiếm các giải pháp cho .NET thì giải pháp khả thi duy nhất, ngoài việc bạn tự xây dựng từ đầu, là Ocelot. Đây là một dự án rất thú vị và mạnh mẽ, được sử dụng ngay cả trong các mẫu chính thức của Microsoft.

Hãy triển khai API Gateway cho cổng bán bảo hiểm mẫu của chúng ta bằng cách sử dụng Ocelot.

Bắt đầu

Chúng ta bắt đầu với ứng dụng web ASP.NET Core trống. Tất cả những gì chúng ta cần là các tệp Program.csappsettings.json.

Chúng ta bắt đầu bằng cách thêm Ocelot vào dự án của mình bằng cách sử dụng Package Manager Console để cài đặt gói từ Nuget.

Install-Package Ocelot

Trong dự án này, chúng ta cũng sử dụng các tính năng bộ nhớ cache và khám phá dịch vụ Ocelot, vì vậy chúng ta cần thêm hai gói NuGet nữa là:

Install-Package Ocelot.Provider.Eureka
Install-Package Ocelot.Cache.CacheManager

Cuối cùng giải pháp của chúng ta sẽ giống như trong hình dưới đây.

File appsetting.json

Trong bước tiếp theo, chúng ta cần thêm tập tin ocelot.json vào project, tập tin này sẽ lưu trữ cấu hình cổng Ocelot của chúng ta.

Bây giờ chúng ta có thể sửa đổi Program.cs để khởi động đúng tất cả các dịch vụ cần thiết bao gồm cả Ocelot.

public class Program
{
   public static void Main(string[] args)
   {
       BuildWebHost(args).Run();
   }

   public static IWebHost BuildWebHost(string[] args)
   {  
       return WebHost.CreateDefaultBuilder(args)
           .UseUrls("http://localhost:8099")
           .ConfigureAppConfiguration((hostingContext, config) =>
           {
               config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath)
                   .AddJsonFile("appsettings.json", true, true)
                   .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true,
                       true)
                   .AddJsonFile("ocelot.json", false, false)
                   .AddEnvironmentVariables();
           })
           .ConfigureServices(s =>
           {
               s.AddOcelot().AddEureka().AddCacheManager(x => x.WithDictionaryHandle());
           })
           .Configure(a =>
           {
              a.UseOcelot().Wait();
           })
           .Build();
   }
}

Các phần quan trọng nhất ở đây là: thêm tệp cấu hình ocelot.json, thêm dịch vụ Ocelot với sự hỗ trợ của Eureka và trình quản lý Cache.

Nếu bạn nhớ từ phần trước của loạt bài này, chúng ta sử dụng Eureka làm cơ chế đăng ký và khám phá dịch vụ. Ở đây chúng ta muốn tận dụng lợi thế của nó và yêu cầu Ocelot giải quyết các url dịch vụ từ Eureka thay vì hard code.

Chúng ta cũng đang sử dụng hỗ trợ bộ nhớ đệm trong Ocelot để lưu cấu hình API Gateway vào bộ nhớ đệm cùng dữ liệu ít thay đổi.

Để tất cả những điều này hoạt động, bây giờ chúng ta phải cấu hình các tệp cấu hình đúng cách.

Hãy bắt đầu với appsettings.json, nơi chúng ta thêm cấu hình Eureka.

{
  "spring": {
    "application": { "name": "Agent-Portal-Api-Gateway" }
  },
  "eureka": {
    "client": {
      "serviceUrl": "http://localhost:8761/eureka/",
      "shouldRegisterWithEureka": false,
      "validateCertificates": false
    }
  }
}

Bây giờ đã đến lúc làm việc với tệp ocelot.json – phần cấu hình trung tâm của API Gateway của chúng ta. Tệp ocelot.json bao gồm hai phần chính: ReRoutesGlobalConfiguration.

  • ReRoutes xác định các tuyến – lập bản đồ các điểm cuối được API Gateway đưa ra liên kết với các dịch vụ backend. Bảo mật, bộ nhớ đệm và các phép biến đổi cũng có thể được định nghĩa ở đây.
  • GlobalConfiguration xác định cài đặt chung cho toàn bộ API Gateway.

Hãy bắt đầu với GlobalConfiguration:

"GlobalConfiguration": {
    "RequestIdKey": "OcRequestId",
    "AdministrationPath": "/administration",
    "UseServiceDiscovery": true,
    "ServiceDiscoveryProvider": { "Type": "Eureka", "Host": "localhost", "Port": "8761"}
  }

Những điều quan trọng ở đây là: cho phép khám phá dịch vụ sử dụng Eureka.

Bây giờ chúng ta có thể xác định các tuyến. Hãy xác định tuyến đầu tiên của chúng ta sẽ ánh xạ yêu cầu đến API Gateway là HTTP GET cho /Products/{code} đến dịch vụ ProductService hiển thị dữ liệu sản phẩm dưới dạng HTTP GET [serviceHost:port]/api/Products/{code}.

"ReRoutes": [
    {
      "DownstreamPathTemplate": "/api/Products/{everything}",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/Products/{everything}",
      "ServiceName": "ProductService",
      "UpstreamHttpMethod": [ "Get" ]
    }
]

DownstreamPathTemplate chỉ định url của dịch vụ backend, UpstreamPathTemplate chỉ định url được hiển thị bởi api gateway, Downstream và Upstream Schema chỉ định lược đồ, ServiceName chỉ định tên mà dịch vụ Downstream được đăng ký trong Eureka.

Hãy xem một ví dụ khác. Lần này, chúng ta sẽ cấu hình dịch vụ tạo phiếu mua hàng, được PolicyService hiển thị dưới dạng HTTP POST [serviceHost:port]/api/Offer

{
      "DownstreamPathTemplate": "/api/Offer",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/Offers",
      "ServiceName": "PolicyService",
      "UpstreamHttpMethod": [ "Post" ]
 }

Các tính năng nâng cao của Ocelot

Cors

Điều này không liên quan đến Ocelot, nhưng nó thường được yêu cầu hỗ trợ các request cross origin ở lớp API Gateway. Chúng ta cần sửa đổi file Program.cs của mình. Trong phương thức ConfigureServices(), chúng tôi cần thêm:

s.AddCors();

Trong phương thức Configure(), chúng ta cần thêm:

a.UseCors(b => b
          .AllowAnyOrigin()
          .AllowAnyMethod()
          .AllowAnyHeader()
          .AllowCredentials()
);

Security

Tiếp theo, chúng ta sẽ thêm bảo mật dựa trên JWT vào API Gateway của chúng ta. Bằng cách này, yêu cầu chưa được xác thực sẽ không đi qua API Gateway của chúng ta.

Trong phương thức BuildWebHost, chúng ta cần thêm key được sử dụng để xác thực JWT. Trong ứng dụng thực tế, bạn nên lưu trữ key này trong một kho bảo mật an toàn, nhưng với mục đích trình diễn trong hướng dẫn này, chúng ta chỉ tạo một biến như sau:

var key = Encoding.ASCII.GetBytes("THIS_IS_A_RANDOM_SECRET_2e7a1e80-16ee-4e52-b5c6-5e8892453459");

Bây giờ chúng ta cần thiết lập bảo mật trong phương thức ConfigureService():

s.AddAuthentication(x =>
{
     x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
     x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;    
})
.AddJwtBearer("ApiSecurity", x =>
{
      x.RequireHttpsMetadata = false;
      x.SaveToken = true;
      x.TokenValidationParameters = new TokenValidationParameters
      {
           ValidateIssuerSigningKey = true,
           IssuerSigningKey = new SymmetricSecurityKey(key),
           ValidateIssuer = false,
           ValidateAudience = false
       };
});

Với cài đặt này, bây giờ chúng ta có thể quay lại file ocelot.json và định nghĩa các yêu cầu bảo mật cho các tuyến của chúng ta.

Trong trường hợp này, chúng ta yêu cầu người dùng phải được xác thực và token chứa userType xác nhận quyền sở hữu với giá trị SALESMAN.

Hãy xem cách này có thể được cấu hình như thế nào:

{
      "DownstreamPathTemplate": "/api/Products",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/Products",
      "ServiceName": "ProductService",
      "UpstreamHttpMethod": [ "Get" ],
      "FileCacheOptions": { "TtlSeconds": 15 },
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "ApiSecurity",
        "AllowedScopes": []
      },
      "RouteClaimsRequirement": {
        "userType" : "SALESMAN"
      }
    }

Chúng ta đã thêm phần AuthenticationOptions để liên kết cơ chế xác thực được xác định trong Program.cs với Ocelot. Sau đó, chúng ta chỉ định trong RouteClaimsRequirement để yêu cầu phải cung cấp giá trị nào để yêu cầu được chuyển đến dịch vụ backend.

Khám phá dịch vụ

Chúng tôi đã trình bày cách sử dụng Eureka để khám phá dịch vụ. Bạn không nhất thiết phải sử dụng tính năng khám phá dịch vụ, bạn có thể ánh xạ yêu cầu upstream tới các dịch vụ backend bằng cách sử dụng url được hard code.

Nhưng điều này sẽ loại bỏ nhiều lợi thế của kiến ​​trúc dựa trên microservices và làm cho việc triển khai và hoạt động của bạn rất phức tạp, vì bạn phải duy trì sự đồng bộ giữa các url dịch vụ backend với cấu hình Ocelot.

Ngoài Eureka, Ocelot hỗ trợ cơ chế khám phá dịch vụ khác như: Consul và Kubernetes. Bạn có thể đọc thêm về chủ đề này trong tài liệu khám phá dịch vụ Ocelot.

Cân bằng tải

Ocelot cung cấp bộ cân bằng tải (load balancing) tích hợp có thể được định cấu hình cho mỗi tuyến. Có bốn loại nó có sẵn: least connection, round robin, cookie sticky session và dịch vụ khả dụng đầu tiên.

Bạn có thể đọc thêm về cân bằng tải trong tài liệu Ocelot.

Bộ nhớ đệm

Ocelot cung cấp khả năng thực hiện bộ nhớ đệm đơn giản. Sau khi bạn thêm gói Ocelot.Cache.CacheManager và kích hoạt nó như sau:

s.AddOcelot()
    .AddCacheManager(x => { x.WithDictionaryHandle(); })

Bạn có thể cấu hình bộ nhớ đệm cho từng tuyến. Ví dụ: hãy thêm bộ nhớ đệm để định tuyến tìm nạp định nghĩa sản phẩm với mã sản phẩm nhất định:

{
      "DownstreamPathTemplate": "/api/Products/{everything}",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/Products/{everything}",
      "ServiceName": "ProductService",
      "UpstreamHttpMethod": [ "Get" ],
      "FileCacheOptions": { "TtlSeconds": 15 },
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "ApiSecurity",
        "AllowedScopes": []
      },
      "RouteClaimsRequirement": {
        "userType" : "SALESMAN"
      }
    }

Cấu hình này cho Ocelot biết kết quả lưu trong bộ đệm của yêu cầu đã cho trong 15 giây.

Ocelot cũng cung cấp cho bạn khả năng bổ sung các khả năng sử dụng bộ nhớ đệm mạnh mẽ như Redis hoặc memcache.

Bạn có thể đọc thêm về bộ nhớ đệm trong tài liệu bộ nhớ đệm Ocelot .

Rate Limiting

Ocelot hỗ trợ Rate Limiting. Tính năng này giúp bạn bảo vệ các dịch vụ downstream không bị quá tải. Như thường lệ, bạn có thể cấu hình giới hạn tốc độ trên cơ sở tuyến. Để bật giới hạn tốc độ, bạn cần thêm cấu hình sau vào tuyến của mình:

"RateLimitOptions": {
    "ClientWhitelist": [],
    "EnableRateLimiting": true,
    "Period": "1s",
    "PeriodTimespan": 1,
    "Limit": 1
}
  • ClientWhiteList cho phép bạn xác định những client không bị giới hạn tốc độ.
  • EnableRateLimiting cho phép giới hạn tốc độ.
  • Period cấu hình khoảng thời gian mà giới hạn áp dụng (có thể được quy định trong vài giây, phút, giờ hoặc vài ngày).
  • Limit cấu hình số lượng yêu cầu cho phép trong khoảng thời gian nhất định. Nếu trong khoảng thời gian (Period) client vượt quá số lượng theo yêu cầu quy định tại Limit thì sau đó họ phải chờ một khoảng thời gian PeriodTimespan trước khi yêu cầu khác được chuyển tới dịch vụ downstream.

Transformation

Ocelot cho phép chúng tôi cấu hình chuyển đổi (Transformation) tiêu đề và xác nhận quyền sở hữu.

Bạn có thể thêm tiêu đề để yêu cầu và phản hồi. Ngoài giá trị tĩnh bạn cũng có thể sử dụng các placeholder như: {RemoteIpAddress}địa chỉ IP client, {BaseUrl}địa chỉ cơ sở Ocelot, {DownstreamBaseUrl} url dịch vụ downstream và {TraceId} trace id của Butterfly (nếu bạn sử dụng Butterfly distributed tracing). Bạn cũng có thể tìm và thay thế các giá trị tiêu đề.

Ocelot cũng cho phép bạn truy cập các xác nhận quyền sở hữu và chuyển đổi chúng thành tiêu đề, tham số chuỗi truy vấn hoặc các xác nhận quyền sở hữu khác.

Điều này rất hữu ích khi bạn cần chuyển thông tin về người dùng được ủy quyền tới dịch vụ backend. Như mọi khi, bạn chỉ định các biến đổi này cho mỗi tuyến.

Trong ví dụ dưới đây, bạn có thể thấy cách bạn có thể trích xuất xác nhận quyền sở hữu và đưa nó vào tiêu đề CustomerId .

"AddHeadersToRequest": {
    "CustomerId": "Claims[sub] > value[1] > |"
}

Bạn có thể đọc thêm về chủ đề này trong tài liệu chuyển đổi tiêu đề Ocelot và tài liệu chuyển đổi xác nhận quyền sở hữu Ocelot.

Tóm lược

Ocelot cung cấp cho chúng ta tính năng triển khai API Gateway phong phú mà hầu như không cần viết code. Hầu hết công việc bạn phải thực hiện liên quan đến việc xác định đúng các tuyến giữa các điểm cuối mà API Gateway đưa ra và các url dịch vụ backend. Bạn có thể dễ dàng thêm hỗ trợ xác thực và ủy quyền và bộ nhớ đệm.

Ngoài các tính năng được mô tả trong bài đăng này, Ocelot cũng hỗ trợ tổng hợp yêu cầu (request aggregation), ghi nhật ký, web sockets, distributed tracing với dự án Butterfly, Jaeger và trình xử lý ủy quyền.

Bài viết này được dịch từ bài viết gốc tại đây.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *