From d5e80d2c92a79e2ce4f6e119d927b1b1ee5e61a9 Mon Sep 17 00:00:00 2001 From: eiximenis Date: Fri, 15 Nov 2019 10:18:56 +0100 Subject: [PATCH] BFFs section --- Architecture.md | 25 +++++++++++++++++++++++-- BFFs.md | 31 +++++++++++++++++++++++++++++++ _Sidebar.md | 1 + images/Bff/BFFs.png | Bin 0 -> 14506 bytes 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 BFFs.md create mode 100644 images/Bff/BFFs.png diff --git a/Architecture.md b/Architecture.md index e7c63ce..8e7c0c8 100644 --- a/Architecture.md +++ b/Architecture.md @@ -14,16 +14,37 @@ eShopOnContainers includes a simplified EventBus abstraction to handle integrati For a production-grade solutions you should use a more robust implementation based on a robust product such as [NServiceBus](https://github.com/Particular/NServiceBus). You can even see a (somewhat outdated) implementation of eShopOnContainers with NServiceBus here: https://github.com/Particular/eShopOnContainers. +## gRPC + +Most communications between microservices are decoupled using the EventBus and the "pub/sub" pattern. But in some cases, we have explicit communications between microservices. Currently those communications are limited from the custom aggregators to internal microservices. + +For those explicit communications gRPC is used (instead of HTTP/JSON). gRPC is a RPC-based protocol that have great performance and low bandwidth usage, making it the best candidate for internal microservices communication. + ## API Gateways The architecture also includes an implementation of the API Gateway pattern and Backend-For-Front-End (BFF), to publish simplified APIs and include additional security measures for hiding/securing the internal microservices from the client apps or outside consumers. -These sample API Gateways are based on [Ocelot](https://github.com/ThreeMammals/Ocelot), an OSS lightweight API Gateway solution. The API Gateways are deployed as autonomous microservices/containers, so you can test them in a simple development environment by just using Docker Desktop or even with orchestrators like Kubernetes in AKS or Service Fabric. +These API Gateways are implemented using [Envoy](https://www.envoyproxy.io/), an OSS high-performant, production ready, proxy and API Gateway. Currently these API Gateways only perform request forwarding to internal microservices and custom aggregators, giving the clients then ilusion of a single base URL. In the future we plan to add specific features like: -For a production-ready architecture you can either keep using Ocelot, which is simple and easy to use, and it's currently used in production by large companies. If you need additional functionality and a much richer set of features suitable for commercial APIs, you can also substitute those API Gateways and use Azure API Management or any other commercial API Gateway, as shown in the following diagram. +* Automatic translation from/to grpc to/from HTTP/REST. +* Authentication and Authorization management +* Cache support + +If you need additional functionality and a much richer set of features suitable for commercial APIs, you can also add a full API Gateway product like Azure API Management on top of these API Gateways. ![](images/Architecture/azure-api-management-gateway.png) +Alongside the API Gateways a set of "custom aggregators" are provided. Those aggregators provide a simple API to the clients for some operations. + +Currently two aggregators exists: + +1. Mobile Shopping: Aggregator for shopping operations called by Xamarin App +2. Web Shopping: Aggregator for shopping operations called by Web clients (MVC & SPA) + +For more information about the relationship between API Gateways, aggregators, clients and microservices check the + +>**Note** Previous versions of eShopOnContainers were using [Ocelot](https://github.com/ThreeMammals/Ocelot) instead of Envoy. Ocelot is a great netcore OSS open project, to create a API Gateway. Ocelot support a wide set of features, and it is a serious candidate to be used in every netcore based project. However the lack of support for gRPC was the main reason to change Ocelot for Envoy in eShop. + ## Internal architectural patterns There are different types of microservices according to their internal architectural pattern and approaches depending on their purposes, as shown in the image below. diff --git a/BFFs.md b/BFFs.md new file mode 100644 index 0000000..e6ea8eb --- /dev/null +++ b/BFFs.md @@ -0,0 +1,31 @@ +# BFF implementation + +Current implementation of the [BFF pattern](https://samnewman.io/patterns/architectural/bff/) is as follow in the diagram: + +![Schema of BFF](./images/Bff/BFFs.png) + +**Note:** This schema only show one BFF. Each client type (web and mobile) has its own BFF. + +The BFF is composed by two containers: One Envoy proxy and one custom aggregator (Note: The marketing BFF do not have custom aggregator, because has no complex logic). + +## Envoy + +An Envoy proxy acts a ingress for the BFF and **provides a single URL** for the client. All client calls go through the Envoy proxy. Then, based on the some rules, the Envoy proxy can: + +1. Forward the call to the custom aggregator. +2. Forward the call directly to a internal microservice + +## Custom aggregator + +The custom aggregator is another container, that exposes a HTTP/JSON API and has complex methods, that involves data from various internal microservices. Each method of the custom aggregator calls one (or usually more than one) internal microservice, aggregates the results (applying custom logic) and returns data to the client. + +All calls from the aggregator to microservices are performed using gRPC (dashed lines in diagram). + +## Client Application + +Client application calls the BFF only through the single URL exposed by the Envoy proxy. Based on the request data, the request is then forwarded to a internal microservice (single crud calls) or to the custom aggregaor (complex logic calls), but this is transparent to the client. + +When the call is forwarded directly from Envoy to internal microservice, it is performed using HTTP/JSON. That implies, that, right now, internal microservices expose a mix of methods: some in gRPC (called by aggregators) and some in HTTP/JSON (called by Envoy). This is subject to change in the future (all microservices methods could be in gRPC and Envoy could automatically translate between gRPC and HTTP/JSON if needed). + + + diff --git a/_Sidebar.md b/_Sidebar.md index 76dd630..3133e52 100644 --- a/_Sidebar.md +++ b/_Sidebar.md @@ -19,6 +19,7 @@ ## Explore - [Architecture](Architecture) + - [BFF implementation](BFFs) - [Application](Explore-the-application) - [Code](Explore-the-code) - [Simplified CQRS & DDD](Simplified-CQRS-and-DDD) diff --git a/images/Bff/BFFs.png b/images/Bff/BFFs.png new file mode 100644 index 0000000000000000000000000000000000000000..95baa9449d62eb02888a58abcc8b6221e7f37175 GIT binary patch literal 14506 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 literal 0 HcmV?d00001