Пишем Grafana reverse proxy на Go
Очень хотелось назвать статью «Proxy-сервис на Go в 3 строчки», но я выше этого.
В действительности так и есть, основную логику можно уместить в трёх строках. Для нетерпеливых и тех, кто хочет увидеть самую суть:
proxy := httputil.NewSingleHostReverseProxy(url)
r.Header.Set(header, value)
proxy.ServeHTTP(w, r)
Под катом более подробный рассказ для новичков в языке Golang и тех, кому нужно создать обратный прокси в кратчайшие сроки.
Разберём, для чего нужен прокси-сервис, как его реализовать и что под капотом у стандартной библиотеки.
Обратный прокси
Обратный прокси (reverse proxy) — это тип прокси-сервера, который получает запрос от клиента, перенаправляет его на один или несколько серверов и пересылает ответ обратно.
Отличительная черта обратного прокси в том, что он является входной точкой для соединения пользователя с серверами, с которыми сам прокси связан бизнес-логикой. Он определяет, на какие серверы будет передан запрос клиента. Логика построения сети за прокси остается скрыта от пользователя.
Обратный прокси (Reverse Proxy)
Для сравнения, обычный прокси связывает своих клиентов с любым сервером, который им необходим. В этом случае прокси находится перед пользователем и является просто посредником в выполнении запроса.
Обычный прокси (Forward Proxy)
Для чего использовать
Концепцию обратного прокси можно применять в различных ситуациях:
— балансировка нагрузки,
— A/B-тестирование,
— кэширование ресурсов,
— сжатие данных запроса,
— фильтрация трафика,
— авторизация.
Конечно, область применения не ограничивается этими шестью пунктами. Сам факт возможности обработки запроса как до, так и после проксирования даёт большой простор для творчества. В этой статье разберём использование обратного прокси для авторизации.
Задача
Мы разрабатываем панель управления виртуализацией VMmanager 6. В один прекрасный день мы решили дать пользователям бóльшую свободу в мониторинге и визуализации данных кластеров. Для этих целей выбрали Grafana.
Чтобы Grafana заработала с нашими данными, надо было настроить авторизацию. Сделать это несложно, если бы не три «но».
- У нас уже есть единая точка входа — сервис авторизации.
- Мы не хотим заводить и авторизовывать пользователей в Grafana.
- Мы не хотим давать пользователям доступ к Grafana напрямую.
Чтобы соблюсти условия, решили поместить Grafana во внутреннюю сеть и написать обратный прокси. Он будет проверять права в сервисе авторизации и только после этого передавать запрос в Grafana.
Идея
Основная идея — переложить ответственность за авторизацию в Grafana на сервер обратного proxy (официальная документация). Grafana будет принимать любой запрос как авторизованный, если он содержит определённый заголовок. Перед тем, как подставить этот заголовок, мы должны убедиться в правах текущего пользователя на работу с Grafana.
Цепочка вызовов «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 или более продвинутые пакеты для их разбора. В контейнерной среде также часто используются переменные окружения для конфигурации сервисов, на этом пути вам поможет os.Getenv.
Далее идет проверка авторизации:
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, расширяющего пакет 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.
Результат
Весь код доступен на github.
Эмулируем нашу систему с помощью 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.