Այս հոդվածում ես կցանկանայի կիսվել իմ փորձով NJS-ի հետ՝ Nginx Inc-ի կողմից մշակված Nginx-ի JavaScript թարգմանչի հետ՝ նկարագրելով դրա հիմնական հնարավորությունները՝ օգտագործելով իրական օրինակ: NJS-ը JavaScript-ի ենթաբազմություն է, որը թույլ է տալիս ընդլայնել Nginx-ի ֆունկցիոնալությունը: Հարցին
Երկար ժամանակ առաջ…
Իմ վերջին աշխատանքի ժամանակ ես ժառանգեցի gitlab մի շարք խայտաբղետ CI/CD խողովակաշարերով՝ docker-compose, dind և այլ հաճույքներով, որոնք փոխանցվեցին kaniko rails-ին: Պատկերները, որոնք նախկինում օգտագործվել են CI-ում, տեղափոխվել են իրենց սկզբնական տեսքով: Նրանք ճիշտ աշխատեցին մինչև այն օրը, երբ մեր gitlab IP-ն փոխվեց և CI-ն վերածվեց դդմի։ Խնդիրն այն էր, որ դոկերի պատկերներից մեկը, որը մասնակցում էր CI-ին, ուներ git, որը քաշում էր Python մոդուլները ssh-ի միջոցով: ssh-ի համար պետք է մասնավոր բանալի և... այն պատկերում էր՝ known_hosts-ի հետ միասին։ Եվ ցանկացած CI ձախողվեց առանցքային ստուգման սխալի պատճառով իրական IP-ի և հայտնի_hosts-ում նշվածի անհամապատասխանության պատճառով: Գոյություն ունեցող Dockfiles-ից արագ հավաքվեց նոր պատկեր և ավելացվեց տարբերակը StrictHostKeyChecking no
. Բայց անճաշակությունը մնաց, և ցանկություն առաջացավ տեղափոխել libs-ը մասնավոր PyPI պահոց: Լրացուցիչ բոնուս, մասնավոր PyPI-ին անցնելուց հետո, ավելի պարզ խողովակաշարն էր և պահանջների.txt-ի նորմալ նկարագրությունը։
Ընտրությունը կատարված է, պարոնայք։
Մենք ամեն ինչ գործարկում ենք ամպերի և Kubernetes-ի մեջ, և վերջում մենք ուզում էինք ստանալ մի փոքրիկ ծառայություն, որը քաղաքացիություն չունեցող կոնտեյներ էր արտաքին պահեստով: Դե, քանի որ մենք օգտագործում ենք S3, առաջնահերթությունը տրվեց դրան։ Եվ, հնարավորության դեպքում, վավերացումով gitlab-ում (անհրաժեշտության դեպքում կարող եք ինքներդ ավելացնել):
Արագ որոնումը տվեց մի քանի արդյունք՝ s3pypi, pypicloud և շաղգամի համար html ֆայլերի «ձեռքով» ստեղծման տարբերակ: Վերջին տարբերակն ինքնին անհետացավ.
s3pypi. Սա S3 հոստինգ օգտագործելու համար է: Մենք վերբեռնում ենք ֆայլերը, ստեղծում html-ը և վերբեռնում այն նույն դույլում։ Հարմար է տնային օգտագործման համար։
pypicloud. Թվում էր, թե հետաքրքիր նախագիծ էր, բայց փաստաթղթերը կարդալուց հետո ես հիասթափվեցի: Չնայած լավ փաստաթղթերին և ձեր կարիքներին համապատասխան ընդլայնելու հնարավորությանը, իրականում պարզվեց, որ այն ավելորդ է և դժվար է կարգավորել: Կոդի ուղղումը ձեր առաջադրանքներին համապատասխանելու համար, ըստ այն ժամանակվա գնահատականների, կտևի 3-5 օր: Ծառայությանը անհրաժեշտ է նաև տվյալների բազա։ Մենք թողեցինք այն, եթե այլ բան չգտանք:
Ավելի խորը որոնումը տվեց Nginx-ի մոդուլ՝ ngx_aws_auth: Նրա փորձարկման արդյունքը բրաուզերում ցուցադրվել է XML-ը, որը ցույց է տվել S3 դույլի պարունակությունը։ Վերջին անգամ խուզարկության ժամանակ կատարվել է մեկ տարի առաջ։ Պահեստը լքված տեսք ուներ:
Աղբյուր գնալով և կարդալով
Այս օրինակը հիմք ընդունելով, մեկ ժամ անց ես իմ բրաուզերում տեսա նույն XML-ը, ինչ ngx_aws_auth մոդուլն օգտագործելիս, բայց ամեն ինչ արդեն գրված էր JS-ով։
Ինձ շատ դուր եկավ nginx լուծումը: Նախ, լավ փաստաթղթեր և բազմաթիվ օրինակներ, երկրորդը, մենք ստանում ենք Nginx-ի բոլոր առավելությունները ֆայլերի հետ աշխատելու համար (արկղից դուրս), երրորդ, յուրաքանչյուր ոք, ով գիտի, թե ինչպես գրել կոնֆիգուրացիաներ Nginx-ի համար, կկարողանա պարզել, թե ինչն է: Մինիմալիզմը նույնպես ինձ համար պլյուս է, համեմատած Python-ի կամ Go-ի հետ (եթե զրոյից գրված է), էլ չեմ խոսում նեքսուսի մասին։
TL;DR 2 օր անց PyPi-ի փորձնական տարբերակը արդեն օգտագործվել է CI-ում:
Ինչպես է դա աշխատում.
Մոդուլը բեռնված է Nginx-ում ngx_http_js_module
, ներառված է պաշտոնական դոկերի պատկերում: Մենք ներմուծում ենք մեր սցենարը՝ օգտագործելով հրահանգը js_import
դեպի Nginx կոնֆիգուրացիա: Ֆունկցիան կոչվում է հրահանգով js_content
. Հրահանգը օգտագործվում է փոփոխականներ սահմանելու համար js_set
, որը որպես փաստարկ ընդունում է միայն սկրիպտում նկարագրված ֆունկցիան։ Բայց մենք կարող ենք ենթահարցումներ կատարել NJS-ում միայն Nginx-ի միջոցով, ոչ թե 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-ից, ստեղծվում է տողի որոշակի տեսակ, որը ստորագրվում է (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 թույլտվության օրինակ, փոխվել է հնացած կարգավիճակի)
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-խնդրանք և aiohttp-հարցումներ և եթե հարցումը սահմանում է /?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 վերնագիրը փոխարինելով տեքստով/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="/hy/${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-ում, մենք նույնականացման տվյալները կվերահղենք դեպի ենթահարցում, որը պարունակում է ֆունկցիայի կանչ սկրիպտում: Սկրիպտը ևս մեկ ենթահարկ կկատարի Gitlab-ի url-ին, և եթե վավերացման տվյալները ճիշտ են նշված, ապա Gitlab-ը կվերադարձնի 200 կոդը, և փաթեթի վերբեռնումը/ներբեռնումը կթույլատրվի: Ինչու չօգտագործել մեկ ենթահարկ և անմիջապես ուղարկել տվյալները Gitlab-ին: Որովհետև մենք ստիպված կլինենք խմբագրել Nginx կազմաձևման ֆայլը ամեն անգամ, երբ թույլտվության մեջ որևէ փոփոխություն ենք կատարում, և սա բավականին հոգնեցուցիչ խնդիր է: Բացի այդ, եթե Kubernetes-ը օգտագործում է միայն կարդալու արմատային ֆայլային համակարգի քաղաքականություն, ապա դա էլ ավելի է բարդացնում nginx.conf-ը configmap-ի միջոցով: Եվ բացարձակապես անհնար է դառնում կարգավորել Nginx-ը configmap-ի միջոցով՝ միաժամանակ օգտագործելով ծավալների (pvc) և միայն կարդալու համար արմատային ֆայլային համակարգի միացումն արգելող քաղաքականություն (դա նույնպես տեղի է ունենում):
Օգտագործելով 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 = պահանջել ('aws-sdk') և կարիք չկա գրել «հեծանիվ» S3 իսկորոշմամբ:
Անցնենք մինուսներին
Ինձ համար արտաքին JS մոդուլներ ներմուծելու անկարողությունը դարձավ տհաճ, բայց սպասված հատկանիշ։ Վերևի օրինակում նկարագրված պահանջը («կրիպտո») է
Սեղմումը պետք է նաև անջատված լինի 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-ը թեթև բաց կոդով թարգմանիչ է, որը թույլ է տալիս իրականացնել տարբեր JavaScript սկրիպտներ Nginx-ում: Նրա մշակման ընթացքում մեծ ուշադրություն է դարձվել կատարմանը։ Իհարկե, դեռ շատ բան կա, բայց նախագիծը մշակվում է փոքր թիմի կողմից, և նրանք ակտիվորեն ավելացնում են նոր հնարավորություններ և շտկում սխալները: Հուսով եմ, որ մի օր NJS-ը ձեզ թույլ կտա միացնել արտաքին մոդուլներ, ինչը Nginx-ի գործունակությունը կդարձնի գրեթե անսահմանափակ։ Բայց կա NGINX Plus և, ամենայն հավանականությամբ, գործառույթներ չեն լինի:
Source: www.habr.com