Monday, August 23, 2021

Using Go Echo as a Dynamic Reverse Proxy to Multiple Micro-Services

 

In the post Using NGINX auth_request to proxy to dynamically multiple backend servers, I've reviewed the process of using an NGINX auth request to handle the routing decision making, as displayed in the following diagram.




While this method is working fine, there is a cost to such usage. Each request, is sent from the NGINX to the router decision maker, so we need to wait for the send and receive round-trip.

This time, we're going to "merge" the NGINX reverse proxy, and the router micro-service in one micro-service. 




To implement, we will use GO Echo library, which supplies a dynamic reverse proxy capabilities. Let's start with a standard Echo based HTTP server, which implements response to health checks, for example - from the kubernetes liveness and readiness probes:

package main

import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
)

func main() {
server := echo.New()

server.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowHeaders: []string{"*"},
AllowMethods: []string{"POST", "GET"},
AllowCredentials: true,
}))

server.POST("/health", func(ctx echo.Context) error {
ctx.JSON(http.StatusOK, "i am alive")
return nil
})

err := server.Start(":8080")
if err != nil {
panic(err)
}
}



A simple test of our service, would return HTTP code 200, with the "i am alive" payload.



curl -X POST http://localhost:8080/health
"i am alive"
  



Now, let's add the reverse proxy code. Notice that we add this code as an additional middleware over the existing server.



server := echo.New()

// proxy middleware - start
router := produceRouter()
config := middleware.ProxyConfig{
Balancer: router,
Skipper: router.Skip,
}
proxyMiddleware := middleware.ProxyWithConfig(config)
server.Use(proxyMiddleware)
// proxy middleware - end

server.Use(middleware.CORSWithConfig(middleware.CORSConfig{



In this example, our middleware routing logic is dynamically selecting the backend service based on the request host name. Another important issue the the Skip function, allowing us to skip the reverse proxy method, and behave as a standard web server. In this example, we skip the reverse proxy middleware in case the request host name is an IP address. This is very relevant if we want our health probes to continue to function, so we count on kubernetes that uses an IP address instead of host name.



type Router struct {
serviceA *url.URL
serviceB *url.URL
}

func produceRouter() *Router {
serviceA, err := url.Parse("http://service-a.com")
if err != nil {
panic(err)
}

serviceB, err := url.Parse("http://service-b.com")
if err != nil {
panic(err)
}

return &Router{
serviceA: serviceA,
serviceB: serviceB,
}
}

func (b Router) AddTarget(*middleware.ProxyTarget) bool {
return true
}

func (b Router) RemoveTarget(string) bool {
return true
}

func (b Router) Next(context echo.Context) *middleware.ProxyTarget {
urlTarget := b.getProxyUrl(context)

return &middleware.ProxyTarget{
Name: urlTarget.Host,
URL: urlTarget,
Meta: nil,
}
}

func (b Router) getProxyUrl(context echo.Context) *url.URL {
host:= context.Request().Host
if strings.HasPrefix(host,"a"){
return b.serviceA
}

return b.serviceB
}

func (b Router) Skip(context echo.Context) bool {
host := context.Request().Host
firstChar := host[0]
if firstChar >= '0' && firstChar <= '9' {
// fast and naive IP detection
return true
}
return false
}



Final Note


In this post we have implemented a dynamic reverse proxy using GO Echo library. This kind of an implementation could be used for A/B, Blue/Green, and canary selection of a backend functionality. All we need to change is the getProxyUrl to our decision making logic, which can be based on source IP address, request host name, or a randomized split of requests.




No comments:

Post a Comment