In diesem Artikel möchte ich meine Erfahrungen mit NJS teilen, dem JavaScript-Interpreter für Nginx, der von Nginx Inc. entwickelt wird, und dabei an einem realen Beispiel seine Hauptfunktionen beschreiben. NJS ist eine Teilmenge der Programmiersprache JavaScript, die die Funktionalität von Nginx erweitert. Auf die Frage hat Dmitry Volyncev ausführlich geantwortet. Kurz gesagt: NJS ist der nginx-way, während JavaScript fortschrittlicher, ‚nativ‘ und ohne GC ist, im Gegensatz zu Lua.
Vor langer Zeit…
Bei meiner letzten Anstellung erhielt ich ein ererbtes GitLab mit einer Vielzahl verschiedenartiger CI/CD-Pipelines, die Docker-Compose, dind und andere Annehmlichkeiten beinhalteten, die auf Kaniko umgestellt wurden. Die Bilder, die zuvor im CI verwendet wurden, verblieben in ihrem ursprünglichen Zustand. Sie funktionierten einwandfrei, bis unser GitLab eine neue IP bekam und CI in eine Kürbissituation umschlug. Das Problem war, dass eines der Docker-Images, das am CI beteiligt war, git enthielt, das Python-Module über SSH herunterlud. Für SSH wird ein privater Schlüssel benötigt, und … dieser war im Image zusammen mit known_hosts vorhanden. Jeder CI-Prozess endete mit einem Fehler bei der Schlüsselprüfung aufgrund der Diskrepanz zwischen der realen IP und der in known_hosts angegebenen. Aus den vorhandenen Dockfiles wurde schnell ein neues Image gebaut und eine Option hinzugefügt. StrictHostKeyChecking no. Aber der unangenehme Nachgeschmack bleibt, und der Wunsch, die Bibliotheken in ein privates PyPI-Repository zu verschieben, ist entstanden. Ein zusätzlicher Vorteil war, dass nach dem Übergang zu einem privaten PyPI der Pipeline einfacher wurde und eine normale Beschreibung der requirements.txt verfügbar war.
Die Entscheidung ist gefallen, meine Damen und Herren!
Wir arbeiten alles in der Cloud und Kubernetes und letztendlich wollten wir einen kleinen Dienst, der einen stateless Container mit einem externen Speicher repräsentierte. Da wir S3 verwenden, hatte dies auch Priorität. Außerdem war eine Authentifizierung in GitLab wünschenswert (kann bei Bedarf selbst hinzugefügt werden).
Eine schnelle Suche ergab einige Ergebnisse: s3pypi, pypicloud und eine Variante mit „manuellem“ Erstellen von HTML-Dateien für das Repository. Letztere Option fiel sofort weg.
s3pypi: Dies ist ein CLI zum Verwenden von S3-Hosting. Wir laden Dateien hoch, erzeugen HTML und laden es in dasselbe Bucket. Es ist für den Heimgebrauch geeignet.
pypicloud: Es schien ein interessantes Projekt zu sein, aber nach dem Lesen der Dokumentation war ich enttäuscht. Trotz guter Dokumentation und Erweiterungsmöglichkeiten für meine Anforderungen stellte sich heraus, dass es zu umfangreich und kompliziert in der Einrichtung war. Den Code an meine Bedürfnisse anzupassen würde, nach damaligen Schätzungen, 3-5 Tage in Anspruch nehmen. Außerdem benötigt der Dienst eine Datenbank. Wir haben es für den Fall behalten, dass wir nichts Besseres finden.
Eine intensivere Suche ergab ein Modul für Nginx, ngx_aws_auth. Das Ergebnis seiner Tests war XML, das im Browser angezeigt wurde, wodurch der Inhalt des S3-Buckets sichtbar wurde. Der letzte Commit war zum Zeitpunkt der Suche ein Jahr her. Das Repository wirkte verlassen.
Nachdem ich die Quelle konsultiert und gelesen hatte, verstand ich, dass XML zur Laufzeit in HTML konvertiert und an pip übergeben werden kann. Nach ein wenig weiterer Google-Suche zu den Begriffen Nginx und S3 stieß ich auf ein Beispiel für die Authentifizierung in S3, das in JS für Nginx geschrieben wurde. So lernte ich NJS kennen.
Mit diesem Beispiel als Grundlage beobachtete ich nach einer Stunde im Browser dasselbe XML, das ich auch mit dem Modul ngx_aws_auth genutzt hatte, aber jetzt war alles in JS geschrieben.
Die Lösung auf nginx hat mir sehr gefallen. Erstens gibt es eine gute Dokumentation und viele Beispiele, zweitens erhalten wir alle Vorteile von Nginx für die Arbeit mit Dateien (out of the box), drittens kann jeder, der Konfigurationen für Nginx schreiben kann, leicht verstehen, wie alles funktioniert. Auch der Minimalismus im Vergleich zu Python oder Go (wenn man von Grund auf neu schreibt) ist ein Plus für mich, ganz zu schweigen von nexus.
TL;DR Nach 2 Tagen war die Testversion von PyPi bereits im CI eingesetzt.
Wie funktioniert das?
In Nginx wird das Modul ngx_http_js_module, das im offiziellen Docker-Image enthalten ist, geladen. Wir importieren unser Skript mit der Anweisung js_importin die Nginx-Konfiguration. Der Aufruf der Funktion erfolgt mit der Anweisung js_content. Variablen werden mit der Anweisung js_set, die als Argument nur eine im Skript beschriebene Funktion akzeptiert. Subanfragen in NJS können wir jedoch nur mit Hilfe von Nginx durchführen, dort gibt es keinen XMLHttpRequest. Dafür muss in der Nginx-Konfiguration der entsprechende Location-Block hinzugefügt werden. Im Skript muss eine Subanfrage (subrequest) zu diesem Location-Block beschrieben werden. Um auf eine Funktion aus der Nginx-Konfiguration zugreifen zu können, muss der Funktionsname im Skript exportiert werden. export default.
nginx.conf
lade_modul modules/ngx_http_js_module.so;
http {
js_import imported_name von script.js;
server {
höre 8080;
...
location = /sub-query {
intern;
proxy_pass http://upstream;
}
location / {
js_content imported_name.request;
}
}script.js
funktion request(r) {
funktion call_back(resp) {
// Code des Handlers
r.return(resp.status, resp.responseBody);
}
r.subrequest('/sub-query', { method: r.method }, call_back);
}
export default {request}Bei der Anfrage im Browser http://localhost:8080/ landen wir in location /in dem die Direktive js_content eine Funktion aufruft request in unserem Skript beschrieben ist script.js. In der Funktion request wird eine Subanfrage an location = /sub-query, mit der Methode (im aktuellen Beispiel GET), die aus dem Argument (r), implizit übergeben beim Aufruf dieser Funktion. Die Verarbeitung der Antwort der Subanfrage erfolgt in der Funktion call_back.
Versuchen wir S3
Um eine Anfrage an ein privates S3-Speicher zu stellen, benötigen wir:
ACCESS_KEY
SECRET_KEY
S3_BUCKET
Aus der verwendeten http-Methode, dem aktuellen Datum/Uhrzeit, S3_NAME und URI wird eine bestimmte Art von String generiert, die mit SECRET_KEY signiert wird (HMAC_SHA1). Der String sieht dann so aus, AWS $ACCESS_KEY:$HASH, kann im Autorisierungsheader verwendet werden. Dasselbe Datum/Uhrzeit, das zur Generierung des Strings im vorherigen Schritt verwendet wurde, muss im Header hinzugefügt werden X-amz-date. Im Code sieht das so aus:
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(Beispiel für AWS Sign v2-Authentifizierung, in den Status „veraltet“ übersetzt)
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}Ein wenig Erklärung zu _subrequest_uri: Dies ist eine Variable, die abhängig von der ursprünglichen URI eine Anfrage an S3 bildet. Um den Inhalt des „Wurzelverzeichnisses“ zu erhalten, muss eine URI-Anfrage mit einem Trennzeichen gebildet werden delimiter, das eine Liste aller XML-Elemente CommonPrefixes zurückgibt, die den Verzeichnissen entsprechen (im Falle von PyPI, die Liste aller Pakete). Um eine Liste des Inhalts in einem bestimmten Verzeichnis (Liste aller Paketversionen) zu erhalten, muss die URI-Anfrage das Feld prefix mit dem Namen des Verzeichnisses (Pakets) enthalten, das unbedingt mit einem Schrägstrich \/ endet. Andernfalls können bei der Anfrage nach Verzeichnisinhalten Kollisionen auftreten, z. B. gibt es die Verzeichnisse aiohttp-request und aiohttp-requests, und wenn in der Anfrage angegeben wird /?prefix=aiohttp-request, dann wird die Antwort den Inhalt beider Verzeichnisse anzeigen. Wenn jedoch ein Slash am Ende steht, /?prefix=aiohttp-request/, wird die Antwort nur das gewünschte Verzeichnis enthalten. Und wenn wir eine Datei anfordern, sollte die resultierende URI nicht von der ursprünglichen abweichen.
Wir speichern, starten Nginx neu. Im Browser geben wir die Adresse unseres Nginx ein, das Ergebnis der Anfrage wird XML sein, zum Beispiel:
Liste der Verzeichnisse
myback-space
10000
/
false
new/
old/Aus der Liste der Verzeichnisse benötigen wir nur die Elemente CommonPrefixes.
Indem wir im Browser unser gewünschtes Verzeichnis zu unserer Adresse hinzufügen, erhalten wir dessen Inhalt ebenfalls im XML-Format:
Liste der Dateien im Verzeichnis
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
STANDARDWir nehmen aus der Liste der Dateien nur die Elemente. Schlüssel.
Jetzt bleibt nur noch der XML-Inhalt zu parsen und als HTML auszugeben, nachdem der Content-Type-Header auf text/html geändert wurde.
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>'
}Wir probieren PyPI aus.
Wir überprüfen, dass nichts kaputtgeht bei offensichtlich funktionierenden Paketen.
# Создаем для тестов новое окружение
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:8080Wir wiederholen dies mit unseren Bibliotheken.
# Создаем для тестов новое окружение
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:8080Im CI sieht die Erstellung und der Upload eines Pakets so aus:
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}"Authentifizierung
In Gitlab ist es möglich, JWT für die Authentifizierung/Autorisierung externer Dienste zu nutzen. Mit der Direktive auth_request in Nginx leiten wir die Authentifizierungsdaten in eine Teilanfrage weiter, die einen Funktionsaufruf im Skript enthält. Im Skript wird ein weiterer Unteranruf an die URL von Gitlab durchgeführt, und wenn die Authentifizierungsdaten korrekt angegeben wurden, gibt Gitlab den Code 200 zurück und der Download des Pakets wird erlaubt. Warum nicht einfach einen Unteranruf verwenden und die Daten direkt an Gitlab senden? Weil wir dann die Nginx-Konfigurationsdatei jedes Mal anpassen müssten, wenn wir Änderungen an der Authentifizierung vornehmen, und das ist ein recht mühsames Vorhaben. Wenn in Kubernetes außerdem eine Richtlinie für ein read-only root filesystem verwendet wird, wird es noch komplizierter, die nginx.conf über ein configmap zu ersetzen. Eine Konfiguration von Nginx über ein configmap wird auch völlig unmöglich, wenn gleichzeitig Richtlinien existieren, die das Anbinden von Volumes (pvc) und ein read-only root filesystem verbieten (solche Fälle gibt es ebenfalls).
Mit dem Zwischenlink NJS erhalten wir die Möglichkeit, die angegebenen Parameter in der nginx-Konfiguration über Umgebungsvariablen zu ändern und verschiedene Überprüfungen im Skript durchzuführen (z. B. eine falsche 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}Wahrscheinlich stellt sich die Frage: -Warum nicht statische Module verwenden? Dort ist schließlich alles bereits gemacht! Zum Beispiel var AWS = require('aws-sdk') und man muss nicht das 'Fahrrad' mit S3-Authentifizierung neu erfinden!
Kommen wir zu den Nachteilen
Für mich war die Unfähigkeit, externe JS-Module zu importieren, eine unangenehme, aber erwartbare Eigenschaft. Das im obigen Beispiel beschriebene require('crypto'), ist und funktioniert nur für sie. Außerdem gibt es keine Möglichkeit, Code aus Skripten wiederzuverwenden, weshalb er in verschiedene Dateien kopiert werden muss. Ich hoffe, dass diese Funktionalität irgendwann implementiert wird.
Für das aktuelle Projekt sollte außerdem die Kompression in Nginx deaktiviert sein. gzip off;
Das liegt daran, dass es kein gzip-Modul in NJS gibt und es nicht möglich ist, es zu aktivieren. Daher gibt es keine Möglichkeit, mit komprimierten Daten zu arbeiten. Allerdings ist das in diesem Fall nicht wirklich ein Nachteil. Es gibt nicht viel Text, und die übertragenden Dateien sind bereits komprimiert, zusätzliche Kompression wird ihnen nicht viel nützen. Außerdem handelt es sich nicht um einen so stark belasteten oder kritischen Dienst, dass man sich um die Bereitstellung von Inhalten kümmern müsste, um sie ein paar Millisekunden schneller auszuliefern.
Das Debuggen des Skripts ist langwierig und nur über "Prints" in error.log möglich. Abhängig von der eingestellten Protokollstufe info, warn oder error können die Methoden r.log, r.warn, r.error entsprechend verwendet werden. Einige Skripte versuche ich in Chrome (v8) oder im Konsolenwerkzeug njs zu debuggen, aber nicht alle können dort überprüft werden. Beim Debuggen des Codes, d.h. beim funktionalen Testen, sieht die Historie etwa so aus:
docker-compose restart nginx
curl localhost:8080/
docker-compose logs --tail 10 nginxUnd solche Sequenzen können Hunderte umfassen.
Die Programmierung mit Unterabfragen und Variablen verwandelt sich in ein verwirrendes Durcheinander. Manchmal springt man zwischen verschiedenen IDE-Fenstern hin und her, um die Reihenfolge der Ausführungen deines Codes zu verstehen. Es ist nicht schwer, aber manchmal kann es sehr nerven.
Es gibt keine umfassende Unterstützung für ES6.
Vielleicht gibt es noch weitere Nachteile, aber ich bin auf nichts Weiteres gestoßen. Teile bitte deine Erfahrungen, wenn du negative Erfahrungen mit NJS gemacht hast.
Fazit
NJS ist ein leichtgewichtiger Open-Source-Interpreter, der es ermöglicht, verschiedene Szenarien in Nginx in der Programmiersprache JavaScript zu realisieren. Bei seiner Entwicklung wurde viel Wert auf Leistung gelegt. Natürlich gibt es viele Dinge, die noch fehlen, aber das Projekt entwickelt sich durch ein kleines Team und sie fügen aktiv neue Funktionen hinzu und beheben Bugs. Ich hoffe, dass NJS eines Tages die Anbindung externer Module ermöglichen wird, was die Funktionalität von Nginx praktisch unbegrenzt machen könnte. Aber es gibt NGINX Plus, und einige Funktionen werden wahrscheinlich nicht verfügbar sein!
und
/ Выступление Дмитрия Волныева на Saint HighLoad++ 2019
/ Выступление Василия Сошникова на HighLoad++ 2019
Quelle: habr.com
