Pada artikel ini saya ingin berbagi pengalaman saya bekerja dengan NJS, juru bahasa JavaScript untuk Nginx yang dikembangkan oleh Nginx Inc, menjelaskan fitur utamanya menggunakan contoh nyata. NJS adalah bagian dari JavaScript yang memungkinkan Anda memperluas fungsionalitas Nginx. Ketika ditanya mengapa penerjemah sendiri ??? Dmitry Volyntsev menjawab dengan detail. Singkatnya: NJS adalah cara nginx, dan JavaScript lebih progresif, "asli" dan tanpa GC, tidak seperti Lua.
Dahulu kala ...
Pada pekerjaan terakhir saya, saya mewarisi gitlab dengan sejumlah pipeline CI / CD beraneka ragam dengan docker-compose, dind, dan kesenangan lainnya yang dipindahkan ke rel kaniko. Gambar yang sebelumnya digunakan di CI telah dipindahkan ke bentuk aslinya. Mereka bekerja dengan baik sampai hari ketika gitlab kami mengubah IP dan CI-nya menjadi labu. Masalahnya adalah salah satu image buruh pelabuhan yang berpartisipasi dalam CI berisi git, yang menarik modul Python melalui ssh. Ssh membutuhkan kunci pribadi dan ... itu ada dalam gambar bersama dengan known_hosts. Dan CI apa pun gagal memverifikasi kunci karena ketidaksesuaian antara IP asli dan yang ditentukan di known_hosts. Image baru dengan cepat dibangun dari Dockfiles yang ada dan sebuah opsi telah ditambahkanStrictHostKeyChecking no... Tetapi sisa rasa yang tidak menyenangkan tetap ada dan ada keinginan untuk mentransfernya ke repositori PyPI pribadi. Bonus tambahan, setelah beralih ke PyPI pribadi, menjadi pipeline yang lebih sederhana dan deskripsi normal dari Requirement.txt
Pilihan sudah dibuat, Tuan-tuan!
Kami memutar semuanya di cloud dan Kubernetes, dan sebagai hasilnya, kami ingin mendapatkan layanan kecil yang merupakan container stateless dengan penyimpanan eksternal. Nah, karena kami menggunakan S3, maka itu yang menjadi prioritas. Dan, jika memungkinkan, dengan otentikasi di gitlab (Anda dapat menambahkannya sendiri jika perlu).
Pencarian cepat menghasilkan beberapa hasil untuk s3pypi, pypicloud, dan opsi untuk "secara manual" menghasilkan file html untuk lobak. Opsi terakhir menghilang dengan sendirinya.
s3pypi: Ini adalah cli untuk menggunakan hosting S3. Kami mengunggah file, membuat html, dan mengisi ember yang sama. Cocok untuk digunakan di rumah.
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:8080CI, :
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 - !
Contoh penggunaan NJS dari Dmitry Volyntsev
njs - skrip JavaScript asli dalam pidato nginx / Dmitry Volnyev di Saint HighLoad ++ 2019
NJS dalam produksi / pidato Vasily Soshnikov di HighLoad ++ 2019