- PVSM.RU - https://www.pvsm.ru -

Пишем Grafana reverse proxy на Go

Пишем Grafana reverse proxy на Go - 1

Очень хотелось назвать статью «Proxy-сервис на Go в 3 строчки», но я выше этого.

В действительности так и есть, основную логику можно уместить в трёх строках. Для нетерпеливых и тех, кто хочет увидеть самую суть:

proxy := httputil.NewSingleHostReverseProxy(url)
r.Header.Set(header, value)
proxy.ServeHTTP(w, r)

Под катом более подробный рассказ для новичков в языке Golang и тех, кому нужно создать обратный прокси в кратчайшие сроки.

Разберём, для чего нужен прокси-сервис, как его реализовать и что под капотом у стандартной библиотеки.

Обратный прокси

Обратный прокси [1] (reverse proxy) — это тип прокси-сервера, который получает запрос от клиента, перенаправляет его на один или несколько серверов и пересылает ответ обратно.

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

Пишем Grafana reverse proxy на Go - 2
Обратный прокси (Reverse Proxy)

Для сравнения, обычный прокси связывает своих клиентов с любым сервером, который им необходим. В этом случае прокси находится перед пользователем и является просто посредником в выполнении запроса.

Пишем Grafana reverse proxy на Go - 3
Обычный прокси (Forward Proxy)

Для чего использовать
Концепцию обратного прокси можно применять в различных ситуациях:
— балансировка нагрузки,
— A/B-тестирование,
— кэширование ресурсов,
— сжатие данных запроса,
— фильтрация трафика,
— авторизация.

Конечно, область применения не ограничивается этими шестью пунктами. Сам факт возможности обработки запроса как до, так и после проксирования даёт большой простор для творчества. В этой статье разберём использование обратного прокси для авторизации.

Задача

Мы разрабатываем панель управления виртуализацией VMmanager 6. В один прекрасный день мы решили дать пользователям бóльшую свободу в мониторинге и визуализации данных кластеров. Для этих целей выбрали Grafana [2].

Чтобы Grafana заработала с нашими данными, надо было настроить авторизацию. Сделать это несложно, если бы не три «но».

  1. У нас уже есть единая точка входа — сервис авторизации.
  2. Мы не хотим заводить и авторизовывать пользователей в Grafana.
  3. Мы не хотим давать пользователям доступ к Grafana напрямую.

Чтобы соблюсти условия, решили поместить Grafana во внутреннюю сеть и написать обратный прокси. Он будет проверять права в сервисе авторизации и только после этого передавать запрос в Grafana.

Идея

Основная идея — переложить ответственность за авторизацию в Grafana на сервер обратного proxy (официальная документация [3]). Grafana будет принимать любой запрос как авторизованный, если он содержит определённый заголовок. Перед тем, как подставить этот заголовок, мы должны убедиться в правах текущего пользователя на работу с Grafana.

Пишем Grafana reverse proxy на Go - 4
Цепочка вызовов «Grafana-proxy, или Туда и обратно»

Реализация

Функция main довольно стандартна. Мы стартуем http-сервер, который будет принимать подключения на 4000 порту и обрабатывать любой адрес “/”, с которым произойдет подключение.

func main() {
    http.HandleFunc("/", handlerProxy)
    if err := http.ListenAndServe(":4000", nil); err != nil {
        panic(err)
    }
}

Основная часть работы происходит в обработчике запросов.

[полный код под катом]

func handlerProxy(w http.ResponseWriter, r *http.Request) {
    fmt.Println(r.URL.Host)
    if strings.HasPrefix(r.URL.String(), "/api") {
        //Проверка прав в сервисе авторизации
    }

    url, err := url.Parse(fmt.Sprintf("http://%s/", grafanaHost))
    if err != nil {
        SendJSONError(w, err.Error())
        return
    }
    proxy := httputil.NewSingleHostReverseProxy(url)

    fmt.Println(r.URL.Host)
    r.Header.Set(grafanaHeader, grafanaUser)

    proxy.ServeHTTP(w, r)
}

Пройдемся по параметрам. Основные переменные в примере я вынес в константы:

grafanaUser = "admin" //Пользователь, под которым мы будем авторизовываться
grafanaHost = "grafana:3000" //Адрес расположения grafana
grafanaHeader = "X-GRAFANA-AUTH" //Header, наличие которого определяет успешную авторизацию

