Unë bëra depon time PyPI me autorizim dhe S3. Në Nginx

Në këtë artikull do të doja të ndaj përvojën time me NJS, një përkthyes JavaScript për Nginx i zhvilluar nga Nginx Inc, duke përshkruar aftësitë e tij kryesore duke përdorur një shembull real. NJS është një nëngrup i JavaScript që ju lejon të zgjeroni funksionalitetin e Nginx. Për pyetjen pse perkthyesi yt??? Dmitry Volyntsev u përgjigj në detaje. Me pak fjalë: NJS është nginx-way, dhe JavaScript është më progresiv, "vendas" dhe pa GC, ndryshe nga Lua.

Shumë kohë më parë…

Në punën time të fundit, kam trashëguar gitlab me një numër tubacionesh CI/CD të larmishme me docker-compose, dind dhe kënaqësi të tjera, të cilat u transferuan në kaniko rails. Imazhet që ishin përdorur më parë në CI u zhvendosën në formën e tyre origjinale. Ata funksionuan siç duhet deri në ditën kur IP-ja jonë e gitlab ndryshoi dhe CI u shndërrua në një kungull. Problemi ishte se një nga imazhet e dokerit që mori pjesë në CI kishte git, i cili tërhoqi modulet e Python përmes ssh. Për ssh ju duhet një çelës privat dhe ... ishte në imazh së bashku me known_hosts. Dhe çdo CI dështoi me një gabim verifikimi kyç për shkak të një mospërputhjeje midis IP-së reale dhe asaj të specifikuar në known_hosts. Një imazh i ri u mblodh shpejt nga Dockfiles ekzistues dhe opsioni u shtua StrictHostKeyChecking no. Por shija e keqe mbeti dhe kishte një dëshirë për të zhvendosur libs në një depo private PyPI. Një bonus shtesë, pas kalimit në PyPI private, ishte një tubacion më i thjeshtë dhe një përshkrim normal i kërkesave.txt

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

Ne drejtojmë gjithçka në retë dhe Kubernetes, dhe në fund donim të merrnim një shërbim të vogël që ishte një kontejner pa shtetësi me ruajtje të jashtme. Epo, meqenëse ne përdorim S3, iu dha përparësi. Dhe, nëse është e mundur, me vërtetim në gitlab (mund ta shtoni vetë nëse është e nevojshme).

Një kërkim i shpejtë dha disa rezultate: s3pypi, pypicloud dhe një opsion me krijimin "manual" të skedarëve html për rrepat. Opsioni i fundit u zhduk vetvetiu.

s3pypi: Ky është një kli për përdorimin e pritjes S3. Ne ngarkojmë skedarët, gjenerojmë html dhe e ngarkojmë në të njëjtën kovë. I përshtatshëm për përdorim në shtëpi.

pypicloud: Më dukej një projekt interesant, por pasi lexova dokumentacionin u zhgënjeva. Pavarësisht dokumentacionit të mirë dhe aftësisë për t'u zgjeruar për t'iu përshtatur nevojave tuaja, në realitet doli të ishte i tepërt dhe i vështirë për t'u konfiguruar. Korrigjimi i kodit për t'iu përshtatur detyrave tuaja, sipas vlerësimeve të asaj kohe, do të kishte marrë 3-5 ditë. Shërbimi gjithashtu ka nevojë për një bazë të dhënash. E lamë në rast se nuk gjenim gjë tjetër.

Një kërkim më i thelluar dha një modul për Nginx, ngx_aws_auth. Rezultati i testimit të tij ishte XML i shfaqur në shfletues, i cili tregoi përmbajtjen e kovës S3. Kryerja e fundit në kohën e kërkimit ishte një vit më parë. Depoja dukej e braktisur.

Duke shkuar te burimi dhe duke lexuar PEP-503 Kuptova se XML mund të konvertohet në HTML menjëherë dhe t'i jepet pip. Pasi kërkova pak më shumë rreth Nginx dhe S3, hasa në një shembull të vërtetimit në S3 të shkruar në JS për Nginx. Kështu u njoha me NJS-në.

Duke marrë këtë shembull si bazë, një orë më vonë pashë në shfletuesin tim të njëjtën XML si kur përdorja modulin ngx_aws_auth, por gjithçka ishte shkruar tashmë në JS.

Më pëlqeu shumë zgjidhja nginx. Së pari, dokumentacion i mirë dhe shumë shembuj, së dyti, ne marrim të gjitha të mirat e Nginx për të punuar me skedarë (jashtë kutisë), së treti, kushdo që di të shkruajë konfigurime për Nginx do të jetë në gjendje të kuptojë se çfarë është çfarë. Minimalizmi është gjithashtu një plus për mua, në krahasim me Python ose Go (nëse shkruhet 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 kjo gjë?

Moduli është i ngarkuar në Nginx ngx_http_js_module, i përfshirë në imazhin zyrtar të dokerit. Ne importojmë skriptin tonë duke përdorur direktivën js_importnë konfigurimin Nginx. Funksioni thirret nga një direktivë js_content. Direktiva përdoret për të vendosur variabla js_set, i cili merr si argument vetëm funksionin e përshkruar në skript. Por ne mund të ekzekutojmë nënpyetje në NJS vetëm duke përdorur Nginx, jo ndonjë XMLHttpRequest. Për ta bërë këtë, vendndodhja përkatëse duhet të shtohet në konfigurimin Nginx. Dhe skripti duhet të përshkruajë një nënkërkesë për këtë vendndodhje. Për të qenë në gjendje të aksesoni një funksion nga konfigurimi Nginx, emri i funksionit duhet të eksportohet në vetë skriptin 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}

