Kam krijuar depozitarin tim PyPI me autorizim dhe S3. Në Nginx

Në këtë artikull, dua të ndaj përvojën time me NJS, interpretorin JavaScript për Nginx i zhvilluar nga kompanitë Nginx Inc, duke përshkruar në një shembull të vërtetë mundësitë e tij kryesore. NJS është një nënset i gjuhës programuese JavaScript që lejon zgjerimin e funksionalitetit të Nginx. Për pyetjen pse një interpretor të tillë??? e shpjegoi në detaje Dmitry Volyntsev. Nëse ta themi shkurt: NJS është mënyra Nginx, ndërsa JavaScript është më progresiv, "vendës" dhe pa GC krahasuar me Lua.

Një herë e një kohë…

Në punën time të mëparshme, trashëgova një gitlab me një numër të caktuar pipeline të ndryshëm CI/CD me docker-compose, dind dhe gjërat e tjera të bukura, të cilat u transmetuan në kaniko. Imazhet që ishin përdorur më parë në CI, u transferuan në formën e tyre të origjinës. Ato funksionuan mirë deri në atë ditë, kur gitlab-i ynë ndryshoi IP dhe CI u shndërrua në një të papërdorshme. Problemi ishte se në njërin nga docker-imazhet, i përfshirë në CI, ishte git, i cili përmes ssh tërhiqte modulet Python. Për ssh nevojitet një çelës privat dhe… ai ishte në imazh së bashku me known_hosts. Çdo CI përfundonte me një gabim në verifikimin e çelësit për shkak të përputhjes së papërshtatshëm mes IP-së reale dhe asaj të cituar në known_hosts. Nga Dockerfile-t e disponueshëm, një imazh i ri u ndërtua shpejt dhe u shtua opsioni StrictHostKeyChecking no. Por një shije e pakëndshme mbeti dhe u shfaq dëshira për të transferuar bibliotekat në një depo private PyPI. Një bonus i shtuar, pas kalimit në PyPI private, ishte një pipeline më i thjeshtë dhe një përshkrim normal i requirements.txt.

Zgjedhja është bërë, Zoti!

Ne përqendrohemi në mjete dhe Kubernetes, dhe përfundimisht doja të merrja një shërbim të vogël që përbënte një kontejner stateless me një depo të jashtme. Dhe pasi përdorim S3, prioriteti ishte për të. Po ashtu, ndihmoi autentifikimi në gitlab (mund të shkruhet vetë nëse është e nevojshme).

Një kërkim i shpejtë dha disa rezultate s3pypi, pypicloud dhe një variant me krijimin "manual" të skedarëve html për depo. Varianti i fundit u hoq menjëherë.

s3pypi: Kjo është një cli për përdorimin e hostimit në S3. Ngarkojmë skedarët, gjenerojmë html dhe ngarkojmë në të njëjtën vaskë. Për përdorim shtëpiak është i përshtatshëm.

pypicloud: U duk si një projekt interesant, por pas leximit të dokumentacionit erdhi një zhgënjim. Pavarësisht dokumentacionit të mirë dhe mundësive të zgjerimit për detyrat tuaja, në fakt doli të ishte tejet i tejkaluar dhe i ndërlikuar në konfigurim. Të korrigjoje kodin për nevojat e tua, sipas llogaritjeve të atëhershme, do të merrte 3-5 ditë. Po ashtu, shërbimi kërkonte një DB. E lanë atë për çfarëdo rasti, nëse nuk gjejmë asgjë tjetër.

Një kërkim më i thelluar dha një modul për Nginx, ngx_aws_auth. Rezultati i testimit të tij ishte një XML i shfaqur në shfletues, ku mund të shihej përmbajtja e vaskës S3. Komenti i fundit, në momentin e kërkimit, kishte qenë një vit më parë. Depo dukej e braktisur.

Duke u drejtuar te burimi i parë dhe duke lexuar PEP-503 kuptova që XML mund të konvertohet në HTML në fluturim dhe t'i jepet pip. Pasi të kisha kërkuar pak më shumë për fjalët Nginx dhe S3, pashë një shembull të autentikimit në S3 të shkruar në JS për Nginx. Kështu e njoha NJS.

Duke marrë si bazë këtë shembull, pas një ore vëzhgova në shfletuesin tim të njëjtin XML si me modulin ngx_aws_auth, por tashmë ishte shkruar gjithçka në JS.

Më pëlqente shumë zgjidhja në nginx. Së pari, ka dokumentacion të mirë dhe shumë shembuj, së dyti, ne marrim të gjitha avantazhet e Nginx për punën me skedarët (nga kutia), së treti, çdo njeri që di të shkruajë konfigurations për Nginx, mund të kuptojë se si qëndron puna. Gjithashtu, minimalizmi është një avantazh për mua, krahasuar me Python ose Go (nëse shkruajmë nga e para), për të mos përmendur nexus.

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