Для примера этого вполне достаточно, на практике может потребоваться выполнять предустановку этих значений. Вы можете передавать их proxy как параметры командной строки, затем использовать flag [4] или более продвинутые пакеты для их разбора. В контейнерной среде также часто используются переменные окружения для конфигурации сервисов, на этом пути вам поможет os.Getenv [5].

Далее идет проверка авторизации:

if strings.HasPrefix(r.URL.String(), "/api") {
    err := CheckRights(r.Header)
    if err != nil {
        SendJSONError(w, err.Error())
        return
    }
}

Если запрос идёт на grafana.host/api, проверяем права текущего пользователя на использование Grafana. Проверка необходима, чтобы на каждый GET-запрос JS-скрипта или PNG-иконки не беспокоить точку авторизации. Статический контент мы будем проксировать без дополнительных проверок. Для этого передаем map с заголовками, в которых содержится сессия пользователя, в сервис авторизации. Это может быть обычный GET-запрос. Устройство сервиса авторизации здесь значения не имеет. Если данные авторизации не устраивают, закрываем соединение, возвращая ошибку.

После проверок формируем объект базового пути:

url, err := url.Parse(fmt.Sprintf("http://%s/", grafanaHost))

С помощью стандартного пакета httputil [6], расширяющего пакет http, формируем объект ReverseProxy.

proxy := httputil.NewSingleHostReverseProxy(url)

ReverseProxy — это обработчик запроса, который примет входящий запрос, отправит его в Grafana и передаст ответ обратно клиенту.

Он будет направлять все запросы по адресу “базовый путь + запрошенный url”. Если пользователь пришел по адресу proxy:4000/api/something, его запрос будет превращен в grafana:3000/api/something, где grafana:3000 — базовый путь, переданный в NewSingleHostReverseProxy, а /api/something — входящий запрос.

Добавляем авторизационный заголовок для Grafana и вызываем метод ServeHTTP, который сделает основную работу по обработке запроса.

r.Header.Set(grafanaHeader, grafanaUser)
proxy.ServeHTTP(w, r)

Под капотом ServeHTTP делает довольно много работы, например, обрабатывает заголовок X-Forwarded-For или 101 ответ сервера на смену протокола. Основная работа метода — отправить запрос на составной адрес и полученный ответ переложить в ResponseWriter.

Пишем Grafana reverse proxy на Go - 5
Результат

Весь код доступен на github [7].

Проверка

Эмулируем нашу систему с помощью Docker. Создадим два контейнера — proxy и Grafana в одной сети. Точку авторизации создавать не будем, считаем, что она всегда отвечает утвердительно. Контейнер Grafana будет недоступен вне сети, контейнер с proxy доступен на определённом порту.

Создаём сеть:

docker network create --driver=bridge --subnet=192.168.0.0/16 gnet

Поднимаем контейнер Grafana с настроенным режимом авторизации посредством заголовка:

docker run -d --name=grafana --network=gnet -e "GF_AUTH_PROXY_ENABLED=true" -e "GF_AUTH_PROXY_HEADER_NAME=X-GRAFANA-AUTH" grafana/grafana

Обращаю ваше внимание, что это демонстрационная и не окончательная конфигурация. Как минимум, необходимо установить пароль администратора и запретить автоматическую регистрацию пользователей.

Поднимаем reverse proxy:

docker run -d --name proxy -p 4000:4000 --network=gnet grafana_proxy:latest

В браузере переходим на localhost:4000.

Отлично, перед нами авторизованная Grafana.

Dockerfile для сборки контейнера с proxy и более подробная инструкция по поднятию контейнеров есть на github [7].

Автор: Дмитрий Сыроватский

Источник [8]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/340256

Ссылки в тексте:

[1] Обратный прокси: https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%80%D0%B0%D1%82%D0%BD%D1%8B%D0%B9_%D0%BF%D1%80%D0%BE%D0%BA%D1%81%D0%B8

[2] Grafana: https://grafana.com/

[3] официальная документация: https://grafana.com/docs/auth/auth-proxy/

[4] flag: https://golang.org/pkg/flag/

[5] os.Getenv: https://gobyexample.com/environment-variables

[6] httputil: https://golang.org/pkg/net/http/httputil/

[7] доступен на github: https://github.com/460s/grafana_proxy

[8] Источник: https://habr.com/ru/post/480446/?utm_source=habrahabr&utm_medium=rss&utm_campaign=480446