Unë krijova depozitën time PyPI me autorizim dhe S3. Në Nginx

Në këtë artikull do të ndaj përvojën time me NJS, interpreterin JavaScript për Nginx të zhvilluar nga kompania Nginx Inc, duke përshkruar me një shembull të vërtetë mundësitë e tij kryesore. NJS është një nëngrup i gjuhës programim JavaScript, i cili lejon zgjerimin e funksionalitetit të Nginx. Për pyetjen pse një interpreter tëndin??? ka dhënë një përgjigje të detajuar Dmitri Volyntsev. Nëse e përmbledhim: NJS është mënyra nginx, ndërsa JavaScript është më progresiv, 'vendase' dhe pa GC në krahasim me Lua.

Një kohë e gjatë më parë...

Në punën time të kaluar, trashëguam një gitlab me disa CI/CD pipeline të larmishme me docker-compose, dind dhe mrekullira të tjera, që u përkthyen në kaniko. Imazhet që ishin përdorur më parë në CI mbetën në formën e tyre origjinale. Ato punuan mirë deri në ditën kur gitlab-i ynë ndryshoi IP-në dhe CI u shndërrua në një të papërmbushur. Problemi ishte se në një nga imazhet docker, që merrnin pjesë në CI, kishte git, i cili shkarkonte modula Python përmes ssh. Për ssh nevojitej një çelës privat dhe... ai ndodhej në imazh së bashku me known_hosts. Dhe çdo CI përfundonte me një gabim verifikimi çelësi për shkak të mos përputhjes së IP-reale me atë të specifikuar në known_hosts. Nga Dockerfile-të e pranishëm u krijua shpejt një imazh i ri dhe u shtua opsioni StrictHostKeyChecking no. Por një shije e pakëndshme mbeti dhe u shfaq dëshira për të transferuar librat në një depo të privatizuar PyPI. Një bonus shtesë, pas kalimit në PyPI të privatizuar, ishte një pipeline më i thjeshtë dhe një përshkrim normal të requirements.txt

Zgjedhja është bërë, Zotërinj!

Ne gjithçka e migrojmë në re dhe Kubernetes dhe në fund donim të marrim një shërbim të vogël që përfaqësonte një konteiner stateless me një ruajtje të jashtme. Prandaj, pasi përdorim S3, përparësia ishte për të. Dhe nëse është e mundur, me autentifikim në gitlab (mund të shkruhet edhe vetë sipas nevojës).

Një kërkim i shpejtë dha disa rezultate s3pypi, pypicloud dhe opsioni me 'krijimin manual' të skedarëve html për depo. Opsioni i fundit u eliminua vetvetiu.

s3pypi: Ky është cli për përdorimin e hostimit në S3. Ngarkojmë skedarët, gjenerojmë html dhe i ngarkojmë në të njëjtën kub. Për përdorim në shtëpi do të jetë i mjaftueshëm.

pypicloud: Duke duke një projekt interesant, por pas leximit të dokumentacionit erdhi zhgënjimi. Edhe pse shkalla e dokumentacionit dhe mundësitë e zgjerimit për nevojat tona ishin të mira, në praktikë doli të ishte tepër i ndërlikuar dhe i vështirë për t'u konfiguruar. Rregullimi i kodit për nevojat e mia, sipas llogarive të atëhershme, do të merrte rreth 3-5 ditë. Po ashtu, shërbimi ka nevojë për një DB. E lamë atë në rast se nuk gjenim gjë tjetër.

Një kërkim më të thelluar solli modul për Nginx, ngx_aws_auth. Si rezultat i testimit të tij u shfaq një XML i dukshëm në shfletues, ku ishte e qartë përmbajtja e bucket S3. Komiti më i fundit, në momentin e kërkimit, ishte një vit më parë. Depoja dukej e braktisur.

Duke iu drejtuar burimit të parë dhe duke lexuar PEP-503 kuptova se XML mund të konvertohej në HTML në flakadane dhe të dërgohej nga pip. Pas pak kërkimesh për fjalët Nginx dhe S3, ndesha një shembull autentikimi në S3 të shkruar në JS për Nginx. Kështu u njoha me NJS.

