Dans cet article, je souhaite partager mon expérience de travail avec NJS, un interpréteur JavaScript pour Nginx développé par Nginx Inc, décrivant ses principales fonctionnalités à l'aide d'un exemple réel. NJS est un sous-ensemble de JavaScript qui vous permet d'étendre les fonctionnalités de Nginx. Lorsqu'on lui a demandé pourquoi son propre interprète ??? Dmitry Volyntsev a répondu en détail. En bref: NJS est nginx-way, et JavaScript est plus progressif, "natif" et sans GC, contrairement à Lua.
Il y a longtemps ...
Lors de mon dernier travail, j'ai hérité de gitlab avec un certain nombre de pipelines CI / CD hétéroclites avec docker-compose, dind et autres délices qui ont été transférés sur les rails kaniko. Les images qui étaient précédemment utilisées dans CI ont été déplacées dans leur forme d'origine. Ils ont fonctionné correctement jusqu'au jour où notre gitlab a changé son IP et CI s'est transformé en citrouille. Le problème était que l'une des images du docker qui participait au CI contenait git, qui tirait des modules Python via ssh. Ssh a besoin d'une clé privée et ... c'était dans l'image avec les known_hosts. Et tout CI n'a pas réussi à vérifier la clé en raison d'une non-concordance entre l'adresse IP réelle et celle spécifiée dans known_hosts. Une nouvelle image a été rapidement construite à partir des Dockfiles existants et une option a été ajoutéeStrictHostKeyChecking no
... Mais l'arrière-goût désagréable est resté et il y avait un désir de le transférer dans un dépôt privé PyPI. Un bonus supplémentaire, après le passage à un PyPI privé, est devenu un pipeline plus simple et une description normale d'exigences.txt
Le choix est fait, messieurs!
Nous tournons tout dans les nuages et Kubernetes, et à la fin nous voulions obtenir un petit service qui était un conteneur sans état avec stockage externe. Eh bien, puisque nous utilisons S3, c'était la priorité. Et, si possible, avec l'authentification dans gitlab (vous pouvez l'ajouter vous-même si nécessaire).
Une recherche rapide a donné plusieurs résultats pour s3pypi, pypicloud et une option pour générer "manuellement" des fichiers html pour le navet. La dernière option a disparu d'elle-même.
s3pypi: C'est le cli pour utiliser l'hébergement S3. Nous téléchargeons des fichiers, générons du html et remplissons le même seau. Convient pour un usage domestique.
pypicloud: , . , . , , 3-5 . . , .
Nginx, ngx_aws_auth. XML , S3. , , . .
PEP-503 , XML HTML pip. Nginx S3 S3 JS Nginx. NJS.
, XML, ngx_aws_auth, JS.
nginx . - , - Nginx ( ), - Nginx, . , Python Go ( ), nexus.
TL;DR 2 PyPi CI.
?
Nginx ngx_http_js_module
, docker-. c js_import
Nginx. js_content
. js_set
, . NJS Nginx, XMLHttpRequest. Nginx . (subrequest) . Nginx, 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}
http://localhost:8080/
location /
js_content
request
script.js
. request
location = /sub-query
, ( GET) (r)
, . call_back
.
S3
S3-, :
ACCESS_KEY
SECRET_KEY
S3_BUCKET
http-, /, S3_NAME URI , (HMAC_SHA1) SECRET_KEY. , AWS $ACCESS_KEY:$HASH
, . /, , X-amz-date
. :
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/(?<s3_path>.*) {
internal;
proxy_set_header X-amz-date $s3_datetime;
proxy_set_header Authorization $s3_auth;
proxy_pass $s3_endpoint/$s3_path;
}
location ~ "^/(?<prefix>[\w-]*)[/]?(?<postfix>[\w-\.]*)$" {
js_content s3.request;
}
}
s3.js
( AWS Sign v2, deprecated)
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 + '\n\n\n\n';
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}
_subrequest_uri
: uri S3. «», uri- delimiter
, xml- CommonPrefixes, ( PyPI, ). ( ), uri- prefix () /. , . aiohttp-request aiohttp-requests /?prefix=aiohttp-request
, . , /?prefix=aiohttp-request/
, . , uri .
, Nginx. Nginx, XML, :
<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Name>myback-space</Name>
<Prefix></Prefix>
<Marker></Marker>
<MaxKeys>10000</MaxKeys>
<Delimiter>/</Delimiter>
<IsTruncated>false</IsTruncated>
<CommonPrefixes>
<Prefix>new/</Prefix>
</CommonPrefixes>
<CommonPrefixes>
<Prefix>old/</Prefix>
</CommonPrefixes>
</ListBucketResult>
CommonPrefixes
.
, , , XML:
<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Name> myback-space</Name>
<Prefix>old/</Prefix>
<Marker></Marker>
<MaxKeys>10000</MaxKeys>
<Delimiter></Delimiter>
<IsTruncated>false</IsTruncated>
<Contents>
<Key>old/giphy.mp4</Key>
<LastModified>2020-08-21T20:27:46.000Z</LastModified>
<ETag>"00000000000000000000000000000000-1"</ETag>
<Size>1350084</Size>
<Owner>
<ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID>
<DisplayName></DisplayName>
</Owner>
<StorageClass>STANDARD</StorageClass>
</Contents>
<Contents>
<Key>old/hsd-k8s.jpg</Key>
<LastModified>2020-08-31T16:40:01.000Z</LastModified>
<ETag>"b2d76df4aeb4493c5456366748218093"</ETag>
<Size>93183</Size>
<Owner>
<ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID>
<DisplayName></DisplayName>
</Owner>
<StorageClass>STANDARD</StorageClass>
</Contents>
</ListBucketResult>
Key
.
XML HTML, Content-Type 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>'
}
PyPI
, .
#
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:8080
.
#
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:8080
CI, :
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}"
Gitlab JWT / . auth_request Nginx, . url Gitlab- , Gitlab 200 / . Gitlab? , Nginx , - , . , Kubernetes read-only root filesystem, nginx.conf configmap. Nginx configmap (pvc) read-only root filesystem ( ).
NJS, nginx - (, 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 ~ "^/(?<prefix>[\w-]*)[/]?(?<postfix>[\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}
: - ? ! , var AWS = require('aws-sdk') "" S3-!
, JS-, , . require('crypto'), build-in- require . - . , - .
Nginx gzip off;
, gzip- NJS , . , . , . , .
«» error.log. info, warn error 3 r.log, r.warn, r.error . Chrome (v8) njs, . , , history :
docker-compose restart nginx
curl localhost:8080/
docker-compose logs --tail 10 nginx
.
, . IDE . , .
ES6.
- , . NJS.
NJS - open-source , Nginx JavaScript. . , . , - NJS , Nginx . NGINX Plus - !
Exemples d'utilisation de NJS de Dmitry Volyntsev
njs - scripts JavaScript natifs dans le discours de nginx / Dmitry Volnyev à Saint HighLoad ++ 2019
NJS en production / Discours de Vasily Soshnikov à HighLoad ++ 2019