Kur kërkohet në shfletues http://localhost:8080/ futemi në location /në të cilën direktiva js_content thërret një funksion request përshkruar në skenarin tonë script.js. Nga ana tjetër, në funksion request bëhet një nënpyetje për të location = /sub-query, me një metodë (në shembullin aktual GET) të marrë nga argumenti (r), kalohet në mënyrë implicite kur thirret ky funksion. Përgjigja e nënkërkesës do të përpunohet në funksion call_back.

Po provon S3

Për të bërë një kërkesë për ruajtjen private S3, na duhen:

ACCESS_KEY

SECRET_KEY

S3_BUCKET

Nga metoda e përdorur http, data/ora aktuale, S3_NAME dhe URI, krijohet një lloj i caktuar vargu, i cili nënshkruhet (HMAC_SHA1) duke përdorur SECRET_KEY. Tjetra është një linjë si AWS $ACCESS_KEY:$HASH, mund të përdoret në kokën e autorizimit. E njëjta datë/kohë që është përdorur për të gjeneruar vargun në hapin e mëparshëm duhet të shtohet në kokë 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/(?<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(Shembulli i autorizimit AWS Sign v2, i ndryshuar në status të 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ë shpjegim i vogël për _subrequest_uri: kjo është një variabël që, në varësi të uri fillestare, formon një kërkesë për S3. Nëse keni nevojë të merrni përmbajtjen e "rrënjës", atëherë duhet të krijoni një kërkesë uri që tregon kufirin delimiter, i cili do të kthejë një listë të të gjithë elementëve CommonPrefixes xml, që korrespondojnë me drejtoritë (në rastin e PyPI, një listë e të gjitha paketave). Nëse keni nevojë të merrni një listë të përmbajtjeve në një direktori specifike (një listë me të gjitha versionet e paketës), atëherë kërkesa uri duhet të përmbajë një fushë prefiksi me emrin e drejtorisë (paketës) që domosdoshmërisht përfundon me një të pjerrët /. Përndryshe, përplasjet janë të mundshme kur kërkohen përmbajtjet e një drejtorie, për shembull. Ka direktori aiohttp-kërkesa dhe aiohttp-kërkesa dhe nëse kërkesa specifikon /?prefix=aiohttp-request, atëherë përgjigja do të përmbajë përmbajtjen e të dy drejtorive. Nëse ka një prerje në fund, /?prefix=aiohttp-request/, atëherë përgjigja do të përmbajë vetëm drejtorinë e kërkuar. Dhe nëse kërkojmë një skedar, atëherë uri që rezulton nuk duhet të ndryshojë nga ai origjinal.

Ruani dhe rinisni Nginx. Në shfletuesin ne futim adresën e Nginx-it tonë, rezultati i kërkesës do të jetë XML, për shembull:

Lista e drejtorive

<?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>

Nga lista e drejtorive do t'ju nevojiten vetëm elementet CommonPrefixes.

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

Lista e skedarëve në një direktori

<?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>&#34;00000000000000000000000000000000-1&#34;</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>&#34;b2d76df4aeb4493c5456366748218093&#34;</ETag>
    <Size>93183</Size>
    <Owner>
      <ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID>
      <DisplayName></DisplayName>
    </Owner>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
</ListBucketResult>

Nga lista e skedarëve do të marrim vetëm elementë Key.

Gjithçka që mbetet është të analizoni XML-në që rezulton dhe ta dërgoni atë si HTML, duke zëvendësuar fillimisht kokën e llojit të përmbajtjes me tekst/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="/sq/${reValue.groups.v}">${a_text}</a>`);
    }
  }

  return '<html><body>n' + out.join('</br>n') + 'n</html></body>'
}

Po provon PyPI

Ne kontrollojmë që asgjë të mos prishet askund në paketat që dihet 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 libet 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}"

vërtetim

Në Gitlab është e mundur të përdoret JWT për vërtetimin/autorizimin e shërbimeve të jashtme. Duke përdorur direktivën auth_request në Nginx, ne do t'i ridrejtojmë të dhënat e vërtetimit në një nënkërkesë që përmban një thirrje funksioni në skript. Skripti do të bëjë një nënkërkesë tjetër në url-në e Gitlab dhe nëse të dhënat e vërtetimit janë specifikuar saktë, atëherë Gitlab do të kthejë kodin 200 dhe ngarkimi/shkarkimi i paketës do të lejohet. Pse të mos përdorni një nënpyetje dhe t'i dërgoni menjëherë të dhënat në Gitlab? Sepse atëherë do të na duhet të modifikojmë skedarin e konfigurimit Nginx sa herë që bëjmë ndonjë ndryshim në autorizim, dhe kjo është një detyrë mjaft e lodhshme. Gjithashtu, nëse Kubernetes përdor një politikë të sistemit të skedarëve rrënjë vetëm për lexim, atëherë kjo shton edhe më shumë kompleksitet kur zëvendëson nginx.conf përmes configmap. Dhe bëhet absolutisht e pamundur konfigurimi i Nginx përmes configmap duke përdorur njëkohësisht politika që ndalojnë lidhjen e vëllimeve (pvc) dhe sistemin e skedarëve rrënjë vetëm për lexim (kjo ndodh gjithashtu).

Duke përdorur ndërmjetësin NJS, ne kemi mundësinë të ndryshojmë parametrat e specifikuar në konfigurimin nginx duke përdorur variablat e mjedisit dhe të bëjmë disa kontrolle në skript (për shembull, një URL e specifikuar gabimisht).

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}

Me shumë mundësi po lind pyetja: -Pse të mos përdorni module të gatshme? Gjithçka tashmë është bërë atje! Për shembull, var AWS = kërkon ('aws-sdk') dhe nuk ka nevojë të shkruani një "biçikletë" me vërtetim S3!

Le të kalojmë tek të këqijat

Për mua, pamundësia për të importuar module të jashtme JS u bë një veçori e pakëndshme, por e pritshme. E përshkruar në shembullin e mësipërm, kërkesa ('kripto') është modulet e integruara dhe kërkojnë vetëm punë për ta. Gjithashtu nuk ka asnjë mënyrë për të ripërdorur kodin nga skriptet dhe ju duhet ta kopjoni dhe ngjisni atë në skedarë të ndryshëm. Shpresoj se një ditë ky funksion do të zbatohet.

Kompresimi duhet të çaktivizohet gjithashtu për projektin aktual në Nginx gzip off;

Për shkak se nuk ka modul gzip në NJS dhe është e pamundur ta lidhni atë; prandaj, nuk ka asnjë mënyrë për të punuar me të dhëna të kompresuara. Vërtetë, kjo nuk është me të vërtetë një minus për këtë rast. Nuk ka shumë tekst, dhe skedarët e transferuar tashmë janë të ngjeshur dhe kompresimi shtesë nuk do t'i ndihmojë shumë. Gjithashtu, ky nuk është një shërbim aq i ngarkuar ose kritik që duhet të shqetësoheni me dërgimin e përmbajtjes disa milisekonda më shpejt.

Korrigjimi i skriptit kërkon shumë kohë dhe është i mundur vetëm përmes "printimeve" në error.log. Në varësi të informacionit, paralajmërimit ose gabimit të nivelit të regjistrimit të caktuar, është e mundur të përdoren përkatësisht 3 metoda r.log, r.warn, r.error. Mundohem të korrigjoj disa skripta në Chrome (v8) ose në veglën e konsolës njs, por jo gjithçka mund të kontrollohet atje. Kur korrigjoni kodin, i njohur ndryshe si testimi funksional, historia duket diçka si kjo:

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

dhe mund të ketë qindra sekuenca të tilla.

Shkrimi i kodit duke përdorur nën-pyetje dhe variabla për to kthehet në një lëmsh ​​të ngatërruar. Ndonjëherë filloni të nxitoni nëpër dritare të ndryshme IDE duke u përpjekur të kuptoni sekuencën e veprimeve të kodit tuaj. Nuk është e vështirë, por ndonjëherë është shumë e bezdisshme.

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

Mund të ketë disa mangësi të tjera, por nuk kam hasur asgjë tjetër. Ndani informacione nëse keni përvojë negative duke përdorur NJS.

Përfundim

NJS është një interpretues i lehtë me burim të hapur që ju lejon të implementoni skripte të ndryshme JavaScript në Nginx. Gjatë zhvillimit të tij, vëmendje e madhe i është kushtuar performancës. Natyrisht, ka ende shumë që mungojnë, por projekti po zhvillohet nga një ekip i vogël dhe ata po shtojnë në mënyrë aktive veçori të reja dhe po rregullojnë gabimet. Shpresoj që një ditë NJS do t'ju lejojë të lidhni module të jashtme, të cilat do ta bëjnë funksionalitetin Nginx pothuajse të pakufizuar. Por ka NGINX Plus dhe ka shumë të ngjarë që nuk do të ketë veçori!

Depo me kod të plotë për artikullin

njs-pypi me mbështetje AWS Sign v4

Përshkrimi i direktivave të modulit ngx_http_js_module

Depoja zyrtare e NJS и dokumentacionin

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

njs - skriptimi vendas JavaScript në nginx / Fjalimi nga Dmitry Volnyev në Saint HighLoad++ 2019

NJS në prodhim / Fjalimi i Vasily Soshnikov në HighLoad++ 2019

Nënshkrimi dhe vërtetimi i kërkesave për REST në AWS

Burimi: www.habr.com