Nginx — великолепный веб-сервер. Все мы привыкли использовать его в связке с бекендомами на разных языках программирования. Но оказывается можно писать простые программы прямо внутри конфигурационного файла Nginx. Это можно использовать для балансировки, написания простых API и даже отдавать динамические страницы прямо из конфига.
В статье мы разберем примеры написания простых программ в конфиге nginx.
Выглядит это как написание кода в конфиге, что выглядит диковато, но удобно. Код выполняется асинхронно, не вмешиваясь в основной цикл событий Nginx, без коллбэков. Работает быстро и, что немаловажно, в совместимости с другими модулями и всем базовым функционалом.
Основным решением для Lua + Nginx считается OpenResty. Там много готовых модулей, как собственных на Lua, так и интегрированных из Nginx. Он отлично масштабируется и при этом сохраняет высокую производительность и пропускную способность Nginx.
Установка
Сам Nginx устанавливать не нужно, OpenResty включает его в свою сборку.
wget https://openresty.org/download/openresty-1.15.8.3.tar.gz tar -xvf openresty-1.15.8.3.tar.gz cd openresty-1.15.8.3/ sudo apt-get install build-essential
Также доустановим ещё несколько пакетов для поддержки CLI, регулярных выражений и SSL:
sudo apt-get install libreadline-dev libncurses5-dev libpcre3-dev libssl-dev perl
И начнём сборку
./configure -j2 --with-pcre-jit --with-ipv6 make -j2 sudo make install
Наконец, запускаем OpenResty
sudo /usr/local/openresty/bin/openresty
Вывода не последует, сервер просто запустится и будет доступен:

Остановка:
sudo /usr/local/openresty/bin/openresty -s quit
Hello world
Сначала создадим директорию и конфиг для нашего сайта
sudo mkdir /usr/local/openresty/nginx/sites sudo nano /usr/local/openresty/nginx/sites/default.conf
server { listen 80 default_server; listen [::]:80 default_server; root /usr/local/openresty/nginx/html/default; index index.html index.htm; location / { default_type 'text/plain'; content_by_lua_file /usr/local/openresty/nginx/html/default/index.lua; } }
Исполнять скрипты можно прямо в конфиге, но удобнее сразу подключить внешний файл
sudo nano /usr/local/openresty/nginx/html/default/index.lua
local name = ngx.var.arg_name or "Anonymous" ngx.say("Hello, ", name, "!")
sudo mkdir /usr/local/openresty/nginx/html/default sudo mv /usr/local/openresty/nginx/html/index.html /usr/local/openresty/nginx/html/default