Si funksionon?

Në Nginx, ngarkohet moduli ngx_http_js_module, i përfshirë në imazhin zyrtar docker. Importojmë skriptin tonë përmes direktivës js_importnë konfigurimin e Nginx. Thirrja e funksionit bëhet përmes direktivës js_content. Përdorimi i variablave bëhet përmes direktivës js_set, e cila si argument merr vetëm funksionin e përshkruar në skript. Ndërsa, për të kryer nënkërkesat në NJS, ne mund të bëjmë vetëm me ndihmën e Nginx, pa XMLHTTPRequest. Për këtë, në konfigurimin e Nginx duhet të shtohet vendndodhja përkatëse. Në skript duhet të përshkruhet një nënkërkesë (subrequest) për këtë vendndodhje. Që të kemi mundësi të thërrasim funksionin nga konfigurimi i Nginx, në vetë skriptin duhet të eksportojmë emrin e funksionit 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) {
    // kodi i menaxherit
    r.return(resp.status, resp.responseBody);
  }

  r.subrequest('/sub-query', { method: r.method }, call_back);
}

export default {request}

Kur bëjmë një kërkesë në shfletuesin http://localhost:8080/ ne arrijmë në location /në të cilin direktiva js_content thërret funksionin ) me të dhënat. Ja një lidhje që mund ta vendosni thjesht në fushën e shfletuesit dhe pastaj të rikrijoni faqen (kjo duhet bërë vetëm një herë): e përshkruar në skriptin tonë script.js. Nga ana e saj, funkcioni ) me të dhënat. Ja një lidhje që mund ta vendosni thjesht në fushën e shfletuesit dhe pastaj të rikrijoni faqen (kjo duhet bërë vetëm një herë): bëhet një nënkërkesë në location = /sub-query, me metodën (në këtë shembull GET) e marrë nga argumenti (r), e cila i kalon implicit kur thërrasim këtë funksion. Përpunimi i përgjigjes së nënkërkesës do të kryhet në funksionin call_back.

Provojmë S3

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

ACCESS_KEY

SECRET_KEY

S3_BUCKET

Nga metoda http të përdorur, data/dita aktuale, S3_NAME dhe URI, gjenerohet një varg i caktuar që nënshkruhet (HMAC_SHA1) me ndihmën e SECRET_KEY. Më pas, vargu i tillë, AWS $ACCESS_KEY:$HASH, mund të përdoret në headerin e autorizimit. E njëjta datë/ditë, e cila u përdor për të gjeneruar vargun në hapin e mëparshëm, duhet të shtohet në headerin X-amz-date. Në kod 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(shembuj autorizimi AWS Sign v2, i transformuar në status të vjetruar)

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}

Pak shpjegim rreth _subrequest_uri: kjo është një variabël që formon kërkesën për S3 në varësi të uri-t fillestar. Nëse nevojitet të fitojmë përmbajtjen e "rrënjës", në këtë rast duhet të formojmë uri-kërkesë me specifikimin e ndarësit delimiter, e cila do të kthejë listën e të gjithë elementeve xml CommonPrefixes, që përputhen me direktorët (në rastin e PyPI, lista e të gjitha paketave). Nëse duam të fitojmë një listë të përmbajtjes në një drejtor të caktuar (lista e të gjitha versioneve të paketave), atëherë uri-kërkesë duhet të përmbajë fushën prefix me emrin e direktorive (paketës) që përfundon patjetër me ndarësin \/. Në të kundërt, mund të ndodhin kolizione në kërkesën për përmbajtjen e direktorive, për shembull. Ka direktorë aiohttp-request dhe aiohttp-requests dhe nëse në kërkesë përmendet /?prefix=aiohttp-request, atëherë në përgjigje do të ketë përmbajtjen e të dy direktorëve. Nëse përkundrazi, në fund ka një ndarës, /?prefix=aiohttp-request/, atëherë në përgjigje do të jetë vetëm direktoria e nevojshme. Dhe nëse po kërkojmë një skedar, atëherë uri-fundor nuk duhet të ndryshojë nga uri-i fillestar.

Ruajmë, rinisnim 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 do të nevojiten vetëm elementet CommonPrefixes.

Duke shtuar, në shfletues, drejtorinë që na nevojitet, do të fitojmë përmbajtjen e saj gjithashtu në formën e XML:

Lista e skedave në direktor

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 Key.

Tani duhet të analizojmë XML-në e marrë dhe ta kthejmë si HTML, duke zëvendësuar fillimisht titullin Content-Type me 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>'
}

Po provojmë PyPI

Kontrollojmë që asgjë dhe askund të mos prishet në paketat që janë dëshmuar se funksionojnë.

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

E përsërisim me bibliotekat tona.

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

Autentifikimi

