I denna artikel vill jag dela med mig av erfarenheter kring NJS, en JavaScript-tolk för Nginx som utvecklas av Nginx Inc, och beskriva dess grundläggande funktioner med hjälp av ett verkligt exempel. NJS är en delmängd av programmeringsspråket JavaScript som möjliggör utökning av Nginx funktionalitet. När man ställer frågan svarar Dmitrij Volyntsev utförligt. I korthet är NJS nginx-way, och JavaScript är mer avancerat, "inhemskt" och utan GC till skillnad från Lua.
För länge sedan…
På mitt förra jobb ärvde jag ett gitlab med ett antal blandade CI/CD-pipelines med docker-compose, dind och andra läckerheter, som hade konverterats till kaniko. Bilderna som användes tidigare i CI flyttades i oförändrat skick. De fungerade felfritt tills den dag då vårt gitlab bytte IP och CI förvandlades till en pumpa. Problemet var att i en av docker-bilderna som deltog i CI, fanns git, som via ssh drog ner Python-moduler. För ssh behövs en privat nyckel och … den var i bilden tillsammans med known_hosts. Och varje CI avslutades med ett fel på nyckelkontrollen på grund av en mismatch mellan det verkliga IP:t och det angivna i known_hosts. Av de befintliga dockfile:arna sammanställdes snabbt en ny bild och alternativet StrictHostKeyChecking no. Men den obehagliga känslan fanns kvar och jag ville överföra biblioteken till ett privat PyPI-repo. En extra fördel var att efter övergången till privat PyPI skulle pipeline bli enklare och en normal beskrivning av requirements.txt.
Valet är gjort, mina herrar!
Vi kör allt i molnet och Kubernetes, och till slut ville vi få en liten tjänst som bestod av en stateless-container med extern lagring. Eftersom vi använder S3 blev det även prioriterat. Och om möjligt med autentisering i gitlab (kan också kodas av oss själva om det behövs).
En snabb sökning gav några resultat som s3pypi, pypicloud och alternativet med "manuell" skapelse av html-filer för repo. Det sista alternativet föll bort av sig själv.
s3pypi: Det är ett CLI för att använda hosting på S3. Vi laddar upp filer, genererar html och laddar upp i samma bucket. Det passar bra för hemmabruk.
pypicloud: Verket verkade intressant men efter att ha läst dokumentationen blev jag besviken. Trots bra dokumentation och möjligheter att anpassa det för egna behov visade det sig vara överdrivet och svårt att konfigurera. Att justera koden för mina behov skulle, enligt dåvarande uppskattningar, ta 3-5 dagar. Tjänsten behöver också en databas. Vi lät det vara kvar ifall vi inte skulle hitta något annat.
En djupare sökning ledde till modulen för Nginx, ngx_aws_auth. Resultatet av dess test blev XML som visades i webbläsaren, där innehållet i S3-bucketen kunde ses. Den senaste committen, vid tidpunkten för sökningen, var för ett år sedan. Repositoriet såg övergivet ut.
Genom att hänvisa till källan och läsa förstod jag att XML kan konverteras till HTML i farten och skickas till pip. Efter att ha googlat lite mer på orden Nginx och S3 snubblade jag över ett exempel på autentisering i S3 skrivet i JS för Nginx. Så lärde jag mig om NJS.
När jag tog detta exempel som grund, efter en timme, såg jag samma XML i min webbläsare som när jag använde modulen ngx_aws_auth, men allt var nu skrivet i JS.
Lösningen på nginx gillade jag verkligen. För det första bra dokumentation och många exempel, för det andra får vi alla fördelar med Nginx för filhantering (utan ytterligare konfiguration), för det tredje kommer vem som helst som kan skriva konfigurationer för Nginx att kunna förstå vad som händer. Minimalism är också en fördel för mig, jämfört med Python eller Go (om man skulle skriva från grunden), för att inte tala om nexus.
TL;DR: Efter 2 dagar var testversionen av PyPi redan använd i CI.
Hur fungerar det?
I Nginx laddas modulen ngx_http_js_module, inkluderad i den officiella docker-bilden. Vi importerar vårt skript med hjälp av direktivet js_importi Nginx-konfigurationen. Anropet av funktionen görs med direktivet js_content. För att ställa in variabler används direktivet js_set, som endast accepterar funktionen som definieras i skriptet som argument. Och vi kan endast utföra delanrop i NJS med hjälp av Nginx, ingen XMLHttpRequest där. För detta måste en motsvarande location läggas till i Nginx-konfigurationen. I skriptet måste en delanrop (subrequest) till denna location beskrivas. För att kunna anropa en funktion från Nginx-konfigurationen måste namnet på funktionen exporteras 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}Vid begäran i webbläsaren http://localhost:8080/ hamnar vi i location /där direktivet js_content anropar funktionen request som beskrivs i vårt skript script.js. I sin tur gör funktionen request ett underfråga till location = /sub-query, med metoden (i det aktuella exemplet GET) som erhålls från argumentet (r), som implicit överförs vid anropet av denna funktion. Bearbetning av svaret från underfrågan kommer att utföras i funktionen call_back.
Vi testar S3
För att göra en begäran till en privat S3-lagring behöver vi:
ACCESS_KEY
SECRET_KEY
S3_BUCKET
Från den använda http-metoden, det aktuella datumet/tiden, S3_NAME och URI genereras en sträng av viss typ som signeras (HMAC_SHA1) med hjälp av SECRET_KEY. Denna sträng, av typen AWS $ACCESS_KEY:$HASH, kan användas i auktoriseringshuvudet. Samma datum/tid som används för att generera strängen i föregående steg måste läggas till i huvudet X-amz-date. I koden ser det ut så här:
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(exempel på AWS Sign v2-auktorisering som har blivit avskriven)
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}Lite förklaring om _subrequest_uri: detta är en variabel som beroende på den ursprungliga uri formar en begäran till S3. Om man vill hämta innehållet i "roten", måste man i sådana fall bilda en uri-begäran med angivelsen av avgränsaren delimiter, som kommer att returnera en lista över alla xml-element CommonPrefixes, vilket motsvarar mappar (i fallet med PyPI, listan över alla paket). Om man vill få en lista över innehållet i en viss mapp (listan över alla versioner av paket), då måste uri-begäran innehålla fältet prefix med mappens (paketets) namn som nödvändigtvis slutar på snedstreck /. Annars kan det uppstå konflikter vid begäran om innehållet i mappen, till exempel. Det finns mappar aiohttp-request och aiohttp-requests och om begäran anges /?prefix=aiohttp-request, då kommer svaret att innehålla innehållet från båda katalogerna. Om det däremot avslutas med ett snedstreck, /?prefix=aiohttp-request/, kommer svaret endast att innehålla den önskade katalogen. Om vi ber om en fil, får den resulterande URI:en inte skilja sig från den ursprungliga.
Vi sparar, startar om Nginx. I webbläsaren skriver vi in adressen till vår Nginx, resultatet av förfrågan kommer att vara XML, till exempel:
Lista över kataloger
myback-space
10000
/
false
new/
old/Från listan över kataloger behöver vi bara elementen CommonPrefixes.
Genom att lägga till den önskade katalogen i vår adress i webbläsaren får vi dess innehåll även som XML:
Lista över filer i katalogen
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
STANDARDFrån listan över filer tar vi endast elementen Nyckel.
Det återstår att parsa den erhållna XML:en och returnera den som HTML, efter att vi har bytt ut Content-Type-headern till text/html.
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>'
}Vi testar PyPI
Vi kontrollerar att ingenting går sönder på kända fungerande paket.
# Создаем для тестов новое окружение
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:8080Vi upprepar med våra bibliotek.
# Создаем для тестов новое окружение
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:8080I CI, ser skapande och uppladdning av paket ut så här:
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}"Autentisering
I GitLab kan JWT användas för autentisering/auktorisering av externa tjänster. Genom att använda direktivet auth_request i Nginx kan vi omdirigera autentiseringsdata till en undersökning som innehåller ett funktionsanrop i skriptet. I skriptet kommer ytterligare en undersökning att göras mot GitLabs URL, och om autentiseringsdata anges korrekt kommer GitLab att returnera kod 200, och uppladdning/nedladdning av paket tillåts. Varför inte använda en undersökning direkt och skicka data till GitLab omedelbart? För då måste vi ändra Nginx-konfigurationsfilen varje gång vi har några ändringar i autentiseringen, och det är en ganska tidskrävande uppgift. Om Kubernetes också använder en policy för read-only root filesystem, ökar det ytterligare komplikationerna vid byte av nginx.conf via configmap. Det blir helt omöjligt att konfigurera Nginx genom configmap vid samtidig användning av policyer som blockerar anslutning av volymer (pvc) och read-only root file system (det kan hända också).
Genom att använda NJS som ett mellanled får vi möjlighet att ändra angivna parametrar i nginx-konfigurationen med hjälp av miljövariabler och göra vissa kontroller i skriptet (till exempel för en felaktigt angiven 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 ~ "^/(?[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}Det är nog en fråga som växer fram: - Varför inte använda färdiga moduler? Där är ju allt redan gjort! Till exempel, var AWS = require('aws-sdk') och man behöver inte skriva 'cykeln' med S3-autentisering!
Låt oss gå vidare till nackdelarna
För mig blev oförmågan att importera externa JS-moduler en obehaglig, men förväntad egenskap. Det som beskrivs i exemplet ovan require('crypto'), är och require fungerar bara för dem. Det finns också ingen möjlighet att återanvända kod från skript och man måste kopiera och klistra in den i olika filer. Jag hoppas att denna funktionalitet kommer att genomföras någon gång.
För det aktuella projektet ska komprimering vara avstängd i Nginx gzip off;
Eftersom det inte finns någon gzip-modul i NJS och det är omöjligt att koppla in den, finns det därmed inget sätt att arbeta med komprimerade data. Men det är inte särskilt negativt för detta fall. Det finns inte mycket text, och de filer som överförs är redan komprimerade, så ytterligare komprimering hjälper inte så mycket. Dessutom är det inte en så belastad eller kritisk tjänst att man behöver oroa sig för att leverera innehåll ett par millisekunder snabbare.
Felsökning av skript är långsam och möjlig endast via "printar" i error.log. Beroende på den inställda loggnivån kan info, warn eller error använda de tre metoderna r.log, r.warn, r.error. Jag försöker felsöka vissa skript i Chrome (v8) eller det konsolverktyg som njs, men allt är inte möjligt att kontrollera där. Vid felsökning av kod, alltså funktionell testning, ser historiken ut ungefär så här:
docker-compose restart nginx
curl localhost:8080/
docker-compose logs --tail 10 nginxoch sådana sekvenser kan vara hundratals.
Att skriva kod med hjälp av underfrågor och variabler för dem blir en invecklad trassel. Ibland börjar man leta igenom olika fönster i IDE:n för att försöka förstå sekvensen av kodens åtgärder. Det är inte svårt, men ibland är det väldigt frustrerande.
Det finns ingen fullständig support för ES6.
Det kan finnas fler nackdelar, men jag har inte stött på något mer. Dela gärna med er av information om ni har negativ erfarenhet av att använda NJS.
Slutsats
NJS är en lättvikts open-source tolk som möjliggör implementering av olika skript i Nginx med JavaScript. Under dess utveckling har stor uppmärksamhet ägnats åt prestanda. Självklart saknas det fortfarande mycket, men projektet utvecklas av ett litet team som aktivt tillför nya funktioner och åtgärdar buggar. Jag hoppas att NJS en dag kommer att tillåta koppling av externa moduler, vilket skulle göra Nginx:s funktionalitet nästan oändlig. Men det finns NGINX Plus, så vissa funktioner kommer troligen inte att finnas!
och
/ Выступление Дмитрия Волныева на Saint HighLoad++ 2019
/ Выступление Василия Сошникова на HighLoad++ 2019
Källa: habr.com