Dhe duke marrë si bazë këtë shembull, pas një orë, po shikoja në shfletuesin tim të njëjtin XML, siç ishte me modulin ngx_aws_auth, por tashmë gjithçka ishte e shkruar në JS.

Zgjidhja në nginx më pëlqeu shumë. Së pari, dokumentacioni i mirë dhe shumë shembuj, së dyti, ne përfitojmë të gjitha avantazhet që ofron Nginx për punën me skedarët (nga kutia), së treti, çdo njeri që di të shkruajë konfigurata për Nginx do të ishte në gjendje të kuptonte se çfarë ka kuptim. Gjithashtu, minimalizmi është një pikë pozitive për mua, krahasuar me Python ose Go (nëse shkruajmë nga fillimi), duke mos përmendur nexus.

TL;DR Pas 2 ditësh, versioni testues i PyPi tashmë ishte përdorur në CI.

Si funksionon kjo?

Në Nginx ngarkohet moduli ngx_http_js_module, i përfshirë në imazhin zyrtar docker. Importojmë skenarin tonë me ndihmën e direktivës js_importnë konfigurimin Nginx. Thirrja e funksionit bëhet me ndihmën e direktivës js_content. Për të vendosur variablat përdoret direktiva js_set, e cila pranon si argument vetëm funksionin e përshkruar në skenarin. Por ne mund të kryejmë nënkërkesa në NJS vetëm me ndihmën e Nginx, pa XMLHttpRequest. Për këtë, në konfigurimin Nginx duhet të shtohet një lokacion përkatës. Po ashtu, në skenarin duhet të përshkruhet një nënkërkesë (subrequest) në këtë lokacion. Për të pasur mundësinë të kallëzojmë funksionin nga konfigurata Nginx, në vetë skenarin emri i funksionit duhet të eksportohet 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}

Me kërkesën në shfletues http://localhost:8080/ në location /ku drejtpërdrejtiva js_content thërret funksionin ) me të dhëna. Ja linja që mund ta kopjoni thjesht në fushën e adresës së shfletuesit, dhe pastaj të rifreskoni faqen (kjo duhet bërë vetëm një herë): është e përshkruar në skripte tonë script.js. Në të njëjtën kohë në funksionin ) me të dhëna. Ja linja që mund ta kopjoni thjesht në fushën e adresës së shfletuesit, dhe pastaj të rifreskoni faqen (kjo duhet bërë vetëm një herë): bëhet një nënkërkesë në location = /sub-query, me metodën (në shembullin aktual GET) të marrë nga argumenti (r), e cila i kalon në mënyrë të pavetëdijshme në thirrjen e këtij funksioni. Përpunimi i përgjigjes së nënkërkesës do të bëhet në funksionin call_back.

Provojmë S3

Për të bërë një kërkesë për një depo S3 private, na nevojiten:

KEY_ACCESS

KEY_SECRET

S3_BUCKET

Nga metoda http në përdorim, data/orari aktuale, S3_NAME dhe URI krijohet një varg të caktuar që nënshkruhet (HMAC_SHA1) duke përdorur KEY_SECRET. Më pas, vargu i formatit AWS $ACCESS_KEY:$HASH, mund të përdoret në titullin e autorizimit. E njëjta datë/orar që u përdor për të gjeneruar vargun në hapin e mëparshëm, duhet të shtohet në titullin X-amz-date. Në kod, kjo duket kështu:

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(shembulli i autorizimit AWS Sign v2, është quajtur tani i vjetëruar)

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}

