Selles artiklis tahan jagada oma kogemust NJS-i, Nginxis kasutatava JavaScripti tõlgendaja kohta, mille on välja töötanud Nginx Inc. Kirjeldan, kasutades reaalset näidet, selle peamisi võimalusi. NJS on JavaScripti programmeerimiskeele alamhulk, mis võimaldab laiendada Nginxi funktsionaalsust. Küsimusele vastas detailselt Dmitri Volyncev. Kokkuvõttes: NJS on nginx-way, samas kui JavaScript on rohkem progressiivne, 'kodune' ja ilma GC-eta, erinevalt Luast.
Kaua aega tagasi…
Eelmisel töökohal jäi mulle pärandiks gitlab, kus oli mitu erinevat CI/CD-pipe, docker-compose'i, dind'i ja muid ilu, mis viidi kaniko rööbastele. Varasemalt kasutusel olnud pildid kolisid algses vormis. Need töötasid hästi seni, kuni meie gitlabi IP muutus ja CI muutus kõrvitsaks. Probleemiks oli see, et ühes docker-pildis, mis osales CI-s, oli git, mis tõmbas Python'i mooduleid ssh kaudu. SSH jaoks on vajalik privaatvõti ja ... see oli pildis koos known_hosts'iga. Iga CI lõppes võti kontrolli vea tõttu, kuna reaalne IP ei vastanud known_hosts'is märgitud IP-le. Olemasolevatest Dockfile'idest koostati kiiresti uus pilt ja lisati valik StrictHostKeyChecking no. Kuid ebameeldiv maitse jäi ja tekkis soov viia lood privaatsesse PyPI-repositooriumisse. Täiendavaks boonuseks, pärast üleminekut privaatsesse PyPI-sse, oli lihtsam pipe ja normaalsem requirements.txt kirjeldus.
Valik on tehtud, härrad!
Me keerutame kõike pilvedes ja Kuberneteses ning lõpuks soovisime saada väikese teenuse, mis esindaks stateless-konteinerit välise salvestusega. Kuna kasutame S3, oli see ka meie prioriteet. Ja võimalusel gitlabi autentimisega (vajadusel saab ise lisada).
Kiire otsing andis mitu tulemust: s3pypi, pypicloud ja variant 'käsitsi' html-failide loomine repo jaoks. Viimane variant kukkus ise välja.
s3pypi: See on cli S3 hosting'i kasutamiseks. Laadime faile, genereerime html'i ja laadime sama ämber. Koduseks kasutamiseks sobib.
pypicloud: Tundus olevat huvitav projekt, kuid pärast dokumentatsiooni lugemist tuli pettumus. Vaatamata heale dokumentatsioonile ja võimalustele oma vajadustele vastavaks kohandamiseks, oli see tegelikult liiga keeruline ja seadistamine osutus raskeks. Koodi oma vajadustele kohandamine, toona tehtud hinnangute kohaselt, võtaks 3–5 päeva. Samuti vajab teenus andmebaasi. Jätame selle alles juhuks, kui me ei leia midagi paremat.
Süvitsi minev otsing andis Nginxile mõeldud mooduli, ngx_aws_auth. Selle testimise tulemusena saadi XML, mis kuvati brauseris, kust oli näha S3 ämbri sisu. Viimane commit otsimise hetkel oli aasta tagasi. Reposiitrium nägi välja hüljatud.
Pöördudes esialgse allika poole ja lugedes , mõistsin, et XML-i saab jooksvalt HTML-iks konverteerida ja edastada seda pip. Veel veidi googeldades Nginxi ja S3 kohta, leidsin JS-is kirjutatud S3 autentimise näite Nginxile. Nii tutvusin NJS-iga.
Võttes selle näite aluseks, sain tunni aja pärast oma brauseris sama XML-i, nagu ngx_aws_auth mooduli kasutamisel, kuid kõik oli kirjutatud JS-is.
Nginxile põhinev lahendus meeldis mulle väga. Esiteks, suurepärane dokumentatsioon ja arvukalt näiteid; teiseks saame kõik Nginxi eelised failide töötlemisel (karbis); kolmandaks suudab igaüks, kes oskab Nginxi konfigureerida, aru saada, mis ja kuidas. Samuti on mulle plussiks minimalism võrreldes Pythoniga või Go'ga (kui kirjutada nullist), rääkimata nexusest.
TL;DR: Kahe päeva pärast oli PyPi testversioon juba CI-s kasutusele võetud.
Kuidas see töötab?
Nginxisse laaditakse moodul ngx_http_js_module, mis on kaasatud ametlikku docker-pilti. Impordime oma skripti direktiivi abil js_import, Nginxi konfiguratsiooni. Funktsiooni kutsumine toimub direktiivi abil js_content. Muutujate seadmiseks kasutatakse direktiivi js_set, mille argumendiks on ainult skriptis kirjeldatud funktsioon. Aga alameetmeid NJS-is saame teha ainult Nginxi kaudu, mitte mingit XMLHttppRequesti. Selleks peab Nginxi konfiguratsiooni lisama vastava asukoha. Ja skriptis peab olema kirjeldatud selle asukoha alameetme (subrequest) üleskutse. Et saaksime pöörduda konfi funktsiooni poole, peab skripti nimeline funktsioon olema eksporditud 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}Brauseri päringu tegemisel http://localhost:8080/ me satume asukohta /kus käsk js_content kutsub üles funktsiooni request mis on meie skriptis kirjeldatud script.js. Omakorda teostatakse funktsioonis request alamkutsest asukoht = /sub-query, meetodiga (antud näites GET), mis on saadud argumendist (r), mis edastatakse kaudselt selle funktsiooni kutse ajal. Alamkutsu vastuse töötlemine toimub funktsioonis call_back.
Proovitame S3
Privaatse S3-salvestuse päringu tegemiseks vajame:
ACCESS_KEY
SECRET_KEY
S3_BUCKET
Kasutatavast http-meetodist, praegusest kuupäevast/kellast, S3_NAME ja URI genereeritakse teatud moodi string, mis allkirjastatakse (HMAC_SHA1) kasutades SECRET_KEY. Seejärel stringi, mis on kujul AWS $ACCESS_KEY:$HASH, saab kasutada autoriseerimise päises. Sama kuupäev/kellaaeg, mida kasutati stringi genereerimiseks eelmises etapis, peab olema lisatud päisesse X-amz-date. Koodis näeb see välja nii:
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 autoriseerimise näide, muudetud deprecated staatuseks)
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}Veidi selgitust _subrequest_uri: see on muutuja, mis sõltuvalt algsest uri-st genereerib päringu S3-le. Kui on vaja saada "juur" sisu, peaks sellisel juhul olema uri-päring koostatud eraldajaga delimiter, mis tagastab kõigi xml-elementide CommonPrefixes nimekirja, mis vastab kataloogidele (PyPI puhul, nimekirja kõigist pakettidest). Kui on vaja saada sisu loendit teatud kataloogis (kõigi pakettide versioonide nimekiri), peab uri-päring sisaldama prefix välja, mille nimi on katalooge (paketti) lõpetatud kaldkriipsuga /. Vastasel juhul võivad tekkida konfliktid, kui pärida katalooge sisu, näiteks. On kataloogid aiohttp-request ja aiohttp-requests ning kui päringus on need märgitud. /?prefix=aiohttp-request, siis vastuses on kummagi katalooge sisu. Kui aga lõpus on kaldkriips, /?prefix=aiohttp-request/, on vastuses ainult vajalik kataloog. Ja kui me küsime faili, ei tohi tulemuseks olev uri erineda algsest.
Salvestame, taaskäivitame Nginx'i. Brauseris sisestame meie Nginx'i aadressi, päringu tulemus on XML, näiteks:
Kataloogide nimekiri
myback-space
10000
/
false
new/
old/Kataloogide loendist on vaja ainult elemente CommonPrefixes.
Lisades brauserisse meie aadressile vajaliku katalooge, saame selle sisu samuti XML kujul:
Failide nimekiri kataloogis
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
STANDARDFailide nimekiri võtame ainult elemendid Ava.
Jääb üle saadud XML analüüsida ja edastada HTML kujul, eelnevalt asendades Content-Type'i päise text/html'ga.
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>'
}Proovime PyPI
Kontrollime, et kusagil ei juhtuks midagi katki teadaolevalt töökindlate paketidega.
# Создаем для тестов новое окружение
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:8080Kordame koos meie teekidega.
# Создаем для тестов новое окружение
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:8080CI-s näeb paketi loomine ja üleslaadimine välja järgmine:
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}"Ahnikeerimine
Gitlabis on võimalik kasutada JWT-d väliste teenuste autentimiseks/autoriseerimiseks. Kasutades Nginxis direktiivi auth_request, suuname autentimisandmed alamküsitlusele, mis sisaldab funktsiooni kutset skriptis. Skriptis tehakse veel üks alamküsitlus Gitlabi URL-ile ja kui autentimisandmed on õiged, tagastab Gitlab koodi 200 ning allalaadimine/laadimine on lubatud. Miks mitte kasutada ühte alamküsitlust ja saata andmed otse Gitlabi? Sest siis tuleb igal korral, kui meil on autentimisega mingeid muudatusi, muuta Nginxi konfiguratsioonifaili, mis on piisavalt vaevaline tegevus. Kui Kuberneteses on kasutusel read-only root file system'i poliitika, lisab see veelgi rohkem raskusi nginx.conf asendamisel configmap'i kaudu. Ja Nginxi konfigureerimine configmap'i kaudu muutub täiesti võimatuks, kui samal ajal kasutatakse mahupoliitikaid, mis keelavad mahu ühendamise (pvc) ja read-only root file system'i (see juhtub ka).
Kasutanud NJS vaheüksusena, saame muuta nginx konfiguratsioonis näidatud parameetreid keskkonnamuutujate kaudu ja teha skriptis mingeid kontrollmisse (näiteks vale URL).
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 ~ "^/(?P[w-]*)[\/]?(?P[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}Tõenäoliselt tekib küsimus: - Miks mitte kasutada valmis mooduleid? Seal on ju kõik juba tehtud! Näiteks var AWS = require('aws-sdk') ja ei pea kirjutama "ratast" S3-autentimisega!
Liigume miinuste juurde
Minu jaoks sai välistest JS-moodulitest impordi puudumine ebameeldivaks, kuid oodatud omaduseks. Ülaltoodud näites kirjeldatud require('crypto') on ja require töötab ainult nende jaoks. Samuti pole võimalik kasutada koodi skriptidest uuesti, mistõttu tuleb seda erinevatesse failidesse kopeerida ja kleepida. Loodan, et kunagi see funktsionaalsus rakendatakse.
Samuti peab Nginxis olema praeguses projektis kokkusurumine välja lülitatud gzip off;
Kuna NJS-is ei ole gzip-moodulit ja selle lisamine on võimatu, ei ole seega ka võimalik töötada kokku surutud andmetega. Tõsi, see pole ka selle juhtumi jaoks suur miinus. Teksti pole palju, ja edastatavad failid on juba kokkusurutud, seega lisasurumine ei aita oluliselt. Samuti ei ole see nii koormatud ega kriitiline teenus, et muretseda sisu edastamise üle mõne millisekundi kiiremini.
Skripti tõrkeotsing on pikk ja seda saab teha ainult error.log-is „printide” kaudu. Sõltuvalt seadistatud logimisastmest info, warn või error võib kasutada vastavalt 3 meetodit r.log, r.warn, r.error. Mõningaid skripte proovin tõrkeotsida Chromes (v8) või konsolivahendi njs kaudu, kuid mitte kõike ei saa seal kontrollida. Koodi tõrkeotsingu, st funktsionaalsete testide ajal näeb ajalugu välja umbes nii:
docker-compose restart nginx
curl localhost:8080/
docker-compose logs --tail 10 nginxja selliseid järjestusi võib olla sadu.
Koodi kirjutamine alamskeemide ja nende muutujate kasutamisega muutub segaseks alusmudeliks. Vahel hakkad hüppama mööda erinevaid IDE aknaid, püüdes mõista oma koodi tegevuste järjekorda. See pole keeruline, aga mõnikord on see väga närviline.
Täielikku ES6 toetust ei ole.
Võib-olla on veel muid puudusi, kuid ma ei ole millegagi enam kokku puutunud. Jagage teavet, kui teil on NJS-i kasutamisel negatiivne kogemus.
Kokkuvõte
NJS on kerge avatud koodiga tõlgendaja, mis võimaldab Nginxis rakendada erinevaid stsenaariume JavaScripti programmeerimiskeeles. Selle arendamisel pöörati suurt tähelepanu jõudlusele. Loomulikult pole selles veel palju puuduvat, kuid projekt areneb väikese meeskonna jõududega ja nad lisavad aktiivselt uusi funktsioone ning parandavad tõrkeid. Ma loodan, et kunagi võimaldab NJS välist moodulite ühendamist, mis muudab Nginxi funktsionaalsuse praktiliselt piiramatu. Kuid olemas on NGINX Plus ja mingit funktsionaalsust tõenäoliselt ei tule!
ja
/ Выступление Дмитрия Волныева на Saint HighLoad++ 2019
/ Выступление Василия Сошникова на HighLoad++ 2019
Allikas: habr.com
