Любой организации, имеющей в своем распоряжении пару микросервисов, необходим контроль за тем, кто получает к ним доступ и на каких условиях. Такой контроль помогает установить 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.
Никаких проблем, производительность надежная, а шлюз легко масштабируется на любое количество экземпляров и расширяется бесконечным количеством способов. И написание тестов к его логике будет проще простого.
Спасибо за внимание и за то, что дочитали статью до конца!🙂
Читайте также:
- Реализация интерфейсов в Golang
- Бенчмарки в Golang: тестируем производительность кода
- Оптимизация структур в Golang для эффективного распределения памяти
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Igor Domrev: Why should you write your own API gateway — from scratch