Në Gitlab është e mundur të përdoren JWT për autentikimin/autorizimin e shërbimeve të jashtme. Duke përdorur direktivën auth_request në Nginx, ne do të drejtojmë të dhënat e autentikimit në një nënbisedë që përmban thirrjen e funksionit në skenar. Në skenar do të ketë një nënbisedë tjetër në url-në e Gitlab-it dhe nëse të dhënat e autentikimit janë dhënë saktë, atëherë Gitlab do të kthejë kodin 200 dhe do të lejojë ngarkimin/shkarkimin e paketës. Pse të mos përdorim një nënbisedë dhe menjëherë të dërgojmë të dhënat në Gitlab? Sepse do të duhet të ndryshojmë çdo herë skedarin e konfigurimit të Nginx, kur të kemi ndryshime në autorizim, dhe kjo është një detyrë mjaft e ndërlikuar. Po ashtu, nëse në Kubernetes përdoret politika e sistemit të filesit të lexueshëm, atëherë kjo e shton edhe më shumë vështirësitë kur ndërroni nginx.conf përmes configmap. Dhe bëhet plotësisht e pamundur konfigurimi i Nginx përmes configmap kur përdoren paralelisht politika që ndalojnë lidhet e vëllimeve (pvc) dhe sistemi i filesit të lexueshëm (kjo ndodh gjithashtu).

Duke përdorur NJS si një shkallë ndërmjetëse, ne marrim mundësinë të ndryshojmë parametrat e caktuar në konfigurimin e nginx me ndihmën e variablave të ambientit dhe të kryejmë disa kontrolle në skenar (për shembull, nëse URL është dhënë gabim).

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}

Me siguri po lind pyetja: - Pse të mos përdorim modulet e gatshme? Aty është bërë gjithçka! Për shembull, var AWS = require(‘aws-sdk’) dhe nuk është e nevojshme të shkruajmë ‘bicikletë’ me autentikimin e S3!

Le të kalojmë te disavantazhet

Për mua, pamundësia për të importuar modulet e jashtme JS ishte një karakteristikë e pakëndshme, por e pritur. Ai që është përshkruar 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ërdorësh kodin nga skenarët dhe duhet ta kopjosh dhe ngjitesh në skedarë të ndryshëm. Shpresoj se një ditë ky funksionalitet do të realizohet.

Po ashtu, për projektin aktual në Nginx duhet të jetë e çaktivizuar kompresimi gzip off;

Për shkak se nuk ka modul gzip në NJS dhe nuk mund të lidhesh, nuk ka asnjë mundësi për të punuar me të dhëna të kompresuara. Megjithatë, kjo nuk është dukshëm një disavantazh për këtë rast. Teksti nuk është shumë, dhe skedarët që dërgohen janë tashmë të kompresuar, kështu që kompresimi shtesë nuk do të ndihmonte shumë. Po ashtu, ky është një shërbim që nuk është aq i ngarkuar apo kritik sa të shqetësohemi për dorëzimin e përmbajtjes disa milisekonda më shpejt.

Debugging i skripti është i gjatë dhe mund të bëhet vetëm përmes "print-eve" në error.log. Në varësi të nivelit të regjistrimit të vendosur info, warn ose error, është e mundur të përdoren 3 metoda r.log, r.warn, r.error përkatësisht. Disa skripte përpiqem t'i debugg në Chrome (v8) ose në mjetin e konsolës njs, por nuk është gjithçka e mundur të kontrollohet atje. Gjatë debugimit të kodit, që quhet testim funksional, historia duket kështu:

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

dhe mund të ketë qindra të tilla.

Shkrimi i kodit duke përdorur nënkërkesa dhe variabla për to, shndërrohet në një grumbull të komplikuar. Ndonjëherë fillon të studiohesh nëpër dritare të ndryshme të IDE-së për të kuptuar rendin e veprimeve të kodit tuaj. 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ë asgjë tjetër. Ndani informacionin nëse keni përvojë negative me NJS.

Përfundimi

NJS është një interpretuese open-source e lehtë që lejon implementimin e skenarëve të ndryshëm në Nginx në gjuhën JavaScript. Gjatë zhvillimit të tij, u kushtua shumë vëmendje performancës. Sigurisht, ka shumë gjëra që i mungojnë, por projekti po zhvillohet nga një ekip të vogël dhe ata aktivisht shtojnë karakteristika të reja dhe rregullojnë dëmet. Shpresoj që ndonjëherë NJS të lejojë lidhjen e moduleve të jashtme, gjë që do ta bënte funksionalitetin e Nginx pothuajse të pakufizuar. Por ekziston NGINX Plus dhe ndonjë nga karakteristikat ndoshta nuk do të jenë atje!

Repo 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

Repo zyrtare e NJS dhe dokumentacija

Shembuj të përdorimit të NJS nga Dmitry Volyncev

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

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

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

Burimi: habr.com

Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS 🔥 Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS | ProHoster