Uno de los desafíos al trabajar con mico-servicios es como lograr una comunicación sin provocar un acoplamiento entre ellos. Por esta razón, seguramente necesitaras un API Gateway para poder lograrlo exitosamente.
Hay varios en el mercado y de gran variedad. Tenemos algunos proyectos open-source y algunos propuestos por las nubes.
En caso de los on-premise tenemos:
- Ocelot, que es el que veremos, es un proyecto de código abierto realizado en .Net Core.
- Kong, que se encuentra construido a base Nginx.
- Express API Gateway, se encuentra construida sobre Express JS.
En la nube tenemos:
- Azure API Management, Microsoft.
- AWS Api Gateway, Amazon Web Service.
- APigee, Google Cloud.
Como comentamos, vamos a estar viendo Ocelot y como me gusta el open source usaremos un entorno linux con Visual Studio Code.
¿Porque necesitamos API Gateway?
El objetivo principal es facilitarnos las cosas. Si estamos trabajando en un arquitectura de micro-servicios tenemos varias estrategias diferentes, podemos exponer cada api por separado para consumidores individuales, o presentar un API unificada a través de un API Gateway por medio de alguna plataforma.
Vamos a ver un ejemplo. Nuestro ejemplo se basará en un API de comercio electrónico. Seguramente tendremos varias API en un eCommerce, por ejemplo: catálogo de productos, servicios de envío, servicio de impuestos, servicio de pedidos, servicio de inventario, servicio de correo electrónico, servicios de usuarios, etc.
Para nuestro ejemplo solamente crearemos 2 API, la de pedidos y la de catalogo de productos. Con estas 2 será suficiente para ejemplificar la idea principal del API Gateway y el uso de Ocelot.
Nuestra api de catálogo la tendremos configurado en el puerto 5001, la API de órdenes lo haremos en el puerto 5002 y nuestro API Gateway en el 5000. Vamos a crear nuestra estructura en Visual Studio Code.
Lo primero que haremos sera crear un carpeta desde la consola donde tendremos nuestros archivos. La llamare OcelotSample, pero ustedes pueden llamarla como gusten. Luego, por la línea de comando, con el comando de dotnet, creamos un archivo de solución.
Por línea de comando vamos a crear con el archivo de solución. Ahora abrimos el Visual Studio code en esta carpeta y ejecutamos el siguiente comando.
dotnet new sln
Tengo instalado un plugin muy útil que se llama Solution Explorer, este nos dejara crear proyectos sin necesidad de la consola. Crearemos 3 proyectos CatalogApi, OrderApi y ApiGateway. Podemos verlo en la imagen siguiente:
Bien, tenemos listos los proyectos en las carpetas. Nos debemos asegurar que los puertos sean los adecuados. Para esto vamos la carpeta properties de cada proyecto y abrimos el archivo launchSetting.json. Buscamos applicationUrl y le cambiamos el puerto a cada uno.
"ApiGateway": { "commandName": "Project", "launchBrowser": true, "launchUrl": "api/values", "applicationUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }
Vamos a nuestra proyecto de ApiCatalog. Creamos un un controlador que contendrá un método GetProductos. Este método devolverá una colección de Productos. Nuestra clase producto será sencilla como vemos en el código siguiente:
namespace CatalogApi.Controllers { [Route("api/catalog]")] [ApiController] public class CatalogController : ControllerBase { // GET api/values [HttpGet("GetProducts")] public ActionResult<IList<Product>> GetProducts() { var result = new List<Product>(); result.Add(new Product{ ProductId = 1, Description = "Product 1", Price = 10.20M }); result.Add(new Product{ ProductId = 2, Description = "Product 2", Price = .30M }); result.Add(new Product{ ProductId = 3, Description = "Product 3", Price = 1.55M }); return result; } } public class Product{ public int ProductId { get; set; } public string Description { get; set; } public decimal Price { get; set; } } }
En nuestra OrdersApi, tendremos 2 método, el primero será AddOrder y GetOrderByCustomers. Los utilizaran la clase Order, uno como parámetro de entrada otro como parámetro de devolución como un lista del mismo.
namespace OrderApi.Controllers { [Route("api/Orders")] [ApiController] public class OrderController : ControllerBase { [HttpGet("GetOrderByCustomerId/{id}")] public ActionResult<List<Order>> GetOrderByCustomerId(int id) { return ListOrder().Where(p => p.CustomerId == id).ToList(); } [HttpPost("AddOrder")] public ActionResult<List<Order>> AddOrder([FromBody] Order order) { var result = ListOrder(); result.Add(order); return result.ToList(); } private IList<Order> ListOrder(){ var result = new List<Order>(); result.Add(new Order(){ OrderId = 1, CustomerId = 1, CustomerName = "Customer 1"}); result.Add(new Order(){ OrderId = 2, CustomerId = 2, CustomerName = "Customer 2"}); result.Add(new Order(){ OrderId = 3, CustomerId = 3, CustomerName = "Customer 3"}); return result; } } public class Order{ public int OrderId { get; set; } public int CustomerId { get; set; } public string CustomerName { get; set; } } }
Agregar API Gateway
Iremos a nuestro proyecto en ApiGateway. Al igual que en los otros proyectos, eliminaremos el ValuesController, no nos será útil.
Ahora en nuestro proyecto agregaremos el componente Ocelot desde los paguetes Nuget. Lo haremos por la línea de comando que se encuentra integrada en Visual Studio Code.
dotnet "add" "/home/fernando/OcelotSample/ApiGateway/ApiGateway.csproj" "package" "Ocelot"
Lo siguiente es agregar el archivo ocelot.json que será el que contendrá las direcciones y re-direcciones a los servicios. Por el momento, será lo agregar un archivo json con ese nombre.
Siguiente paso, debemos agregar el archivo de configuración al hoster para que esté disponible para en el servicio.
public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .ConfigureAppConfiguration((host, config) => { config.AddJsonFile("ocelot.json"); }).UseStartup<Startup>(); }
Por último, debemos configurar nuestro servicio de Ocelot en el StartUp como vemos en el siguiente ejemplo de código:
public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); services.AddOcelot(Configuration); }
public async Task ConfigureAsync(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseMvc(); await app.UseOcelot(); }
Archivo de configuración de Ocelot
Veamos la estructura del archivo:
{ "ReRoutes":[ { "DownstreamPathTemplate": "/api/catalog/{catchAll}", "DownstreamSCheme": "http", "DownstreamHostAndPorts":[ { "Host": "localhost", "Port": 5002 } ], "UpstreamPathTemplate": "/catalog-api/{catchAll}" }, { "DownstreamPathTemplate": "/api/orders/{catchAll}", "DownstreamSCheme": "http", "DownstreamHostAndPorts":[ { "Host": "localhost", "Port": 5001 } ], "UpstreamPathTemplate": "/orders-api/{catchAll}" } ], "GlobalConfiguration":{ "BaseUrl": "https://localhost:5000" } }
Lo más importante de nuestro archivo de configuración es, Globalconfiguration. Nos permite definir la url base donde Ocelot estara escuchando para recepcionar las solicitudes. En nuestra aplicación, el puerto 5000.
La sección ReRouters es un objetivo del tipo matriz. No permite configurar las redirecciones que necesitemos. Downstream representa las rutas de los microservicios. UpStream representa como el usuario debe acceder al servicio por medio del API Gateway.
Tenemos algunos comodines interesantes, por ejemplo, {catchAll} que capturara todo el tráfico para esa API y lo redireccionará como corresponde.
Ahora hagamos una pruebas con Postman para ver los resultados:
Conclusiones
Si estamos trabajando con microservicios, es natural que necesiten en algún momento comunicarse entre ellos. Un buen camino para poder hacerlo es usar un API Gateway. Con esto nos liberaremos del acoplamiento o de consumir microservicios directamente.