In dit artikel wil ik mijn ervaring delen met NJS, de JavaScript-interpreter voor Nginx ontwikkeld door Nginx Inc., door de belangrijkste mogelijkheden aan de hand van een echt voorbeeld te beschrijven. NJS is een subset van de programmeertaal JavaScript, waarmee de functionaliteit van Nginx kan worden uitgebreid. Op de vraag heeft Dmitry Volyntsev uitgebreid geantwoord. In het kort: NJS is de nginx-way, terwijl JavaScript progressiever, 'natuurlijker' en zonder GC is in vergelijking met Lua.
Lang geleden…
Op mijn vorige werk kreeg ik een gitlab met een aantal diverse CI/CD-pijplijnen met docker-compose, dind en andere schatten, die waren overgezet naar kaniko. De afbeeldingen die eerder in CI werden gebruikt, verhuisden in hun oorspronkelijke staat. Ze functioneerden prima totdat de IP van onze gitlab veranderde en CI in een pompoen veranderde. Het probleem was dat een van de docker-afbeeldingen die deelnam aan CI, git bevatte dat via ssh Python-modules ophaalde. Voor ssh is een privé-sleutel nodig en… deze was in de afbeelding samen met known_hosts. En elke CI eindigde met een foutmelding bij de sleutelcheck door een mismatch tussen het echte IP en dat in known_hosts. Vanuit de beschikbare Dockfile's werd snel een nieuwe afbeelding samengesteld en de optie StrictHostKeyChecking no. Maar de onaangename nasmaak bleef en er ontstond de wens om de libraries naar een privé PyPI-repository te verplaatsen. Een extra voordeel was dat na de overgang naar een privé PyPI de pijplijn eenvoudiger werd en er een normale beschrijving van requirements.txt kwam.
De keuze is gemaakt, heren!
We draaien alles in de cloud en Kubernetes en uiteindelijk wilden we een kleine service die bestond uit een stateless-container met externe opslag. Aangezien we S3 gebruiken, had dat de prioriteit. Idealiter met authenticatie in gitlab (wat je zelf kunt toevoegen indien nodig).
Een vluchtige zoektocht leverde enkele resultaten op: s3pypi, pypicloud en de optie om 'handmatig' html-bestanden voor de repository aan te maken. De laatste optie viel vanzelf af.
s3pypi: Dit is een cli voor het gebruik van hosting op S3. We uploaden bestanden, genereren html en uploaden het naar dezelfde bucket. Voor thuisgebruik is het geschikt.
pypicloud: Het leek een interessant project, maar na het lezen van de documentatie was er teleurstelling. Ondanks de goede documentatie en de uitbreidingsmogelijkheden bleek het in de praktijk overbodig en moeilijk in te stellen. Het aanpassen van de code voor mijn behoeften zou, volgens eerdere schattingen, 3-5 dagen duren. Ook heeft de service een database nodig. We hebben het laten staan voor het geval we verder niets kunnen vinden.
Een diepgaandere zoektocht leverde een module voor Nginx op, ngx_aws_auth. Het resultaat van de tests was een XML die in de browser werd weergegeven, waaruit de inhoud van de S3-bucket zichtbaar was. De laatste commit was, op het moment van zoeken, een jaar geleden. De repository leek verlaten.
Door naar de originele bron te verwijzen en te lezen snapte ik dat XML on the fly kan worden omgezet naar HTML en aan pip kan worden geleverd. Na nog wat te hebben gegoogeld over de woorden Nginx en S3 kwam ik een voorbeeld tegen van authenticatie in S3 geschreven in JS voor Nginx. Zo maakte ik kennis met NJS.
Dit voorbeeld als basis nemend, zag ik na een uur in mijn browser dezelfde XML, die ook bij het gebruik van de module ngx_aws_auth werd weergegeven, maar nu alles was geschreven in JS.
De oplossing op Nginx beviel me erg goed. Ten eerste is er goede documentatie en tal van voorbeelden, ten tweede krijgen we alle voordelen van Nginx voor het werken met bestanden (uit de doos), en ten derde kan iedereen die config-bestanden voor Nginx kan schrijven begrijpen wat wat is. Een bijkomend voordeel voor mij is het minimalisme, in vergelijking met Python of Go (als je vanaf nul schrijft), om nog maar te zwijgen over nexus.
TL;DR Binnen 2 dagen was de testversie van PyPi al gebruikt in CI.
How does it work?
In Nginx wordt de module geladen ngx_http_js_module, opgenomen in de officiële Docker-image. We importeren ons script met behulp van de directief js_importin de configuratie van Nginx. De functie wordt aangeroepen met de directief js_content. Voor het instellen van variabelen gebruiken we de directief js_set, die als argument alleen de functie accepteert die in het script is beschreven. Subverzoeken in NJS kunnen we echter alleen uitvoeren met Nginx, geen XMLHTTPRequest daar. Hiervoor moet er in de Nginx-configuratie een overeenkomstige locatie worden toegevoegd. In het script moet een subverzoek (subrequest) naar deze locatie worden beschreven. Om toegang te krijgen tot de functie vanuit de Nginx-config, moet de naam van de functie in het script worden geëxporteerd. 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}Bij een aanvraag in de browser http://localhost:8080/ komen we terecht in location /waar de directive js_content een functie aanroept request beschreven in ons script script.js. Op zijn beurt wordt in de functie request een subaanroep gedaan naar location = /sub-query, met de methode (in dit voorbeeld GET) verkregen uit de parameter (r), die impliciet wordt meegegeven bij het aanroepen van deze functie. De verwerking van het antwoord op de subaanroep zal plaatsvinden in de functie call_back.
Proberen S3
Om een aanvraag te doen aan een privé S3-opslag, hebben we nodig:
ACCESS_KEY
SECRET_KEY
S3_BUCKET
Uit de gebruikte http-methode, de huidige datum/tijd, S3_NAME en URI wordt een bepaald soort string gegenereerd, die wordt ondertekend (HMAC_SHA1) met behulp van SECRET_KEY. Vervolgens kan de string, van de vorm AWS $ACCESS_KEY:$HASH, worden gebruikt in de autorisatieheader. Dezelfde datum/tijd die is gebruikt voor het genereren van de string in de vorige stap, moet aan de header worden toegevoegd X-amz-date. In de code ziet dit er als volgt uit:
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(voorbeeld van AWS Sign v2 autorisatie, is in status deprecated omgezet)
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}Een beetje uitleg over _subrequest_uri: dit is een variabele die afhankelijk van de oorspronkelijke uri een verzoek naar S3 vormt. Als je de inhoud van de "wortel" wilt krijgen, moet je een uri-aanroep vormen met de aanduiding van de scheidingsteken delimiter, die een lijst van alle xml-elementen CommonPrefixes retourneert, wat overeenkomt met de mappen (in het geval van PyPI, een lijst van alle pakketten). Als je een lijst van inhoud in een bepaalde map wilt krijgen (lijst van alle versies van pakketten), dan moet de uri-aanroep het veld prefix bevatten met de naam van de map (pakket) dat altijd eindigt met een schuine streep /. Anders zijn er mogelijk botsingen bij het aanvragen van de inhoud van een map, bijvoorbeeld. Er zijn mappen aiohttp-request en aiohttp-requests, en als in de aanvraag wordt aangegeven /?prefix=aiohttp-request, dan is de inhoud van beide mappen in het antwoord te zien. Als er echter een schuine streep aan het einde is, /?prefix=aiohttp-request/, dan het antwoord zal alleen de benodigde directory bevatten. En als we een bestand aanvragen, mag de resulterende uri niet verschillen van de oorspronkelijke.
We slaan op, herstarten Nginx. Typ het adres van onze Nginx in de browser, het resultaat van de aanvraag zal XML zijn, bijvoorbeeld:
Lijst van mappen
myback-space
10000
/
false
new/
old/Van de lijst met mappen zijn alleen de elementen nodig CommonPrefixes.
Door in de browser het benodigde pad aan ons adres toe te voegen, krijgen we de inhoud ook in de vorm van XML:
Lijst van bestanden in de map
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
STANDARDVan de lijst met bestanden nemen we alleen de elementen Key.
Het blijft over om de verkregen XML te parseren en deze als HTML te geven, waarbij we de Content-Type header vooraf vervangen door 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>'
}We proberen PyPI
We controleren of er niets kapot gaat bij de zeker werkende pakketten.
# Создаем для тестов новое окружение
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:8080We herhalen met onze libraries.
# Создаем для тестов новое окружение
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:8080In CI ziet de creatie en upload van een pakket er als volgt uit:
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}"Authenticatie
In Gitlab is het mogelijk om JWT te gebruiken voor authenticatie/autorisatie van externe diensten. Door gebruik te maken van de auth_request-directive in Nginx, sturen we de authenticatiegegevens door in een subverzoek dat de functie in het script aanroept. In het script zal er nog een subverzoek naar de Gitlab-URL worden gedaan, en als de authenticatiegegevens correct zijn opgegeven, zal Gitlab een 200-code teruggeven en zal het uploaden/downloaden van het pakket worden toegestaan. Waarom zouden we niet gewoon één subverzoek gebruiken en de gegevens direct naar Gitlab sturen? Omdat we dan het Nginx-configuratiebestand elke keer moeten aanpassen als er wijzigingen in de autorisaties zijn, wat een behoorlijk tijdrovende klus is. Daarnaast, als in Kubernetes er een read-only root filesystem beleid wordt gebruikt, voegt dit nog meer complicaties toe bij het vervangen van nginx.conf via configmap. Het wordt ook absoluut onmogelijk om Nginx te configureren via configmap als er tegelijkertijd beleid is dat het aansluiten van volumes (pvc) en een read-only root filesystem verbiedt (dit gebeurt ook soms).
Door gebruik te maken van NJS als tussenlaag, krijgen we de mogelijkheid om de aangegeven parameters in de Nginx-configuratie te wijzigen met behulp van omgevingsvariabelen en om enkele controles in het script uit te voeren (bijvoorbeeld, een onjuist opgegeven 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}De vraag zal waarschijnlijk opkomen: - Waarom geen gebruik maken van kant-en-klare modules? Daar is alles al gedaan! Bijvoorbeeld, var AWS = require('aws-sdk') en je hoeft niet zelf de 'fiets' te maken met S3-authenticatie!
Laten we overgaan naar de nadelen
Voor mij is de onmogelijkheid om externe JS-modules te importeren een vervelende, maar verwachte eigenschap geworden. De require('crypto') zoals beschreven in het voorbeeld hierboven is en require werkt alleen voor hen. Daarnaast is er geen mogelijkheid om code uit scripts te hergebruiken en moet ik het kopiëren en plakken in verschillende bestanden. Ik hoop dat deze functionaliteit op een dag zal worden geïmplementeerd.
Voor het huidige project moet compressie in Nginx zijn uitgeschakeld. gzip off;
Omdat er geen gzip-module in NJS is en het niet mogelijk is om deze aan te sluiten, is er dus geen mogelijkheid om met gecomprimeerde gegevens te werken. Dit is echter niet echt een nadeel voor deze case. Er is niet veel tekst en de verzonden bestanden zijn al gecomprimeerd, dus verdere compressie zal niet veel helpen. Daarnaast is dit geen bijzonder druk of kritisch systeem, dus er is geen noodzaak om je zorgen te maken over het sneller afleveren van content met enkele milliseconden.
Debugging van de script is tijdrovend en kan alleen via 'prints' in error.log. Afhankelijk van het ingestelde logniveau info, warn of error kunnen de methodes r.log, r.warn en r.error respectievelijk worden gebruikt. Sommige scripts probeer ik te debuggen in Chrome (v8) of met de consoletool njs, maar niet alles kan daar worden gecontroleerd. Bij het debuggen van de code, ook wel functionele testen, ziet de geschiedenis er ongeveer zo uit:
docker-compose restart nginx
curl localhost:8080/
docker-compose logs --tail 10 nginxen dergelijke volgordes kunnen honderden keren voorkomen.
Het schrijven van code met behulp van subquery's en variabelen voor hen verandert in een verwarde kluwen. Soms ga je van het ene IDE-venster naar het andere terwijl je probeert te begrijpen wat de volgorde van acties in je code is. Het is niet moeilijk, maar soms kan het behoorlijk frustrerend zijn.
Er is geen volledige ondersteuning voor ES6.
Misschien zijn er nog andere tekortkomingen, maar daar ben ik niet mee geconfronteerd. Deel je informatie als je een negatieve ervaring hebt met NJS.
Conclusie
NJS is een lichtgewicht open-source interpreter die het mogelijk maakt om verschillende scripts in Nginx uit te voeren met de programmeertaal JavaScript. Bij de ontwikkeling werd veel aandacht besteed aan prestaties. Natuurlijk mist het nog veel functionaliteiten, maar het project wordt ontwikkeld door een klein team dat actief nieuwe functies toevoegt en bugs verhelpt. Ik hoop dat NJS op een dag het mogelijk maakt om externe modules aan te sluiten, zodat de functionaliteit van Nginx vrijwel onbeperkt wordt. Maar er is NGINX Plus en bepaalde functies zullen waarschijnlijk niet beschikbaar zijn!
en
/ Выступление Дмитрия Волныева на Saint HighLoad++ 2019
/ Выступление Василия Сошникова на HighLoad++ 2019
Bron: habr.com
