Почему вам стоит написать свой API-шлюз с нуля



Книга Почему вам стоит написать свой API-шлюз с нуля

Любой организации, имеющей в своем распоряжении пару микросервисов, необходим контроль за тем, кто получает к ним доступ и на каких условиях. Такой контроль помогает установить API-шлюз.


Но стоит ли использовать уже имеющийся настраиваемый прокси-сервер, такой как Envoy, Ngnix, Zuul, Kong, aws API gateway (и это еще не полный список)? При том, что у каждого из этих проектов свои достоинства и недостатки, собственный язык конфигурации, свое сообщество пользователей, книги, документы и руководства.


В этой статье попробуем показать: все, что вам нужно, достигается с помощью нескольких строк кода на Golang.


Это возможнj благодаря тем хорошим штукам, которые есть в "net/http/httputil". Это расширенный по сравнению с "net/http" пакет, который содержит сетевые утилиты.


Одна из таких утилит  —  httputil.ReverseProxy. Как следует из названия, она работает со всем, что связано с сетью и необходимо для прозрачной переадресации HTTP-запроса, и делает это в одной строке кода: Proxy(targetUrl).ServeHTTP(ctx.Writer, ctx.Request).


Для нее подходит любой HTTP-фреймворк на Golang, например gin или fast-http (одни из самых быстрых из имеющихся серверных фреймворков).


Обратный прокси-сервер не ограничивается прозрачным реверсным проксированием и используется для изменения перехваченных запросов и ответов на них.


Например, представим типичный сценарий, каждый запрос в котором должен быть:


  • зарегистрирован;
  • выполнен с метриками;
  • направлен в конкретный микросервис в соответствии с соглашением в пути запроса;
  • аутентифицирован (посредством установки заголовка запроса с незашифрованными данными пользователя);
  • авторизованным;
  • с ограничением скорости;
  • в промежуточной среде в случае с 500 в тело ответа должны быть внесены изменения, оно должно содержать сообщение об ошибке для совершенствования процесса разработки;
  • в эксплуатационной среде в нем должен быть хеш ошибки, задействуемый впоследствии при поиске логов.

Настройка NGNIX, ZUUL или ENVOY для соответствия перечисленным выше пунктам, вероятно, возможна, я не пробовал. Но наверняка она будет небеспроблемной. И насколько бы широкой ни была функциональность этих прокси-серверов, их настройку нельзя сравнить с гибкостью пользовательского кода.


Приведенный ниже пример кода на Golang содержит 87 строк, включающих все вышеперечисленные пункты, кроме аутентификации/авторизации / ограничения скорости, по которым имеется несоответствие, но которые фактически будут реализованы в виде промежуточного программного обеспечения для запросов.


Рассмотрим этот пример и поговорим об интересных его частях:


package reverse_proxy

import (
"bytes"
"fmt"
"github.com/viggin/svc-api-gateway/internal/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/palantir/stacktrace"
"github.com/sirupsen/logrus"
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
"strings"
)

func ReverseProxy(ctx *gin.Context) {
path := ctx.Request.URL.Path
target, err := Target(path)
if err != nil {
fire404Metric()
ctx.AbortWithStatus(http.StatusNotFound)
return
}
if targetUrl, err := url.Parse(target); err != nil {
ctx.AbortWithStatus(http.StatusInternalServerError)
} else {
Proxy(targetUrl).ServeHTTP(ctx.Writer, ctx.Request)
}
}

func fire404Metric() {
models.FailedToFindProxyTarget.Inc()
}

func Target(path string) (string, error) {
parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
if len(parts) <= 1 {
return "", stacktrace.RootCause(fmt.Errorf("failed to parse target host from path: %s", path))
}
targetHost := fmt.Sprintf("svc-%s", parts[1])
targetNamespace := fmt.Sprintf("svc-%s", parts[2])
if targetHost == "" {
return "", stacktrace.RootCause(fmt.Errorf("failed to parse target host from path: %s", path))
}
targetAddr := fmt.Sprintf(
"http://%s.%s:%d/api/%s",
targetHost, targetNamespace, 10000, strings.Join(parts[3:], "/"),
)
return targetAddr, nil
}

func Proxy(address *url.URL) *httputil.ReverseProxy {
p := httputil.NewSingleHostReverseProxy(address)
p.Director = func(request *http.Request) {
request.Host = address.Host
request.URL.Scheme = address.Scheme
request.URL.Host = address.Host
request.URL.Path = address.Path
}
p.ModifyResponse = func(response *http.Response) error {
if response.StatusCode == http.StatusInternalServerError {
u, s := readBody(response)
logrus.Errorf("%s ,req %s ,with error %d, body:%s", u.String(), address, response.StatusCode, s)
response.Body = ioutil.NopCloser(bytes.NewReader([]byte(fmt.Sprintf("error %s", u.String()))))
} else if response.StatusCode > 300 {
_, s := readBody(response)
logrus.Errorf("req %s ,with error %d, body:%s", address, response.StatusCode, s)
response.Body = ioutil.NopCloser(bytes.NewReader([]byte(s)))
}
return nil
}
return p
}

func readBody(response *http.Response) (uuid.UUID, string) {
defer response.Body.Close()
all, _ := ioutil.ReadAll(response.Body)
u := uuid.New()
var s string
if len(all) > 0 {
s = string(all)
}
return u, s
}

  • Функция Target возвращает внутренний адрес k8s микросервиса, к которому осуществляется обращение. Например, запрос к /api/product/frontend/users станет http://svc.product.frontend.cluster.local:10000/api/users (внутренним k8s DNS). Соглашение здесь такое, что путь запроса включает имя сервиса и пространство имен сервиса, а имя сервиса k8s  —  это svc-NAME.
  • p.ModifyResponse содержит код, с помощью которого в ответ вносятся изменения и регистрируются ошибки.
  • models.FailedToFindProxyTarget.Inc()  —  сообщение о метриках.
  • Proxy(targetUrl).ServeHTTP(ctx.Writer, ctx.Request) —  переадресация запроса (перенаправление).

Вот и всё. Нужно какое-либо пользовательское поведение? Настройте его здесь. Еще немного магии Golang  —  и этот код превратится в шлюз для запросов gRPC и GraphQL.


Никаких проблем, производительность надежная, а шлюз легко масштабируется на любое количество экземпляров и расширяется бесконечным количеством способов. И написание тестов к его логике будет проще простого.


Спасибо за внимание и за то, что дочитали статью до конца!🙂


1077   0  

Comments

    Ничего не найдено.