Примеры
Ниже собраны более практичные примеры из разных источников:
ruhighload.com
server { location /hello { default_type 'text/html'; content_by_lua ' ngx.say("Hello <b>world</b>!") '; } }
server { location / { default_type 'text/plain'; content_by_lua_file /var/www/lua/index.lua; } location /admin { default_type 'text/plain'; content_by_lua_file /var/www/lua/admin.lua; } }
http { # объявляем глобальный контейнер lua_shared_dict stats 1m; server { location / { content_by_lua ' # увеличим переменную hits на 1 при каждом запросе ngx.shared.stats:incr("hits", 1) # выведем текущее значение ngx.say(ngx.shared.stats:get("hits")) '; } } }
apt-get install lua-nginx-redis
server { location / { content_by_lua ' local redis = require "nginx.redis" local red = redis:new() local ok, err = red:connect("127.0.0.1", 6379) ok, err = red:incr("test") local res, err = red:get("test") ngx.say("hits: ", res) '; } }
openresty.org
Routing MySQL Queries Based On URI Args
Dynamic Request Routing Based on Redis
Web App for OpenResty User Survey
Code and data for the openresty.org site — любой сайт, посвящённый определенной веб-технологии, использует её, и openresty.org не исключение
habr.com/ru/post/270463
-- search.lua local string = ngx.var.arg_string -- получим параметр из GET запроса if string == nil then ngx.exec("/") -- если параметра нет, то сделаем редирект end local path = "/?string=" .. string local redis = require "resty.redis" -- подключим библиотеку по работе с redis local red = redis:new() red:set_timeout(1000) -- 1 sec local ok, err = red:connect("127.0.0.1", 6379) if not ok then ngx.exec(path) -- если нельзя подключиться к redis, то сделаем редирект end res, err = red:get("search:" .. string); -- получим данные из redis if res == ngx.null then ngx.exec(path) -- если данных нет, то сделаем редирект else ngx.header.content_type = 'application/json' ngx.say(res) -- если данные есть, то отдадим их end
# nginx.conf location /search-by-string { content_by_lua_file lua/search.lua; }
habr.com/ru/post/326486
В блоке http {} инициализируем lua.
Код с комментариями:
# путь до локально установленных *.lua библиотек с добавлением системных путей lua_package_path "/usr/local/lib/lua/?.lua;;"; init_by_lua_block { -- подключение основного модуля -- в принципе, этот блок можно опустить require "resty.core" collectgarbage("collect") -- just to collect any garbage }в блоках *_lua_block уже идёт lua-код со своим синтаксисом и функциями.
Основной сервер, который принимает на себя внешние запросы.
Код с комментариями:
server { listen 80; server_name test.domain.local; location / { # проверяем наличие cookie "upid" и если нет — выставляем по желаемому алгоритму if ($cookie_upid = "") { # инициализируем пустую переменную nginx-а, в которую запишем выбранный ID бэкенда set $upstream_id ''; rewrite_by_lua_block { -- инициализируем математический генератор для более рандомного рандома используя время nginx-а math.randomseed(ngx.time()) -- также пропускаем первое значение, которое совсем не рандомное (см документацию) math.random(100) local num = math.random(100) -- получив число, бесхитростно и в лоб реализуем веса 20% / 80% if num > 20 then ngx.var.upstream_id = 1 ngx.ctx.upid = ngx.var.upstream_id else ngx.var.upstream_id = 2 ngx.ctx.upid = ngx.var.upstream_id end -- ID запоминаем в переменной nginx-а "upstream_id" и в "upid" таблицы ngx.ctx модуля lua, которая используется для хранения значений в рамках одного запроса } # отдаём клиенту куку "upid" со значением выбранного ID # время жизни явно не задаём, потому она будет действительна только на одну сессию (до закрытия браузера), что нас устраивает add_header Set-Cookie "upid=$upstream_id; Domain=$host; Path=/"; } # если же кука у клиента уже есть, то запоминаем ID в ngx.ctx.upid текущего запроса if ($cookie_upid != "") { rewrite_by_lua_block { ngx.ctx.upid = ngx.var.cookie_upid } } # передаём обработку запроса на блок upstream-ов proxy_pass http://ab_test; } }Блок upstream, который используя lua заменяет встроенную логику nginx.
Код с комментариями:
upstream ab_test { # заглушка, чтобы nginx не ругался. В алгоритме не участвует server 127.0.0.1:8001; balancer_by_lua_block { local balancer = require "ngx.balancer" -- инициализируем локальные переменные -- port выбираем динамически, в зависимости от запомненного ID бэкенда local host = "127.0.0.1" local port = 8000 + ngx.ctx.upid -- задаём выбранный upstream и обрабатываем код возврата local ok, err = balancer.set_current_peer(host, port) if not ok then ngx.log(ngx.ERR, "failed to set the current peer: ", err) return ngx.exit(500) end -- в общем случае надо, конечно же, искать доступный бэкенд, но нам не к чему } }Ну и простой демонстрационный бэкенд, на который в итоге придут клиенты.
код без комментариев:
server { listen 127.0.0.1:8001; server_name test.domain.local; location / { root /var/www/html; index index.html; } } server { listen 127.0.0.1:8002; server_name test.domain.local; location / { root /var/www/html; index index2.html; } }При запуске nginx-a с этой конфигурацией в логи свалится предупреждение:
use of lua-resty-core with LuaJIT 2.0 is not recommended; use LuaJIT 2.1+ instead while connecting to upstream
2Гис (пост)
Эту часть придумал и сделал наш коллега AotD. Есть хранилище картинок. Их надо показывать пользователям, причем желательно производить при этом некоторые операции, например, resize. Картинки мы храним в ceph, это аналог Amazon S3. Для обработки картинок используется ImageMagick. На ресайзере есть каталог с кэшем, туда складываются обработанные картинки.
Парсим запрос пользователя, определяем картинку, нужное ему разрешение и идем в ceph, затем на лету обрабатываем и показываем.
require "config" local function return_not_found(msg) ngx.status = ngx.HTTP_NOT_FOUND if msg then ngx.header["X-Message"] = msg end ngx.exit(0) end local name, size, ext = ngx.var.name, ngx.var.size, ngx.var.ext if not size or size == '' then return_not_found() end if not image_scales[size] then return_not_found('Unexpected image scale') end local cache_dir = static_storage_path .. '/' .. ngx.var.first .. '/' .. ngx.var.second .. '/' local original_fname = cache_dir .. name .. ext local dest_fname = cache_dir .. name .. size .. ext -- make sure the file exists local file = io.open(original_fname) if not file then -- download file contents from ceph ngx.req.read_body() local data = ngx.location.capture("/ceph_loader", {vars = { name = name .. ext }}) if data.status == ngx.HTTP_OK and data.body:len()>0 then os.execute( "mkdir -p " .. cache_dir ) local original = io.open(original_fname, "w") original:write(data.body) original:close() else return_not_found('Original returned ' .. data.status) end end local magick = require("imagick") magick.thumb(original_fname, image_scales[size], dest_fname) ngx.exec("@after_resize")
Подключаем биндинг imagic.lua. Должен быть доступен LuaJIT.
# Old images location ~ ^/(small|small_wide|medium|big|mobile|scaled|original|iphone_(preview|retina_preview|big|retina_big|small|retina_small))_ { rewrite /([^/]+)$ /__CEPH_BUCKET__/$1 break; proxy_pass __UPSTREAM__; } # Try get image from ceph, then from local cache, then from scaled by lua original # If image test.png is original, when user wants test_30x30.png: # 1) Try get it from ceph, if not exists # 2) Try get it from /cache/t/es/test_30x30.ong, if not exists # 3) Resize original test.png and put it in /cache/t/es/test_30x30.ong location ~ ^/(?<name>(?<first>.)(?<second>..)[^_]+)((?<size>_[^.]+)|)(?<ext>\.[a-zA-Z]*)$ { proxy_intercept_errors on; rewrite /([^/]+)$ /__CEPH_BUCKET__/$1 break; proxy_pass __UPSTREAM__; error_page 404 403 = @local; } # Helper failover location for upper command cause you can't write # try_files __UPSTREAM__ /cache/$uri @resizer =404; location @local { try_files /cache/$first/$second/$name$size$ext @resize; } # If scaled file not found in local cache resize it with lua magic! location @resize { # lua_code_cache off; content_by_lua_file "__APP_DIR__/lua/serve_image.lua"; } # serve scaled file, invoked in @resizer serve_image.lua location @after_resize { try_files /cache/$first/$second/$name$size$ext =404; } # used in @resizer serve_image.lua to download original image # $name contains original image file name location =/ceph_loader { internal; rewrite ^(.+)$ /__CEPH_BUCKET__/$name break; proxy_set_header Cache-Control no-cache; proxy_set_header If-Modified-Since ""; proxy_set_header If-None-Match ""; proxy_pass __UPSTREAM__; } location =/favicon.ico { return 404; } location =/robots.txt {}
Firewall для API. Валидация запроса, идентификация клиента, контроль rps и шлагбаум для тех, кто нам не нужен.
module(..., package.seeall); local function ban(type, element) CStorage.banPermanent:set(type .. '__' .. element, 1); ngx.location.capture('/postgres_ban', { ['vars'] = { ['type'] = type, ['value'] = element} }); end local function checkBanned(apiKey) -- init search criteria local searchCriteria = {}; searchCriteria['key'] = apiKey; if ngx.var.remote_addr then searchCriteria['ip'] = ngx.var.remote_addr; end; -- search in ban lists for type, item in pairs(searchCriteria) do local storageKey = type .. '__' .. item; if CStorage.banPermanent:get(storageKey) then ngx.exit(444); elseif CStorage.banTmp:get(storageKey) then -- calculate rps and check is our client still bad boy 8-) local rps = CStorage.RPS:incr(storageKey, 1); if not(rps) then CStorage.RPS:set(storageKey, 1, 1); rps=1; end; if rps then if rps > config.app_params['ban_params']['rps_for_ip_to_permanent_ban'] then CStorage.RPS:delete(storageKey); ban(type, item); ngx.exit(444); elseif config.app_params['ban_params']['rps_for_ip_to_tmp_ban'] > 0 and rps == config.app_params['ban_params']['rps_for_ip_to_tmp_ban'] then local attemptsCount = CStorage.banTmp:incr(storageKey, 1) - 1; if attemptsCount > config.app_params['ban_params']['tmp_ban']['max_attempt_to_exceed_rps'] then -- permanent ban CStorage.banTmp:delete(storageKey); ban(type, item); end; end; end; ngx.exit(444); end; end; end; local function checkTemporaryBlocked(apiKey) local blockedData = CStorage.tmpBlockedDemoKeys:get(apiKey); if blockedData then --storage.tmpBlockedDemoKeys:incr(apiKey, 1); -- think about it. return CApiException.throw('tmpDemoBlocked'); end; end; local function checkRPS(apiKey) local rps = nil; -- check rps for IP and ban it if it's needed if ngx.var.remote_addr then local ip = 'ip__' .. tostring(ngx.var.remote_addr); rps = CStorage.RPS:incr(ip, 1); if not(rps) then CStorage.RPS:set(ip, 1, 1); rps = 1; end; if rps > config.app_params['ban_params']['rps_for_ip_to_permanent_ban'] then ban('ip', tostring(ngx.var.remote_addr)); ngx.exit(444); elseif config.app_params['ban_params']['rps_for_ip_to_tmp_ban'] > 0 and rps > config.app_params['ban_params']['rps_for_ip_to_tmp_ban'] then CStorage.banTmp:set(ip, 1, config.app_params['ban_params']['tmp_ban']['time']); ngx.exit(444); end; end; local apiKey_key_storage = 'key_' .. apiKey['key']; -- check rps for key rps = CStorage.RPS:incr(apiKey_key_storage, 1); if not(rps) then CStorage.RPS:set(apiKey_key_storage, 1, 1); rps = 1; end; if apiKey['max_rps'] and rps > tonumber(apiKey['max_rps']) then if apiKey['mode'] == 'demo' then CApiKey.blockTemporary(apiKey['key']); return CApiException.throw('tmpDemoBlocked'); else CApiKey.block(apiKey['key']); return CApiException.throw('blocked'); end; end; -- similar check requests per period (RPP) for key if apiKey['max_request_count_per_period'] and apiKey['period_length'] then local rpp = CStorage.RPP:incr(apiKey_key_storage, 1); if not(rpp) then CStorage.RPP:set(apiKey_key_storage, 1, tonumber(apiKey['period_length'])); rpp = 1; end; if rpp > tonumber(apiKey['max_request_count_per_period']) then if apiKey['mode'] == 'demo' then CApiKey.blockTemporary(apiKey['key']); return CApiException.throw('tmpDemoBlocked'); else CApiKey.block(apiKey['key']); return CApiException.throw('blocked'); end; end; end; end; function run() local apiKey = ngx.ctx.REQUEST['key']; if not(apiKey) then return CApiException.throw('unauthorized'); end; apiKey = tostring(apiKey) -- check permanent and temporary banned checkBanned(apiKey); -- check api key apiKey = CApiKey.getData(apiKey); if not(apiKey) then return CApiException.throw('forbidden'); end; apiKey = JSON:decode(apiKey); if not(apiKey['is_active']) then return CApiException.throw('blocked'); end; apiKey['key'] = tostring(apiKey['key']); -- check is key in tmp blocked list if apiKey['mode'] == 'demo' then checkTemporaryBlocked(apiKey['key']); end; -- check requests count per second and per period checkRPS(apiKey); -- set apiKey's json to global parameter; in index.lua we send it through nginx to php application ngx.ctx.GLOBAL['api_key'] = JSON:encode(apiKey); end;
module(..., package.seeall); local function checkApiVersion() local apiVersion = ''; if not (ngx.ctx.REQUEST['version']) then local nginx_request = tostring(ngx.var.uri); local version = nginx_request:sub(2,4); if tonumber(version:sub(1,1)) and tonumber(version:sub(3,3)) then apiVersion = version; else return CApiException.throw('versionIsRequired'); end; else apiVersion = ngx.ctx.REQUEST['version']; end; local isSupported = false; for i, version in pairs(config.app_params['supported_api_version']) do if apiVersion == version then isSupported = true; end; end; if not (isSupported) then CApiException.throw('unsupportedVersion'); end; ngx.ctx.GLOBAL['api_version'] = apiVersion; end; local function checkKey() if not (ngx.ctx.REQUEST['key']) then CApiException.throw('unauthorized'); end; end; function run() checkApiVersion(); checkKey(); end;
module ( ..., package.seeall ) function init() if not(ngx.ctx.GLOBAL['CApiKey']) then ngx.ctx.GLOBAL['CApiKey'] = {}; end end; function flush() CStorage.apiKey:flush_all(); CStorage.apiKey:flush_expired(); end; function load() local dbError = nil; local dbData = ngx.location.capture('/postgres_get_keys'); dbData = dbData.body; dbData, dbError = rdsParser.parse(dbData); if dbData ~= nil then local rows = dbData.resultset if rows then for i, row in ipairs(rows) do local cacheKeyData = {}; for col, val in pairs(row) do if val ~= rdsParser.null then cacheKeyData[col] = val; else cacheKeyData[col] = nil; end end CStorage.apiKey:set(tostring(cacheKeyData['key']),JSON:encode(cacheKeyData)); end; end; end; end; function checkNotEmpty() if not(ngx.ctx.GLOBAL['CApiKey']['loaded']) then local cnt = CHelper.tablelength(CStorage.apiKey:get_keys(1)); if cnt == 0 then load(); end; ngx.ctx.GLOBAL['CApiKey']['loaded'] = 1; end; end; function getData(key) checkNotEmpty(); return CStorage.apiKey:get(key); end; function getStatus(key) key = getData(key); local result = ''; if key ~= nil then key = JSON:decode(key); if key['is_active'] ~= nil and key['is_active'] == true then result = 'allowed'; else result = 'blocked'; end; else result = 'forbidden'; end; return result; end; function blockTemporary(apiKey) apiKey = tostring(apiKey); local isset = getData(apiKey); if isset then CStorage.tmpBlockedDemoKeys:set(apiKey, 1, config.app_params['ban_params']['time_demo_apikey_block_tmp']); end; end; function block(apiKey) apiKey = tostring(apiKey); local keyData = getData(apiKey); if keyData then ngx.location.capture('/redis_get', { ['vars'] = { ['key'] = apiKey } }); keyData['is_active'] = false; CStorage.apiKey:set(apiKey,JSON:encode(cacheKeyData)); end; end;
module ( ..., package.seeall ) apiKey = ngx.shared.apiKey; RPS = ngx.shared.RPS; RPP = ngx.shared.RPP; banPermanent = ngx.shared.banPermanent; banTmp = ngx.shared.banTmp; tmpBlockedDemoKeys = ngx.shared.tmpBlockedDemoKeys;
Бонус! Примеры без использования Lua вообще.
Только конфиги, только хардкор
server { listen 80; server_name example.org; } server { listen 80; server_name www.example.org; return 301 $scheme://example.org$request_uri; }
server { listen 80; return 301 https://$host$request_uri; } server { listen 443 ssl; # let the browsers know that we only accept HTTPS add_header Strict-Transport-Security max-age=2592000; ... }
location /old-site { rewrite ^/old-site/(.*) http://example.org/new-site/$1 permanent; }
open_file_cache max=1000 inactive=20s; open_file_cache_valid 30s; open_file_cache_min_uses 2; open_file_cache_errors on;
upstream backend { server 127.0.0.1:8080; keepalive 32; } server { ... location /api/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection ""; } }
Напоследок, большой список конфигурационных шаблонов с Lua и без, с разной степенью сложности
Заключение
Lua в Nginx в общем и OpenResty в частности гораздо быстрее и легковеснее php. Они помогают расширить базовый функционал Nginx, сделать его гибче, сохранив скорость обработки запроса и возможность тонкой настройки. OpenResty использует в проде огромное количество компаний, обеспечивая ему богатую экосистему и сильную поддержку комьюнити. Поле для экспериментов с Lua почти не ограничено, поэтому он может пригодиться в самых неожиданных местах. Если вы еще не пробовали Lua-in-Nginx, самое время изучить эту тему подробнее.
На правах рекламы
Необходим сервер для размещения сайта? Наша компания предлагает надёжные серверы с посуточной или единоразовой оплатой, каждый сервер подключён к интернет-каналу в 500 Мегабит и бесплатно защищён от DDoS-атак!
ссылка на оригинал статьи https://habr.com/ru/company/vdsina/blog/504308/

Добавить комментарий