Një sqarim i vogël në lidhje me _subrequest_uri: kjo është një variablë që në varësi të uri-së origjinale formon një kërkesë në S3. Nëse duhet të marrësh përmbajtjen e 'korrikut', atëherë duhet të formosh një uri-kërkese me specifikimin e ndarësit delimiter, i cili do të kthejë një listë të të gjithë elementeve xml-CommonPrefixes që korrespondon me direktoritë (në rastin e PyPI, lista e të gjithë paketimeve). Nëse duhet të marrësh një listë përmbajtjeje në një directory të caktuar (lista e të gjithë versioneve të paketimeve), atëherë uri-kërkesa duhet të përmbajë fushën prefix me emrin e direktorisë (paketit) që përfundon patjetër me slash /. Në të kundërt, mund të ndodhin kolizione duke kërkuar përmbajtjen e një direktorie, për shembull. Ka drejtoritë aiohttp-request dhe aiohttp-requests dhe nëse në kërkesë do të jetë e specifikuar /?prefix=aiohttp-request, atëherë përgjigjja do të ketë përmbajtjen e të dy drejtorive. Nëse në fund do të jetë slashi, /?prefix=aiohttp-request/, atëherë në përgjigje do të jetë vetëm direktorja e nevojshme. Dhe nëse kërkojmë një skedar, atëherë uri përfundimtar nuk duhet të ndryshojë nga origjinali.

Ruajmë, rihapim Nginx. Në shfletues shkruajmë adresën tonë të Nginx, rezultati i kërkesës do të jetë XML, për shembull:

Lista e direktorive

myback-space
  
  
  10000
  /
  false
  
    new/
  
  
    old/

Nga lista e direktorive na nevojiten vetëm elementët CommonPrefixes.

Duke shtuar, në shfletues, në adresën tonë drejtorinë e nevojshme, do të marrim përmbajtjen e saj gjithashtu në formën XML:

Lista e skedarëve në direktorinë

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
      
    
    STANDARD

Nga lista e skedarëve do të marrim vetëm elementët Çelësi.

Tani mbetet që XML e marrë ta analizojmë dhe ta dërgojmë si HTML, përpara se të ndërrohet titulli Content-Type në 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>'
}

Provojmë PyPI

Kontrollojmë që askund dhe asgjë të mos prishet në paketat që punojnë.

# Создаем для тестов новое окружение
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

Përsërisim me librat tanë.

# Создаем для тестов новое окружение
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

Në CI, krijimi dhe ngarkimi i paketës duket kështu:

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}"

Autentikimi

Në Gitlab, është e mundur të përdoret JWT për autentifikimin/autorizimin e shërbimeve të jashtme. Duke shfrytëzuar direktivën auth_request në Nginx, do të drejtojmë të dhënat e autentifikimit në një nënkërkesë që përmban thirrjen e funksionit në skript. Në skript do të bëhet një nënkërkesë tjetër në url-në e Gitlab-it, dhe nëse të dhënat e autentifikimit janë dhënë saktë, Gitlab do të kthejë kodin 200 dhe do të lejohet ngarkimi/shkarkimi i paketës. Pse të mos përdorim një nënkërkesë dhe të dërgojmë të dhënat menjëherë në Gitlab? Sepse do të duhet të rregullojmë skedarin e konfigurimit të Nginx çdo herë kur kemi ndonjë ndryshim në autorizim, dhe kjo është një detyrë mjaft e bezdisshme. Po ashtu, nëse në Kubernetes përdoret politika e sistemit të filesh read-only, kjo e bën edhe më të komplikuar zëvendësimin e nginx.conf përmes configmap. Dhe bëhet krejtësisht e pamundur konfigurimi i Nginx përmes configmap kur përdoren njëkohësisht politikat që ndalojnë lidhjen e volumave (pvc) dhe sistemi i filesh read-only (kjo ndodh ndonjëherë).

Duke përdorur NJS si komponent ndërmjetës, ne fitojmë mundësinë për të ndryshuar parametrat e caktuar në konfigurimin e nginx me anë të variablave mjedisorë dhe për të bërë disa kontrolle në skript (p.sh., URL e gabuar).

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}

Po e them se mund të lindë pyetja: - Pse të mos përdorim modulat e gatshme? Aty është bërë gjithçka! Për shembull, var AWS = require(‘aws-sdk’) dhe nuk është nevoja të shkruajmë ‘bicikletën’ me autentifikimin S3!

