[Серверная оптимизация, API, DevOps] Haproxy — программирование и конфигурирование средствами Lua
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Сервер Haproxy имеет встроенные средства для выполнения скриптов Lua.
Язык программирования Lua для расширения возможностей различных серверов используется очень широко. Например, на Lua можно программировать для серверов Redis, Nginx (nginx-extras, openresty), Envoy. Это вполне закономерно, так как язык программирования Lua как раз и был разработан для удобства встраивания в приложения в качестве скриптового языка.
В этом сообщении я рассмотрю варианты использования Lua для расширения возможностей Haproxy.
Согласно документации, скрипты Lua на сервере Haproxy могут выполняться в шести контекстах:
— body context (контекст времени загрузки конфигурации сервера Haproxy, когда выполняются скрипты, заданные директивой lua-load);
— init context (контекст функций, которые вызываются сразу после загрузки конфигурации, и зарегистрированы системной функции core.register_init(function);
— task context (контекст функций, выполняемых по расписанию и зарегистрированных системной функцией core.register_task(function));
— action context (контекст функций, зарегистрированных системной функцией сore.register_action(function));
— sample-fetch context (контекст функций, зарегистрированных системной функцией сore.register_fetches(function));
— converter context (контекст функций, зарегистрированных системной функцией сore.register_converters(function)).
Фактически есть еще один контекст выполнения, который не указан в документации:
— service context (контекст функций, зарегистрированных системной функцией сore.register_service(function));
Начнем с самой простой конфигурации сервера Haproxy. Конфигурация состоит из двух секций frontend — то есть то, к чему обращается клиент с запросом, и backend — то, куда проксируется запрос клиента через сервер Haproxy:
frontend jwt
mode http
bind *:80
use_backend backend_app
backend backend_app
mode http
server app1 app:3000
Теперь все запросы, приходящие на порт 80 Haproxy будут перенаправлены на порт 3000 сервера app.
Services
Services — это функции, определенные в скриптах Lua, которые формируют ответ без обращения к бэкенду. Эти функции регистрируются вызовом системной функции сore.register_service(function)).
Определим простейший Service в файле guarde.lua:
function _M.hello_world(applet)
applet:set_status(200)
local response = string.format([[<html><body>Hello World!</body></html>]], message);
applet:add_header("content-type", "text/html");
applet:add_header("content-length", string.len(response))
applet:start_response()
applet:send(response)
end
И зарегистрируем ее как Service в файле register.lua:
package.path = package.path .. "./?.lua;/usr/local/etc/haproxy/?.lua"
local guard = require("guard")
core.register_service("hello-world", "http", guard.hello_world);
Параметр «http» является триггером, который допускает использование Service только в контексте http запроса (mode http).
Дополним конфигурацию сервера Haproxy:
global
lua-load /usr/local/etc/haproxy/register.lua
frontend jwt
mode http
bind *:80
use_backend backend_app
http-request use-service lua.hello-world if { path /hello_world }
backend backend_app
mode http
server app1 app:3000
Теперь, обратившись к серверу Haproxy с запросом /hello_world, клиент получит не ответ с проксируемого сервера, а ответ сервиса lua.hello-world.
В качестве параметра функции передается контекст запроса в параметре applet. Нет возможности передать дополнительные параметры файле конфигурации.
Actions
Actions — действия, выполняемые после получения запроса от клиента или после получения ответа от проксируемого сервера. Actions могут выполнять асинхронные операции (например запросы к базе данных) и не имеют возвращаемого значения. С сервером Actions общаются путем установки переменных контекста запроса. Контекст запроса предается в качестве параметра при вызове Action. Традиционно имя этого параметра txn. Создадим Action, который будет проверять наличие авторизации Bearer в запросе:
function _M.validate_token_action(txn)
local auth_header = core.tokenize(txn.sf:hdr("Authorization"), " ")
if auth_header[1] ~= "Bearer" or not auth_header[2] then
return txn:set_var("txn.not_authorized", true);
end
local claim = jwt.decode(auth_header[2],{alg="RS256",keys={public=public_key}});
if not claim then
return txn:set_var("txn.not_authorized", true);
end
if claim.exp < os.time() then
return txn:set_var("txn.authentication_timeout", true);
end
txn:set_var("txn.jwt_authorized", true);
end
Зарегистрируем этот Action:
core.register_action("validate-token", { "http-req" }, guard.validate_token_action);
Параметр { «http-req» } является триггером, который позволяет использовать этот Action только в контексте http и только на этапе запроса клиента (и запрещает использовать на этапе ответа проксируемого сервера).
В конфигурации Haproxy, Action регистрируется в секции http-request:
frontend jwt
mode http
bind *:80
http-request use-service lua.hello-world if { path /hello_world }
http-request lua.validate-token if { path -m beg /api/ }
На основании значения переменных, установленных в Action, формируются ACL (Access Control Lists) — ключевой элемент в конфигурациях Haproxy:
acl jwt_authorized var(txn.jwt_authorized) -m bool
use_backend app if jwt_authorized { path -m beg /api/ }
Полный листинг конфигурации сервера Haproxy для Action validate-token:
global
lua-load /usr/local/etc/haproxy/register.lua
frontend jwt
mode http
bind *:80
http-request use-service lua.hello-world if { path /hello_world }
http-request lua.validate-token if { path -m beg /api }
acl bad_request var(txn.bad_request) -m bool
acl not_authorized var(txn.not_authorized) -m bool
acl authentication_timeout var(txn.authentication_timeout) -m bool
acl too_many_request var(txn.too_many_request) -m bool
acl jwt_authorized var(txn.jwt_authorized) -m bool
http-request deny deny_status 400 if bad_request { path -m beg /api/ }
http-request deny deny_status 401 if !jwt_authorized { path -m beg /api/ } || not_authorized { path -m beg /api/ }
http-request return status 419 content-type text/html string "Authentication Timeout" if authentication_timeout { path -m beg /api/ }
http-request deny deny_status 429 if too_many_request { path -m beg /api/ }
http-request deny deny_status 429 if too_many_request { path -m beg /auth/ }
use_backend app if { path /hello }
use_backend app if { path /auth/login }
use_backend app if jwt_authorized { path -m beg /api/ }
backend app
mode http
server app1 app:3000
Fetches
Fetches — это значения которые вычисляются в процессе запроса. Они могут быть только синхронными, и принимают параметры, заданные в конфигурации Haproxy. Например, та же самая проверка авторизации может быть выполнена как Fetch:
function _M.validate_token_fetch(txn)
local auth_header = core.tokenize(txn.sf:hdr("Authorization"), " ")
if auth_header[1] ~= "Bearer" or not auth_header[2] then
return "not_authorized";
end
local claim = jwt.decode(auth_header[2],{alg="RS256",keys={public=public_key}});
if not claim then
return "not_authorized";
end
if claim.exp < os.time() then
return "authentication_timeout";
end
return "jwt_authorized:" .. claim.jti;
end
core.register_fetches("validate-token", _M.validate_token_fetch);
Установка ACL по значениям из Fetches задается так:
http-request set-var(txn.validate_token) lua.validate-token()
acl bad_request var(txn.validate_token) == "bad_request" -m bool
acl not_authorized var(txn.validate_token) == "not_authorized" -m bool
acl authentication_timeout var(txn.validate_token) == "authentication_timeout" -m bool
acl too_many_request var(txn.validate_token) == "too_many_request" -m bool
acl jwt_authorized var(txn.validate_token) -m beg "jwt_authorized"
Converters
Converters в качестве параметра принимают строку и возвращают значение. Converters, также как и Fetches, могут быть только синхронными и принимают параметры, задаваемые в конфигурации Haproxy. В конфигурации Haproxy Converters отделяются от значения, к которому они применяются, запятой.
Соаздадим Converter, который будет заголовку Authorization преобразовывать в строку:
function _M.validate_token_converter(auth_header_string)
local auth_header = core.tokenize(auth_header_string, " ")
if auth_header[1] ~= "Bearer" or not auth_header[2] then
return "not_authorized";
end
local claim = jwt.decode(auth_header[2],{alg="RS256",keys={public=public_key}});
if not claim then
return "not_authorized";
end
if claim.exp < os.time() then
return "authentication_timeout";
end
return "jwt_authorized";
end
core.register_converters("validate-token-converter", _M.validate_token_converter);
В файле конфигурации использование конвертера задается следующим образом:
http-request set-var(txn.validate_token) hdr(authorization),lua.validate-token-converter
К значениею заголовка Authorization, который извлекается системным Fetch hdr() применяется Converter lua.validate-token-converter.
Stick Table
Stick Table — это хранилище пар ключ-значение, которые оптимизировано для учета количества запросов в единицу времени, и служат, прежде всего, для защиты серверов от атак DDoS или брутфорса (напрмер перебора паролей или выкачки запросами REST больших объемов данных). В паре с такими средствами как Fetches и Converters, эти таблицы могут подсчитывать количество запросов, например, с определенным сессионным cookie или jti, не давая тем самым использовать одну авторизацию для организации распределенной атаки с сотен тысяч устройств. К положительным сторонам Stick Table относится скорость работы и простота конфигурирования. К отрицательным — ограниченное количество регистров для учета значений (всего восемь регистров), потребление памяти, потеря данных после перегрузки сервера Haproxy. Рассмотрим как задаются правила в Stick Table:
stick-table type string size 100k expire 30s store http_req_rate(10s)
http-request track-sc1 lua.validate-token()
http-request deny deny_status 429 if { sc_http_req_rate(1) gt 3 }
Строка 1. Создается таблица. В качестве ключа используется значение типа строка. Максимальный размер таблицы 100k. Срок хранения ключа 30 секунд. В качестве значения будут накапливаться количество запросов за последние 10 секунд с одинаковым значением ключа типа строка.
Строка 2. Задается значение ключа, полученного из Fetch lua.validate-token() и регистр 1, в котором будут накапливаться значения (track-sc1)
Строка 3. Если количество запросов с ключом, заданными в строке 2, накопленных в регистре с номером 1 (sc_http_req_rate(1)) превышает 3 — сервер отдает ответ со статусом 429.
Код использованный в данном сообщении доступен в репозитарии. В частности, там есть файл docker-compose.yml, который поможет поднять необходимую для работы среду.
apapacy@gmail.com
5 декабря 2020 года.
===========
Источник:
habr.com
===========
Похожие новости:
- [Облачные вычисления, DevOps, Kubernetes] Понимаем пробы Kubernetes: типы, настройка и лучшие практики (перевод)
- [Open source, Виртуализация, Облачные вычисления, Openshift] Подборка бесплатных книг по OpenShift, 4 преимущества стандартизованной операционной среды SOE и цифровая трансформация
- [Облачные вычисления, DevOps, Kubernetes] 10 антипаттернов деплоя в Kubernetes: распространенные практики, для которых есть другие решения (перевод)
- [.NET, ASP, C] Как создать простое Rest API на .NET Core
- [Высокая производительность, Анализ и проектирование систем, Серверная оптимизация, Распределённые системы] Архитектура отказоустойчивого планировщика задач. Доклад Яндекса
- [Разработка под Android, DevOps] Прокачиваем Android проект с GitHub Actions. Часть 1
- [JavaScript, Node.JS, Яндекс API, Голосовые интерфейсы] Салют от Сбера в Яндекс.Облаке
- [Разработка веб-сайтов, Системное администрирование, DevOps, Микросервисы] End User Monitoring на примере Instana
- [IT-инфраструктура, Amazon Web Services, DevOps, Облачные сервисы] AWS re:Invent. Главные анонсы первого дня (Part 2)
- [IT-инфраструктура, Amazon Web Services, DevOps, Облачные сервисы] AWS re:Invent. Главные анонсы первого дня (Part 1)
Теги для поиска: #_servernaja_optimizatsija (Серверная оптимизация), #_api, #_devops, #_haproxy, #_lua, #_jwt, #_servernaja_optimizatsija (
Серверная оптимизация
), #_api, #_devops
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:40
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Сервер Haproxy имеет встроенные средства для выполнения скриптов Lua. Язык программирования Lua для расширения возможностей различных серверов используется очень широко. Например, на Lua можно программировать для серверов Redis, Nginx (nginx-extras, openresty), Envoy. Это вполне закономерно, так как язык программирования Lua как раз и был разработан для удобства встраивания в приложения в качестве скриптового языка. В этом сообщении я рассмотрю варианты использования Lua для расширения возможностей Haproxy. Согласно документации, скрипты Lua на сервере Haproxy могут выполняться в шести контекстах: — body context (контекст времени загрузки конфигурации сервера Haproxy, когда выполняются скрипты, заданные директивой lua-load); — init context (контекст функций, которые вызываются сразу после загрузки конфигурации, и зарегистрированы системной функции core.register_init(function); — task context (контекст функций, выполняемых по расписанию и зарегистрированных системной функцией core.register_task(function)); — action context (контекст функций, зарегистрированных системной функцией сore.register_action(function)); — sample-fetch context (контекст функций, зарегистрированных системной функцией сore.register_fetches(function)); — converter context (контекст функций, зарегистрированных системной функцией сore.register_converters(function)). Фактически есть еще один контекст выполнения, который не указан в документации: — service context (контекст функций, зарегистрированных системной функцией сore.register_service(function)); Начнем с самой простой конфигурации сервера Haproxy. Конфигурация состоит из двух секций frontend — то есть то, к чему обращается клиент с запросом, и backend — то, куда проксируется запрос клиента через сервер Haproxy: frontend jwt
mode http bind *:80 use_backend backend_app backend backend_app mode http server app1 app:3000 Теперь все запросы, приходящие на порт 80 Haproxy будут перенаправлены на порт 3000 сервера app. Services Services — это функции, определенные в скриптах Lua, которые формируют ответ без обращения к бэкенду. Эти функции регистрируются вызовом системной функции сore.register_service(function)). Определим простейший Service в файле guarde.lua: function _M.hello_world(applet)
applet:set_status(200) local response = string.format([[<html><body>Hello World!</body></html>]], message); applet:add_header("content-type", "text/html"); applet:add_header("content-length", string.len(response)) applet:start_response() applet:send(response) end И зарегистрируем ее как Service в файле register.lua: package.path = package.path .. "./?.lua;/usr/local/etc/haproxy/?.lua"
local guard = require("guard") core.register_service("hello-world", "http", guard.hello_world); Параметр «http» является триггером, который допускает использование Service только в контексте http запроса (mode http). Дополним конфигурацию сервера Haproxy: global
lua-load /usr/local/etc/haproxy/register.lua frontend jwt mode http bind *:80 use_backend backend_app http-request use-service lua.hello-world if { path /hello_world } backend backend_app mode http server app1 app:3000 Теперь, обратившись к серверу Haproxy с запросом /hello_world, клиент получит не ответ с проксируемого сервера, а ответ сервиса lua.hello-world. В качестве параметра функции передается контекст запроса в параметре applet. Нет возможности передать дополнительные параметры файле конфигурации. Actions Actions — действия, выполняемые после получения запроса от клиента или после получения ответа от проксируемого сервера. Actions могут выполнять асинхронные операции (например запросы к базе данных) и не имеют возвращаемого значения. С сервером Actions общаются путем установки переменных контекста запроса. Контекст запроса предается в качестве параметра при вызове Action. Традиционно имя этого параметра txn. Создадим Action, который будет проверять наличие авторизации Bearer в запросе: function _M.validate_token_action(txn)
local auth_header = core.tokenize(txn.sf:hdr("Authorization"), " ") if auth_header[1] ~= "Bearer" or not auth_header[2] then return txn:set_var("txn.not_authorized", true); end local claim = jwt.decode(auth_header[2],{alg="RS256",keys={public=public_key}}); if not claim then return txn:set_var("txn.not_authorized", true); end if claim.exp < os.time() then return txn:set_var("txn.authentication_timeout", true); end txn:set_var("txn.jwt_authorized", true); end Зарегистрируем этот Action: core.register_action("validate-token", { "http-req" }, guard.validate_token_action);
Параметр { «http-req» } является триггером, который позволяет использовать этот Action только в контексте http и только на этапе запроса клиента (и запрещает использовать на этапе ответа проксируемого сервера). В конфигурации Haproxy, Action регистрируется в секции http-request: frontend jwt
mode http bind *:80 http-request use-service lua.hello-world if { path /hello_world } http-request lua.validate-token if { path -m beg /api/ } На основании значения переменных, установленных в Action, формируются ACL (Access Control Lists) — ключевой элемент в конфигурациях Haproxy: acl jwt_authorized var(txn.jwt_authorized) -m bool
use_backend app if jwt_authorized { path -m beg /api/ } Полный листинг конфигурации сервера Haproxy для Action validate-token: global
lua-load /usr/local/etc/haproxy/register.lua frontend jwt mode http bind *:80 http-request use-service lua.hello-world if { path /hello_world } http-request lua.validate-token if { path -m beg /api } acl bad_request var(txn.bad_request) -m bool acl not_authorized var(txn.not_authorized) -m bool acl authentication_timeout var(txn.authentication_timeout) -m bool acl too_many_request var(txn.too_many_request) -m bool acl jwt_authorized var(txn.jwt_authorized) -m bool http-request deny deny_status 400 if bad_request { path -m beg /api/ } http-request deny deny_status 401 if !jwt_authorized { path -m beg /api/ } || not_authorized { path -m beg /api/ } http-request return status 419 content-type text/html string "Authentication Timeout" if authentication_timeout { path -m beg /api/ } http-request deny deny_status 429 if too_many_request { path -m beg /api/ } http-request deny deny_status 429 if too_many_request { path -m beg /auth/ } use_backend app if { path /hello } use_backend app if { path /auth/login } use_backend app if jwt_authorized { path -m beg /api/ } backend app mode http server app1 app:3000 Fetches Fetches — это значения которые вычисляются в процессе запроса. Они могут быть только синхронными, и принимают параметры, заданные в конфигурации Haproxy. Например, та же самая проверка авторизации может быть выполнена как Fetch: function _M.validate_token_fetch(txn)
local auth_header = core.tokenize(txn.sf:hdr("Authorization"), " ") if auth_header[1] ~= "Bearer" or not auth_header[2] then return "not_authorized"; end local claim = jwt.decode(auth_header[2],{alg="RS256",keys={public=public_key}}); if not claim then return "not_authorized"; end if claim.exp < os.time() then return "authentication_timeout"; end return "jwt_authorized:" .. claim.jti; end core.register_fetches("validate-token", _M.validate_token_fetch); Установка ACL по значениям из Fetches задается так: http-request set-var(txn.validate_token) lua.validate-token()
acl bad_request var(txn.validate_token) == "bad_request" -m bool acl not_authorized var(txn.validate_token) == "not_authorized" -m bool acl authentication_timeout var(txn.validate_token) == "authentication_timeout" -m bool acl too_many_request var(txn.validate_token) == "too_many_request" -m bool acl jwt_authorized var(txn.validate_token) -m beg "jwt_authorized" Converters Converters в качестве параметра принимают строку и возвращают значение. Converters, также как и Fetches, могут быть только синхронными и принимают параметры, задаваемые в конфигурации Haproxy. В конфигурации Haproxy Converters отделяются от значения, к которому они применяются, запятой. Соаздадим Converter, который будет заголовку Authorization преобразовывать в строку: function _M.validate_token_converter(auth_header_string)
local auth_header = core.tokenize(auth_header_string, " ") if auth_header[1] ~= "Bearer" or not auth_header[2] then return "not_authorized"; end local claim = jwt.decode(auth_header[2],{alg="RS256",keys={public=public_key}}); if not claim then return "not_authorized"; end if claim.exp < os.time() then return "authentication_timeout"; end return "jwt_authorized"; end core.register_converters("validate-token-converter", _M.validate_token_converter); В файле конфигурации использование конвертера задается следующим образом: http-request set-var(txn.validate_token) hdr(authorization),lua.validate-token-converter
К значениею заголовка Authorization, который извлекается системным Fetch hdr() применяется Converter lua.validate-token-converter. Stick Table Stick Table — это хранилище пар ключ-значение, которые оптимизировано для учета количества запросов в единицу времени, и служат, прежде всего, для защиты серверов от атак DDoS или брутфорса (напрмер перебора паролей или выкачки запросами REST больших объемов данных). В паре с такими средствами как Fetches и Converters, эти таблицы могут подсчитывать количество запросов, например, с определенным сессионным cookie или jti, не давая тем самым использовать одну авторизацию для организации распределенной атаки с сотен тысяч устройств. К положительным сторонам Stick Table относится скорость работы и простота конфигурирования. К отрицательным — ограниченное количество регистров для учета значений (всего восемь регистров), потребление памяти, потеря данных после перегрузки сервера Haproxy. Рассмотрим как задаются правила в Stick Table: stick-table type string size 100k expire 30s store http_req_rate(10s)
http-request track-sc1 lua.validate-token() http-request deny deny_status 429 if { sc_http_req_rate(1) gt 3 } Строка 1. Создается таблица. В качестве ключа используется значение типа строка. Максимальный размер таблицы 100k. Срок хранения ключа 30 секунд. В качестве значения будут накапливаться количество запросов за последние 10 секунд с одинаковым значением ключа типа строка. Строка 2. Задается значение ключа, полученного из Fetch lua.validate-token() и регистр 1, в котором будут накапливаться значения (track-sc1) Строка 3. Если количество запросов с ключом, заданными в строке 2, накопленных в регистре с номером 1 (sc_http_req_rate(1)) превышает 3 — сервер отдает ответ со статусом 429. Код использованный в данном сообщении доступен в репозитарии. В частности, там есть файл docker-compose.yml, который поможет поднять необходимую для работы среду. apapacy@gmail.com 5 декабря 2020 года. =========== Источник: habr.com =========== Похожие новости:
Серверная оптимизация ), #_api, #_devops |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:40
Часовой пояс: UTC + 5