Почему вам стоит написать свой 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.

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

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

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Igor Domrev: Why should you write your own API gateway — from scratch

Предыдущая статья5 проектов на React для начинающих
Следующая статьяeCommerce UI/UX дизайн: карточка товара в примерах