Ebben a cikkben szeretném megosztani tapasztalataimat az NJS-sel, az Nginx-hez készült JavaScript értelmezővel, amelyet az Nginx Inc. fejlesztett, bemutatva egy valós példán keresztül a főbb lehetőségeit. Az NJS a JavaScript nyelv egy alműve, amely lehetővé teszi az Nginx funkcionalitásának kiterjesztését. A kérdésre, Dmitrij Volynczev részletesen válaszolt. Röviden: az NJS az nginx-way, míg a JavaScript progresszívebb, „otthoni” és a Lua-val szemben nem tartalmaz GC-t.
Réges-régen...
A korábbi munkahelyemen egy GitLab örökségét örököltem, amelyben különféle CI/CD pipeline-ok voltak docker-compose-szal, dind-del és más csodákkal, amelyeket kaniko-ra állítottak. Azok a képek, amelyeket a CI során használtak, eredeti formájukban maradtak. Hibátlanul működtek, amíg a GitLab IP címe meg nem változott, és a CI tökölni kezdett. A probléma az volt, hogy az egyik docker-képben, amely részt vett a CI-ban, git volt, ami ssh-n keresztül húzta a Python modulokat. Az ssh-hoz privát kulcs szükséges, és... ez a képben volt a known_hosts-szal együtt. Így bármely CI a kulcs ellenőrzésének hibájával zárult, mivel a valós IP nem egyezett a known_hosts-ban megadottal. A meglévő Dockfile-ból gyorsan egy új képet állítottunk össze, és hozzáadtuk a StrictHostKeyChecking no. De az íze még mindig ott maradt, és megjelent a vágy, hogy a libeket egy privát PyPI tárolóba költöztessük. További bónuszként a privát PyPI-ra való áttérés egy egyszerűbb pipeline-t és a requirements.txt normál leírását jelentette.
A választás megszületett, Uraim!
Mindannyian felhőkben és Kubernetesben dolgozunk, és végül szerettünk volna egy kis szolgáltatást, amely egy stateless konténert képviselt külső tárolóval. Mivel S3-at használunk, így ennek prioritást adtunk. És lehetőség szerint a gitlabba való hitelesítéssel (saját magunk is megírhatjuk szükség esetén).
Gyors keresés néhány eredményt hozott: s3pypi, pypicloud és a „kézi” HTML fájlok készítési lehetősége a tárolóhoz. Az utolsó lehetőség magától elvetődött.
s3pypi: Ez egy CLI az S3 hosztolás használatához. Feltöltjük a fájlokat, generálunk HTML-t, és feltöltjük az ugyanabba a tartályba. Házi használatra megfelelő.
A pypicloud: Érdekes projektnek tűnt, de a dokumentáció elolvasása után csalódás ért. Bár a dokumentáció jó és van lehetőség a saját igényekhez való bővítésre, valójában túl bonyolult és nehezen konfigurálható. A kódot a saját igényekhez igazítani, a korábbi becslések szerint, 3-5 napot venne igénybe. Ezen kívül a szolgáltatáshoz szükséges egy adatbázis. Hagyjuk meg, hátha más megoldást nem találunk.
A mélyebb keresés egy modult hozott a Nginxhez, az ngx_aws_auth-ot. A tesztelés eredménye egy XML volt, ami a böngészőben megjelenítve látszott, tartalmazta az S3 tároló tartalmát. Az utolsó commit, a keresés idején, egy éve készült. A repó elhagyottnak tűnt.
Miután felkerestem az elsődleges forrást és elolvastam , rájöttem, hogy az XML-t menetközben HTML-re lehet konvertálni és továbbítani a pip-nek. Még egy kicsit guglizva az Nginx és az S3 szavakkal, ráakadtam egy példára, ami az S3 autentikációját mutatta be JS-ben az Nginx számára. Így megismerkedtem az NJS-sel.
Ezt a példát alapul véve, egy óra elteltével a böngészőmben ugyanazt az XML-t láttam, amit az ngx_aws_auth modul használatakor, de most már minden JS-ben volt megírva.
Nagyon tetszett a megoldás Nginx-re. Először is, jó dokumentáció és számos példa van, másodszor pedig minden előnyt megkapunk az Nginx fájlkezelésével (alapból), harmadszor bármelyik ember, aki tudja írni a Nginx konfigurációkat, el tudja magyarázni, miről van szó. Továbbá, számomra a minimalizmus is előny, a Python vagy Go-hoz képest (ha nulláról írnánk), nem is beszélve a nexus-ról.
TL;DR Két nap múlva a PyPi tesztverzióját már használtuk a CI-ben.
Hogyan működik ez?
A Nginx-hez betöltjük a modult ngx_http_js_module, amely benne van a hivatalos docker képfájlban. Betöltjük a skriptünket a js_importdirektíva segítségével a Nginx konfigurációjába. A funkció hívása a következő direktívával történik js_content. A változók beállításához a következő direktívát használjuk js_set, amelynek argumentuma csak a skriptben leírt függvény lehet. Az NJS-ben azonban al-kérdéseket csak az Nginx segítségével tudunk végrehajtani, semmiféle XMLHttpRequest. Ehhez a Nginx konfigurációjába hozzá kell adni a megfelelő helyet. A skriptben pedig al-kérdést (subrequest) kell leírni ehhez a helyhez. Ahhoz, hogy lehetőségünk legyen a Nginx konfigurációból hivatkozni a függvényre, a skriptben a függvény nevét exportálni kell. export default.
nginx.conf
load_module modules/ngx_http_js_module.so;
http {
js_import imported_name from script.js;
server {
listen 8080;
...
location = /sub-query {
internal;
proxy_pass http://upstream;
}
location / {
js_content imported_name.request;
}
}script.js
function request(r) {
function call_back(resp) {
// handler's code
r.return(resp.status, resp.responseBody);
}
r.subrequest('/sub-query', { method: r.method }, call_back);
}
export default {request}A böngésző kérésnél http://localhost:8080/ az alábbi helyre érkezünk location /amelyben az utasítás js_content megkeresi a függvényt request amit a szkriptünkben írunk le script.js. A függvényben request al-al kérés készül location = /sub-query, a metódussal (jelen esetben GET) amely az argumentumból származik (r), amely implicit módon átadásra kerül a függvény meghívásakor. A válasz feldolgozása az al-kérésnek a függvényben call_back.
Próbáljuk ki az S3-at
A privát S3-tárolóhoz való kéréshez a következőkre van szükségünk:
ACCESS_KEY
SECRET_KEY
S3_BUCKET
Az alkalmazott http-metódus, a jelenlegi dátum/idő, az S3_NAME és a URI alapján egy adott formátumú string generálódik, amelyet a SECRET_KEY segítségével aláírunk (HMAC_SHA1). A string, amely a következő képet mutatja: AWS $ACCESS_KEY:$HASH, felhasználható az engedélyezési fejlécben. Ugyanazt a dátum/időt, amelyet az előző lépésben a string generálásához használtunk, hozzá kell adni a X-amz-datefejléchez. A kód így néz ki:
nginx.conf
load_module modules/ngx_http_js_module.so;
http {
js_import s3 from s3.js;
js_set $s3_datetime s3.date_now;
js_set $s3_auth s3.s3_sign;
server {
listen 8080;
...
location ~* /s3-query/(?.*) {
internal;
proxy_set_header X-amz-date $s3_datetime;
proxy_set_header Authorization $s3_auth;
proxy_pass $s3_endpoint/$s3_path;
}
location ~ "^/(?[w-]*)[\/]?(?[w-.]*)$" {
js_content s3.request;
}
}s3.js(AWS Sign v2 autorálási példa, amely elavult státuszba került)
var crypt = require('crypto');
var s3_bucket = process.env.S3_BUCKET;
var s3_access_key = process.env.S3_ACCESS_KEY;
var s3_secret_key = process.env.S3_SECRET_KEY;
var _datetime = new Date().toISOString().replace(/[:-]|.d{3}/g, '');
function date_now() {
return _datetime
}
function s3_sign(r) {
var s2s = r.method + 'nnnn';
s2s += `x-amz-date:${date_now()}n`;
s2s += '/' + s3_bucket;
s2s += r.uri.endsWith('/') ? '/' : r.variables.s3_path;
return `AWS ${s3_access_key}:${crypt.createHmac('sha1', s3_secret_key).update(s2s).digest('base64')}`;
}
function request(r) {
var v = r.variables;
function call_back(resp) {
r.return(resp.status, resp.responseBody);
}
var _subrequest_uri = r.uri;
if (r.uri === '/') {
// root
_subrequest_uri = '/?delimiter=/';
} else if (v.prefix !== '' && v.postfix === '') {
// directory
var slash = v.prefix.endsWith('/') ? '' : '/';
_subrequest_uri = '/?prefix=' + v.prefix + slash;
}
r.subrequest(`/s3-query${_subrequest_uri}`, { method: r.method }, call_back);
}
export default {request, s3_sign, date_now}Feltételezzük, hogy _subrequest_uri: ez a változó az alapértelmezett uri-tól függően generálja a kérdést az S3-hoz. Ha a „gyökér” tartalmát kell megkapnunk, akkor szükséges a kérési uri létrehozása, amely tartalmazza a határolót delimiter, amely visszaadja az összes xml-elemet a CommonPrefixes-ben, amely megfelel a mappáknak (ha a PyPI-ról beszélünk, akkor az összes csomag listája). Ha egy adott mappa tartalmának listáját szeretnénk kapni (az összes csomag verzióját), akkor az uri-kérdésnek tartalmaznia kell a prefix mezőt a mappa (csomag) nevével, amely szükségszerűen perjellel (/) végződik. Ellenkező esetben ütközések léphetnek fel a mappa tartalmának lekérésekor, például. Léteznek mappák aiohttp-request és aiohttp-requests nevekkel, és ha a kérésben ez szerepel /?prefix=aiohttp-request, akkor a válasz mindkét könyvtár tartalmát tartalmazza. Ha viszont a végén perjel van, /?prefix=aiohttp-request/, akkor a válasz csak a szükséges könyvtárat tartalmazza. Ha egy fájlt kérünk, az eredményül kapott uri nem térhet el az eredetitől.
Mentjük, újraindítjuk az Nginx-et. A böngészőbe beírjuk az Nginx címét, a kérés eredményeként XML választ kapunk, például:
Könyvtárak listája
myback-space
10000
/
false
new/
old/A könyvtárak listájából csak az elemekre lesz szükségünk CommonPrefixes.
Ha a böngészőben hozzáadjuk a címünkhöz a szükséges könyvtárat, akkor annak tartalmát is XML formátumban kapjuk meg:
Fájlok listája a könyvtárban
myback-space
old/
10000
false
old/giphy.mp4
2020-08-21T20:27:46.000Z
"00000000000000000000000000000000-1"
1350084
02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4
STANDARD
old/hsd-k8s.jpg
2020-08-31T16:40:01.000Z
"b2d76df4aeb4493c5456366748218093"
93183
02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4
STANDARDA fájlok listájából csak az elemekre van szükségünk Kulcs.
Most a kapott XML-t elemezni kell, és HTML formátumban vissza kell adni, miután a Content-Type fejlécet text/html-ra cseréltük.
function request(r) {
var v = r.variables;
function call_back(resp) {
var body = resp.responseBody;
if (r.method !== 'PUT' && resp.status < 400 && v.postfix === '') {
r.headersOut['Content-Type'] = "text/html; charset=utf-8";
body = toHTML(body);
}
r.return(resp.status, body);
}
var _subrequest_uri = r.uri;
...
}
function toHTML(xml_str) {
var keysMap = {
'CommonPrefixes': 'Prefix',
'Contents': 'Key',
};
var pattern = `<k>(?<v>.*?)</k>`;
var out = [];
for(var group_key in keysMap) {
var reS;
var reGroup = new RegExp(pattern.replace(/k/g, group_key), 'g');
while(reS = reGroup.exec(xml_str)) {
var data = new RegExp(pattern.replace(/k/g, keysMap[group_key]), 'g');
var reValue = data.exec(reS);
var a_text = '';
if (group_key === 'CommonPrefixes') {
a_text = reValue.groups.v.replace(///g, '');
} else {
a_text = reValue.groups.v.split('/').slice(-1);
}
out.push(`<a href="/${reValue.groups.v}">${a_text}</a>`);
}
}
return '<html><body>n' + out.join('</br>n') + 'n</html></body>'
}Próbálkozunk a PyPI-vel
Ellenőrizzük, hogy semmi sem törik el a bevált csomagokban.
# Создаем для тестов новое окружение
python3 -m venv venv
. ./venv/bin/activate
# Скачиваем рабочие пакеты.
pip download aiohttp
# Загружаем в приватную репу
for wheel in *.whl; do curl -T $wheel http://localhost:8080/${wheel%%-*}/$wheel; done
rm -f *.whl
# Устанавливаем из приватной репы
pip install aiohttp -i http://localhost:8080Megismételjük a folyamatot a könyvtárainkkal.
# Создаем для тестов новое окружение
python3 -m venv venv
. ./venv/bin/activate
pip install setuptools wheel
python setup.py bdist_wheel
for wheel in dist/*.whl; do curl -T $wheel http://localhost:8080/${wheel%%-*}/$wheel; done
pip install our_pkg --extra-index-url http://localhost:8080A CI-ben a csomag készítése és feltöltése így néz ki:
pip install setuptools wheel
python setup.py bdist_wheel
curl -sSfT dist/*.whl -u "gitlab-ci-token:${CI_JOB_TOKEN}" "https://pypi.our-domain.com/${CI_PROJECT_NAME}"Hitelesítés
A Gitlab-ban lehet használni a JWT-t külső szolgáltatások hitelesítésére/autorizálására. Az Nginx auth_request utasításával átirányítjuk a hitelesítési adatokat egy belső kérésre, amely tartalmazza a funkció hívását a szkriptben. A szkriptben egy újabb belső kérést indítunk a Gitlab URL-jére, és ha a hitelesítési adatok helyesen vannak megadva, akkor a Gitlab 200-as státuszkódot ad vissza, és engedélyezi a csomag feltöltését/letöltését. Miért ne használjunk egyetlen belső kérést, és ne küldjük el az adatokat egyből a Gitlabnak? Mert akkor minden alkalommal módosítani kell az Nginx konfigurációs fájlt, amikor valamilyen változás történik a hitelesítésben, és ez elég időigényes feladat. Továbbá, ha a Kubernetes read-only root filesystem politikáját használja, akkor még több nehézséget ad a nginx.conf configmap átcserélése. Így az Nginx konfigurálása a configmap-en keresztül abszolút lehetetlenné válik, ha párhuzamosan használják a kötetek (pvc) csatlakozását tiltó politikákat és a read-only root filesystem-et (ez is megtörténhet).
Az NJS közvetítőként való használatával lehetőségünk nyílik az nginx konfigurációjában megadott paraméterek módosítására környezeti változók segítségével, és ellenőrzéseket végezhetünk a szkriptben (például hibásan megadott URL esetén).
nginx.conf
location = /auth-provider {
internal;
proxy_pass $auth_url;
}
location = /auth {
internal;
proxy_set_header Content-Length "";
proxy_pass_request_body off;
js_content auth.auth;
}
location ~ "^/(?[w-]*)[\/]?(?[w-.]*)$" {
auth_request /auth;
js_content s3.request;
}s3.js
var env = process.env;
var env_bool = new RegExp(/[Tt]rue|[Yy]es|[Oo]n|[TtYy]|1/);
var auth_disabled = env_bool.test(env.DISABLE_AUTH);
var gitlab_url = env.AUTH_URL;
function url() {
return `${gitlab_url}/jwt/auth?service=container_registry`
}
function auth(r) {
if (auth_disabled) {
r.return(202, '{"auth": "disabled"}');
return null;
}
r.subrequest('/auth-provider',
{method: 'GET', body: ''},
function(res) {
r.return(res.status, "");
});
}
export default {auth, url}Valószínűleg felmerül a kérdés: - De miért ne használjunk kész modulokat? Hiszen ott minden készen van! Például, var AWS = require('aws-sdk') és nem kell a 'biciklit' feltalálnunk az S3 hitelesítéssel!
Nézzük meg a hátrányokat
Számomra a külső JS modulok importálásának lehetetlensége kellemetlen, de várható jellemzővé vált. Az előző példa alapján a require('crypto'), ez és a require csak ezekre működik. Nincs lehetőség a kód újrahasználására a szkriptekből, így azt különböző fájlokba kell másolni és beilleszteni. Remélem, hogy egyszer megvalósítják ezt a funkciót.
A jelenlegi projekthez az Nginx-ben ki kell kapcsolni a tömörítést gzip off;
Mivel az NJS-ben nincs gzip-modul, és ezt nem lehet csatlakoztatni, ezért nincs lehetőség tömörített adatokkal dolgozni. Igaz, hogy ez nem is igazán hátrány ebben az esetben. Nincs sok szöveg, a továbbított fájlok már tömörítve vannak, és a plusz tömörítés nem segít különösebben. Ez a szolgáltatás nem annyira leterhelt vagy kritikus, hogy foglalkozni kelljen a tartalom pár milliszekundummal gyorsabb kiszolgálásával.
A szkript hibakeresése hosszú és csak az error.log-ban „print”-eken keresztül lehetséges. Az aktív naplózási szinttől függően, mint info, warn vagy error, használhatók az r.log, r.warn, r.error módszerek. Néhány szkriptet a Chrome-ban (v8) vagy az njs konzolos eszközében próbálok hibakeresni, de nem mindent lehet ott ellenőrizni. A kód hibakeresése, azaz a funkcionális tesztelés, körülbelül így néz ki:
docker-compose restart nginx
curl localhost:8080/
docker-compose logs --tail 10 nginxés ilyen sorrendek száma akár több száz is lehet.
A kód írása a szublekciók és az azokhoz beállított változók használatával összegubancolódik. Néha különféle IDE ablakok között kezdesz rohangálni, próbálva megfejteni a kódod cselekedeteinek sorrendjét. Nem nehéz, de néha nagyon idegesítő.
Nincs teljes körű támogatás az ES6-ra.
Lehet, hogy vannak még további hátrányok, de én más problémával nem találkoztam. Oszd meg az információt, ha van negatív tapasztalatod az NJS használatával.
Zárás
Az NJS egy könnyű, nyílt forráskódú értelmező, amely lehetővé teszi, hogy különböző forgatókönyveket valósítsunk meg az Nginx-ben JavaScript nyelven. A fejlesztés során a teljesítményre nagy hangsúlyt fektettek. Persze sok minden hiányzik belőle még, de a projekt egy kis csapat erőfeszítéseivel fejlődik, és aktívan adnak hozzá új funkciókat, valamint javítják a hibákat. Remélem, hogy egyszer az NJS lehetővé teszi külső modulok csatlakoztatását, ami gyakorlatilag korlátlan funkcionalitást ad az Nginx-nek. De van NGINX Plus, és valószínűleg nem lesznek ilyen funkciók!
és
/ Выступление Дмитрия Волныева на Saint HighLoad++ 2019
/ Выступление Василия Сошникова на HighLoad++ 2019
Forrás: habr.com