Tani le të kalojmë te disavantazhet.

Për mua, pamundësia e importimit të moduleve JS të jashtme ishte një veçori e pakëndshme, por e pritshme. Ajo që përmendet në shembullin më sipër require(‘crypto’), është modulet e ndërtuara dhe require funksionon vetëm për to. Po ashtu, nuk ka mundësi të ripërdorim kodin nga skriptet dhe duhet ta kopjojmë atë në skedarë të ndryshëm. Shpresoj që një ditë ky funksionalitet të realizohet.

Po ashtu për projektin aktual në Nginx duhet të çaktivizohet kompresimi gzip off;

Sepse nuk ka modul gzip në NJS dhe është e pamundur ta lidhësh atë, për rrjedhojë, nuk ka mundësi të punosh me të dhënat e kompresuara. Megjithatë, kjo nuk është tepër një disavantazh për këtë rast. Nuk ka shumë tekst, dhe skedarët që dërgohen tashmë janë të kompresuar, kështu që një kompresim i mëtejshëm nuk do të ndihmojë shumë. Gjithashtu, ky shërbim nuk është aq i ngarkuar ose kritik sa të merret me dorëzimin e përmbajtjes pak milisekonda më shpejt.

Debugimi i skemës është i gjatë dhe mundësia është vetëm përmes "printave" në error.log. Në varësi të nivelit të regjistrimit të vendosur, info, warn ose error, mund të përdoren 3 metoda r.log, r.warn, r.error përkatësisht. Disa skema përpiqem t'i debugoj në Chrome (v8) ose në mjetin konsolë njs, por jo gjithçka mund të kontrollohet aty. Gjatë debugimit të kodit, që është testi funksional, historia duket më pak si kjo:

docker-compose restart nginx
curl localhost:8080/
docker-compose logs --tail 10 nginx

dhe sekuencat e tilla mund të jenë qindra.

Shkrimi i kodit duke përdorur nënpyetje dhe variabla për to, shndërrohet në një ngatërrim të ndërlikuar. Ndonjëherë fillon të lëvrish nëpër dritaret e ndryshme të IDE, duke u përpjekur të kuptosh sekuencën e veprimeve të kodit tënd. Nuk është e vështirë, por ndonjëherë është shumë stresuese.

Nuk ka mbështetje të plotë për ES6.

Mund të ketë edhe disa disavantazhe të tjera, por nuk kam hasur në ndonjë tjetër. Ndani informacion nëse keni përvojë negative në përdorimin e NJS.

Përfundim

NJS është një interpretor open-source i lehtë që lejon realizimin e skenarëve të ndryshëm në Nginx me gjuhën e programimit JavaScript. Gjatë zhvillimit të tij u kushtua shumë vëmendje performancës. Patjetër që i mungon shumë, por projekti po zhvillohet nga një ekip të vogël dhe ata po shtojnë aktivisht karakteristika të reja dhe po rregullojnë gabimet. Shpresoj që ndonjëherë NJS të lejojë lidhjen e moduleve të jashtme, që do ta bënte funksionalitetin e Nginx pothuajse të pakufizuar. Por ekziston NGINX Plus dhe ndonjë karakteristikë ndoshta nuk do të jetë atje!

Repository me kodin e plotë të artikullit

njs-pypi me mbështetje për AWS Sign v4

Përshkrimi i drejtorive të modulit ngx_http_js_module

Repository zyrtare e NJS dhe dokumentacioni

Shembuj të përdorimit të NJS nga Dmitrij Volyntsev

njs - skriptimi nativ i JavaScript në nginx / Выступление Дмитрия Волныева на Saint HighLoad++ 2019

NJS në production / Выступление Василия Сошникова на HighLoad++ 2019

Nënshkrimi dhe autentikimi i kërkesave REST në AWS

Burimi: habr.com

Blini hosting të besueshëm për faqe interneti me mbrojtje nga DDoS, serverë VPS VDS 🔥 Blini hosting të besueshëm për faqe interneti me mbrojtje nga DDoS, serverë VPS VDS | ProHoster