Энэ нийтлэлд би Nginx Inc-ийн бүтээсэн Nginx-д зориулсан JavaScript орчуулагч NJS-тэй ажиллаж байсан туршлагаа хуваалцахыг хүсч, түүний үндсэн чадавхийг бодит жишээн дээр тайлбарлахыг хүсч байна. NJS нь Nginx-ийн үйл ажиллагааг өргөтгөх боломжийг олгодог JavaScript-ийн дэд хэсэг юм. гэсэн асуултад
Удаан хугацааны өмнө…
Сүүлчийн ажил дээрээ би docker-compose, dind болон kaniko rails руу шилжсэн олон өнгийн CI/CD дамжуулах хоолой бүхий gitlab-ийг өвлөн авсан. Өмнө нь CI-д ашиглагдаж байсан зургуудыг анхны хэлбэрт нь шилжүүлсэн. Манай gitlab IP өөрчлөгдөж, CI хулуу болж хувирах хүртэл тэд зөв ажиллаж байсан. Асуудал нь CI-д оролцсон докерийн зургуудын нэг нь git-тэй байсан бөгөөд Python модулиудыг ssh-ээр дамжуулан татдаг байв. ssh-ийн хувьд танд хувийн түлхүүр хэрэгтэй бөгөөд ... энэ нь мэдэгдэж буй_хостуудын хамт зурган дээр байсан. Бодит IP болон мэдэгдэж буй_hosts-д заасан хооронд таарахгүйн улмаас аливаа CI-д түлхүүр баталгаажуулалтын алдаа гарсан. Одоо байгаа Dockfiles-аас шинэ зургийг хурдан угсарч, сонголтыг нэмсэн StrictHostKeyChecking no
. Гэвч муу амт хэвээр үлдсэн бөгөөд либерүүдийг хувийн PyPI репозитор руу шилжүүлэх хүсэл байсан. Хувийн PyPI-д шилжсэний дараа нэмэлт урамшуулал бол илүү энгийн дамжуулах хоолой, шаардлага.txt-ийн ердийн тайлбар байв.
Сонголт хийгдлээ, ноёд оо!
Бид бүх зүйлийг үүлэн болон Кубернетес дээр ажиллуулдаг бөгөөд эцэст нь бид гадаад хадгалалт бүхий харьяалалгүй контейнер байсан жижиг үйлчилгээг авахыг хүссэн. За тэгээд бид S3 ашигладаг болохоор эн тэргүүнд тавигдсан. Боломжтой бол gitlab дээр баталгаажуулалттай (шаардлагатай бол та өөрөө нэмж болно).
Шуурхай хайлт хийснээр хэд хэдэн үр дүн гарсан: s3pypi, pypicloud болон манжингийн html файлуудыг "гараар" үүсгэх сонголт. Сүүлийн сонголт нь өөрөө алга болсон.
s3pypi: Энэ бол S3 хостинг ашиглахад зориулагдсан cli юм. Бид файлуудыг байршуулж, html үүсгэж, ижил хувин руу байршуулдаг. Гэрийн хэрэглээнд тохиромжтой.
pypicloud: Энэ нь сонирхолтой төсөл юм шиг санагдаж байсан ч баримт бичгийг уншсаны дараа би урам хугарсан. Хэдийгээр сайн бичиг баримттай, таны хэрэгцээнд нийцүүлэн өргөжүүлэх боломжтой байсан ч бодит байдал дээр энэ нь шаардлагагүй бөгөөд тохируулахад хэцүү болсон. Тухайн үеийн тооцоогоор өөрийн даалгаварт тохируулан кодыг засч залруулахад 3-5 хоног шаардагдана. Үйлчилгээнд мөн мэдээллийн сан хэрэгтэй. Өөр зүйл олдохгүй бол бид үүнийг орхисон.
Илүү нарийвчилсан хайлтаар Nginx ngx_aws_auth модулийг оллоо. Түүний туршилтын үр дүн нь S3 хувингийн агуулгыг харуулсан хөтөч дээр XML-г харуулсан. Хамгийн сүүлд эрэн сурвалжлах ажиллагаа жилийн өмнө болсон. Хадгалах газар хаягдсан харагдсан.
Эх сурвалж руу нь очиж уншсанаар
Энэ жишээг үндэслэн нэг цагийн дараа би хөтөч дээрээ ngx_aws_auth модулийг ашиглахтай ижил XML-г харсан боловч бүх зүйл JS дээр бичигдсэн байсан.
Надад nginx шийдэл үнэхээр таалагдсан. Нэгдүгээрт, сайн баримт бичиг, олон жишээ, хоёрдугаарт, бид Nginx-ийн файлуудтай ажиллахад зориулсан бүх давуу талыг олж авдаг (хайрцагнаас гарсан), гуравдугаарт, Nginx-ийн тохиргоог хэрхэн бичихийг мэддэг хэн бүхэн юу болохыг олж мэдэх боломжтой болно. Минимализм нь Python эсвэл Go (хэрэв эхнээс нь бичсэн бол) -тай харьцуулахад миний хувьд нэмэлт зүйл юм.
TL;DR 2 хоногийн дараа PyPi-ийн туршилтын хувилбарыг CI-д аль хэдийн ашигласан.
энэ нь хэрхэн ажилладаг вэ?
Модуль нь Nginx-д ачаалагдсан ngx_http_js_module
, албан ёсны докерын зурагт орсон. Бид удирдамжийг ашиглан скриптээ импортолдог js_import
Nginx тохиргоонд. Уг функцийг удирдамжаар дууддаг js_content
. Заавар нь хувьсагчдыг тохируулахад хэрэглэгддэг js_set
, энэ нь зөвхөн скриптэд тодорхойлсон функцийг аргумент болгон авдаг. Гэхдээ бид зөвхөн Nginx ашиглан NJS дээр дэд асуулга ажиллуулж чадна, ямар ч XMLHttpRequest биш. Үүнийг хийхийн тулд Nginx тохиргоонд тохирох байршлыг нэмэх шаардлагатай. Мөн скрипт нь энэ байршлын дэд хүсэлтийг тайлбарлах ёстой. 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, тодорхой төрлийн стринг үүсгэгдэж, SECRET_KEY ашиглан гарын үсэг зурсан (HMAC_SHA1). Дараа нь иймэрхүү мөр байна 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 зөвшөөрлийн жишээ, хуучирсан статус болгон өөрчилсөн)
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}
талаар бага зэрэг тайлбар _subrequest_uri
: энэ нь анхны uri-аас хамааран S3 руу хүсэлт үүсгэдэг хувьсагч юм. Хэрэв та "үндэс" -ийн агуулгыг авах шаардлагатай бол хязгаарлагчийг зааж өгсөн uri хүсэлтийг үүсгэх хэрэгтэй. delimiter
, энэ нь сангуудад харгалзах бүх CommonPrefixes xml элементүүдийн жагсаалтыг буцаана (PyPI-ийн хувьд бүх багцын жагсаалт). Хэрэв та тодорхой лавлах дахь агуулгын жагсаалтыг (бүх багцын хувилбаруудын жагсаалт) авах шаардлагатай бол uri хүсэлт нь лавлах (багц) -ын нэр бүхий угтвар талбарыг заавал налуу зураасаар төгссөн байх ёстой. Үгүй бол, жишээлбэл, лавлахын агуулгыг хүсэх үед зөрчилдөх боломжтой. Хэрэв хүсэлтэд заасан бол 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-г задлан шинжилж, эхлээд Content-Type толгой хэсгийг текст/html-ээр сольж 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="/mn/${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-г гадны үйлчилгээнүүдийг баталгаажуулах/зөвшөөрөхөд ашиглах боломжтой. Nginx дахь auth_request удирдамжийг ашиглан бид баталгаажуулалтын өгөгдлийг скрипт дэх функцийн дуудлага агуулсан дэд хүсэлт рүү дахин чиглүүлэх болно. Скрипт нь Gitlab url-д дахин дэд хүсэлт гаргах бөгөөд хэрэв баталгаажуулалтын өгөгдлийг зөв зааж өгсөн бол Gitlab 200 кодыг буцааж өгөх бөгөөд багцыг байршуулах/татаж авахыг зөвшөөрөх болно. Яагаад нэг дэд асуулга ашиглаад Gitlab руу өгөгдлийг шууд илгээж болохгүй гэж? Учир нь бид зөвшөөрөлд ямар нэгэн өөрчлөлт хийх бүрт Nginx тохиргооны файлыг засварлах шаардлагатай болдог бөгөөд энэ нь нэлээд уйтгартай ажил юм. Түүнчлэн, хэрэв Кубернетес зөвхөн уншигдах үндсэн файлын системийн бодлогыг ашигладаг бол nginx.conf-г тохиргооны зургаар солих үед энэ нь илүү төвөгтэй байдлыг нэмэгдүүлдэг. Эзлэхүүн (pvc) болон зөвхөн уншигдах үндсэн файлын системийн холболтыг хориглох бодлогыг нэгэн зэрэг ашиглахын зэрэгцээ Nginx-ийг configmap-ээр тохируулах нь туйлын боломжгүй болно (энэ нь бас тохиолддог).
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') юм
Nginx дээрх одоогийн төслийн хувьд шахалтыг мөн идэвхгүй болгох шаардлагатай gzip off;
Учир нь NJS-д gzip модуль байхгүй бөгөөд үүнийг холбох боломжгүй тул шахсан өгөгдөлтэй ажиллах арга байхгүй. Үнэн, энэ нь энэ хэргийн хувьд хасах зүйл биш юм. Текст тийм ч их биш бөгөөд шилжүүлсэн файлууд аль хэдийн шахагдсан бөгөөд нэмэлт шахалт нь тэдэнд тийм ч их тус болохгүй. Түүнчлэн, энэ нь контентыг хэдхэн миллисекунд хурдан хүргэх гэж санаа зовох ёстой тийм ачаалалтай эсвэл чухал үйлчилгээ биш юм.
Скриптийг дибаг хийхэд удаан хугацаа шаардагдах бөгөөд зөвхөн error.log дахь "хэвлэх" замаар л боломжтой. Бүртгэлийн түвшний мэдээлэл, анхааруулах эсвэл алдаанаас хамааран r.log, r.warn, r.error гэсэн 3 аргыг тус тус ашиглах боломжтой. Би Chrome (v8) эсвэл njs консол хэрэгсэл дээр зарим скриптийг дибаг хийхийг оролддог боловч бүгдийг нь шалгах боломжгүй. Функциональ тест гэх мэт кодыг дибаг хийх үед түүх дараах байдалтай харагдана.
docker-compose restart nginx
curl localhost:8080/
docker-compose logs --tail 10 nginx
мөн ийм олон зуун дараалал байж болно.
Дэд асуулга, хувьсагчийг ашиглан код бичих нь орооцолдсон ээдрээ болж хувирдаг. Заримдаа та кодынхоо үйлдлийн дарааллыг олох гэж янз бүрийн IDE цонхнуудыг тойрон гүйж эхэлдэг. Энэ нь хэцүү биш ч заримдаа маш их ядаргаатай байдаг.
ES6-д бүрэн дэмжлэг байхгүй.
Бусад дутагдалтай талууд байж болох ч би өөр зүйлтэй тулгараагүй. Хэрэв та NJS-ийг ашиглах сөрөг туршлагатай бол мэдээллээ хуваалцаарай.
дүгнэлт
NJS бол Nginx дээр янз бүрийн JavaScript скриптүүдийг хэрэгжүүлэх боломжийг олгодог хөнгөн нээлттэй эхийн орчуулагч юм. Түүнийг хөгжүүлэх явцад гүйцэтгэлд ихээхэн анхаарал хандуулсан. Мэдээжийн хэрэг, дутуу зүйл их байгаа ч төслийг жижиг баг боловсруулж байгаа бөгөөд тэд идэвхтэй шинэ боломжуудыг нэмж, алдаануудыг засаж байна. Хэзээ нэгэн цагт NJS танд гадаад модулиудыг холбох боломжийг олгоно гэж найдаж байна, энэ нь Nginx функцийг бараг хязгааргүй болгоно. Гэхдээ NGINX Plus байгаа бөгөөд ямар ч функц байхгүй байх магадлалтай!
Эх сурвалж: www.habr.com