Nilitengeneza hazina yangu ya PyPI kwa idhini na S3. Kwenye Nginx

Katika makala hii ningependa kushiriki uzoefu wangu na NJS, mkalimani wa JavaScript kwa Nginx iliyotengenezwa na Nginx Inc, akielezea uwezo wake mkuu kwa kutumia mfano halisi. NJS ni sehemu ndogo ya JavaScript inayokuruhusu kupanua utendakazi wa Nginx. Kwa swali kwanini mkalimani wako mwenyewe??? Dmitry Volyntsev alijibu kwa undani. Kwa kifupi: NJS ni nginx-njia, na JavaScript inaendelea zaidi, "asili" na bila GC, tofauti na Lua.

Muda mrefu uliopita…

Katika kazi yangu ya mwisho, nilirithi gitlab na bomba kadhaa za motley CI/CD zilizo na docker-compose, dind na starehe zingine, ambazo zilihamishiwa kwenye reli za kaniko. Picha ambazo zilitumiwa hapo awali katika CI zilihamishwa katika umbo lao asili. Walifanya kazi ipasavyo hadi siku ambayo gitlab IP yetu ilibadilika na CI ikageuka kuwa malenge. Shida ilikuwa kwamba moja ya picha za docker ambazo zilishiriki katika CI zilikuwa na git, ambayo ilivuta moduli za Python kupitia ssh. Kwa ssh unahitaji ufunguo wa faragha na... ilikuwa kwenye picha pamoja na known_hosts. Na CI yoyote imeshindwa na hitilafu kuu ya uthibitishaji kwa sababu ya kutolingana kati ya IP halisi na ile iliyobainishwa katika wapashi wanaojulikana. Picha mpya ilikusanywa haraka kutoka kwa Faili zilizopo za Dock na chaguo liliongezwa StrictHostKeyChecking no. Lakini ladha mbaya ilibaki na kulikuwa na hamu ya kuhamisha libs kwenye hazina ya kibinafsi ya PyPI. Bonasi ya ziada, baada ya kubadili PyPI ya kibinafsi, ilikuwa bomba rahisi na maelezo ya kawaida ya mahitaji.txt

Uchaguzi umefanywa, Waheshimiwa!

Tunaendesha kila kitu katika mawingu na Kubernetes, na mwisho tulitaka kupata huduma ndogo ambayo ilikuwa chombo kisicho na uraia na hifadhi ya nje. Kweli, kwa kuwa tunatumia S3, kipaumbele kilipewa. Na, ikiwezekana, na uthibitishaji katika gitlab (unaweza kuiongeza mwenyewe ikiwa ni lazima).

Utafutaji wa haraka ulitoa matokeo kadhaa: s3pypi, pypicloud na chaguo na uundaji wa "mwongozo" wa faili za html kwa turnips. Chaguo la mwisho lilitoweka peke yake.

s3pypi: Hii ni cli kwa kutumia S3 hosting. Tunapakia faili, kutengeneza html na kuipakia kwenye ndoo sawa. Inafaa kwa matumizi ya nyumbani.

pypicloud: Ilionekana kama mradi wa kupendeza, lakini baada ya kusoma hati nilikatishwa tamaa. Licha ya nyaraka nzuri na uwezo wa kupanua ili kukidhi mahitaji yako, kwa kweli iligeuka kuwa ya ziada na ngumu kusanidi. Kurekebisha msimbo ili kuendana na kazi zako, kulingana na makadirio ya wakati huo, kungechukua siku 3-5. Huduma pia inahitaji hifadhidata. Tuliiacha ikiwa hatujapata kitu kingine chochote.

Utafutaji wa kina zaidi ulitoa moduli ya Nginx, ngx_aws_auth. Matokeo ya majaribio yake yalikuwa XML iliyoonyeshwa kwenye kivinjari, ambayo ilionyesha yaliyomo kwenye ndoo ya S3. Ahadi ya mwisho wakati wa utaftaji ilikuwa mwaka mmoja uliopita. Hifadhi ilionekana kutelekezwa.

Kwa kwenda kwenye chanzo na kusoma PEP-503 Niligundua kuwa XML inaweza kubadilishwa kuwa HTML kwenye kuruka na kupewa bomba. Baada ya kuvinjari zaidi kuhusu Nginx na S3, nilikutana na mfano wa uthibitishaji katika S3 ulioandikwa kwa JS kwa Nginx. Hivyo ndivyo nilivyokutana na NJS.

Kwa kuchukua mfano huu kama msingi, saa moja baadaye niliona kwenye kivinjari changu XML sawa na wakati wa kutumia ngx_aws_auth moduli, lakini kila kitu kilikuwa kimeandikwa tayari katika JS.

Nilipenda sana suluhisho la nginx. Kwanza, nyaraka nzuri na mifano mingi, pili, tunapata mazuri yote ya Nginx kwa kufanya kazi na faili (nje ya boksi), tatu, mtu yeyote anayejua jinsi ya kuandika configs kwa Nginx ataweza kujua ni nini. Minimalism pia ni faida kwangu, ikilinganishwa na Python au Go (ikiwa imeandikwa kutoka mwanzo), bila kutaja nexus.

TL;DR Baada ya siku 2, toleo la majaribio la PyPi lilikuwa tayari kutumika katika CI.

Jinsi gani kazi?

Moduli imepakiwa kwenye Nginx ngx_http_js_module, iliyojumuishwa kwenye picha rasmi ya kizimbani. Tunaingiza hati yetu kwa kutumia maagizo js_importkwa usanidi wa Nginx. Kazi inaitwa na maagizo js_content. Maagizo hutumiwa kuweka vigezo js_set, ambayo huchukua kama hoja tu kazi iliyoelezwa kwenye hati. Lakini tunaweza kutekeleza maswali madogo katika NJS kwa kutumia Nginx pekee, sio XMLHttpRequest yoyote. Kwa kufanya hivyo, eneo linalofanana lazima liongezwe kwenye usanidi wa Nginx. Na hati lazima ieleze ombi dogo kwa eneo hili. Ili kuweza kufikia chaguo za kukokotoa kutoka kwa usanidi wa Nginx, jina la chaguo la kukokotoa lazima lisafirishwe kwenye hati yenyewe 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}

Unapoombwa kwenye kivinjari http://localhost:8080/ tunaingia location /ambayo maagizo js_content huita kazi request ilivyoelezwa katika hati yetu script.js. Kwa upande wake, katika kazi request subquery inafanywa location = /sub-query, na njia (katika mfano wa sasa GET) iliyopatikana kutoka kwa hoja (r), ilipitishwa bila kuficha wakati chaguo hili la kukokotoa linapoitwa. Jibu la ombi dogo litachakatwa katika chaguo la kukokotoa call_back.

Kujaribu S3

Ili kutuma ombi la hifadhi ya kibinafsi ya S3, tunahitaji:

ACCESS_KEY

SECRET_KEY

S3_BUCKET

Kutoka kwa mbinu ya http iliyotumika, tarehe/saa ya sasa, S3_NAME na URI, aina fulani ya mfuatano inatolewa, ambayo imetiwa sahihi (HMAC_SHA1) kwa kutumia SECRET_KEY. Ifuatayo ni mstari kama AWS $ACCESS_KEY:$HASH, inaweza kutumika katika kichwa cha uidhinishaji. Tarehe/saa ile ile ambayo ilitumika kutengeneza mfuatano katika hatua ya awali lazima iongezwe kwenye kichwa X-amz-date. Kwa nambari inaonekana kama hii:

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(Mfano wa idhini ya AWS Sign v2, umebadilishwa hadi hali iliyoacha kutumika)

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}

Maelezo kidogo kuhusu _subrequest_uri: hii ni tofauti ambayo, kulingana na uri ya awali, huunda ombi kwa S3. Ikiwa unahitaji kupata yaliyomo kwenye "mizizi", basi unahitaji kuunda ombi la uri linaloonyesha delimiter. delimiter, ambayo itarudisha orodha ya vipengele vyote vya CommonPrefixes xml, sambamba na saraka (kwa upande wa PyPI, orodha ya vifurushi vyote). Ikiwa unahitaji kupata orodha ya yaliyomo kwenye saraka maalum (orodha ya matoleo yote ya kifurushi), basi ombi la uri lazima liwe na uwanja wa kiambishi awali na jina la saraka (kifurushi) ambacho lazima kiishie na kufyeka /. Vinginevyo, migongano inawezekana wakati wa kuomba yaliyomo kwenye saraka, kwa mfano. Kuna saraka aiohttp-ombi na aiohttp-maombi na ikiwa ombi linabainisha /?prefix=aiohttp-request, basi jibu litakuwa na yaliyomo kwenye saraka zote mbili. Ikiwa kuna kufyeka mwishoni, /?prefix=aiohttp-request/, basi jibu litakuwa na saraka inayohitajika tu. Na ikiwa tunaomba faili, basi uri inayosababisha haipaswi kutofautiana na ile ya awali.

Hifadhi na uanze tena Nginx. Katika kivinjari tunaingiza anwani ya Nginx yetu, matokeo ya ombi yatakuwa XML, kwa mfano:

Orodha ya saraka

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

Kutoka kwenye orodha ya saraka utahitaji tu vipengele CommonPrefixes.

Kwa kuongeza saraka tunayohitaji kwa anwani yetu kwenye kivinjari, tutapokea pia yaliyomo katika fomu ya XML:

Orodha ya faili kwenye saraka

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

Kutoka kwenye orodha ya faili tutachukua vipengele pekee Key.

Kilichobaki ni kuchanganua XML inayotokana na kuituma kama HTML, baada ya kwanza kubadilisha kichwa cha Aina ya Maudhui na maandishi/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="/sw/${reValue.groups.v}">${a_text}</a>`);
    }
  }

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

Kujaribu PyPI

Tunaangalia kuwa hakuna kitu kinachovunjika popote kwenye vifurushi vinavyojulikana kufanya kazi.

# Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ для тСстов Π½ΠΎΠ²ΠΎΠ΅ ΠΎΠΊΡ€ΡƒΠΆΠ΅Π½ΠΈΠ΅
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

Tunarudia na libs zetu.

# Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ для тСстов Π½ΠΎΠ²ΠΎΠ΅ ΠΎΠΊΡ€ΡƒΠΆΠ΅Π½ΠΈΠ΅
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

Katika CI, kuunda na kupakia kifurushi inaonekana kama hii:

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

Uthibitishaji

Katika Gitlab inawezekana kutumia JWT kwa uthibitishaji/uidhinishaji wa huduma za nje. Kwa kutumia maagizo ya auth_request katika Nginx, tutaelekeza upya data ya uthibitishaji kwa ombi dogo lililo na simu ya kukokotoa katika hati. Hati itafanya ombi lingine dogo kwa url ya Gitlab na ikiwa data ya uthibitishaji ilibainishwa kwa usahihi, basi Gitlab itarudisha msimbo 200 na upakiaji/upakuaji wa kifurushi utaruhusiwa. Kwa nini usitumie subquery moja na kutuma data mara moja kwa Gitlab? Kwa sababu basi tutalazimika kuhariri faili ya usanidi wa Nginx kila wakati tunapofanya mabadiliko yoyote katika idhini, na hii ni kazi ya kuchosha. Pia, ikiwa Kubernetes hutumia sera ya mfumo wa faili wa kusoma tu, basi hii inaongeza ugumu zaidi wakati wa kuchukua nafasi ya nginx.conf kupitia usanidi. Na inakuwa haiwezekani kabisa kusanidi Nginx kupitia usanidi wakati huo huo ukitumia sera zinazokataza unganisho la kiasi (pvc) na mfumo wa faili wa kusoma pekee (hii pia hufanyika).

Kwa kutumia NJS ya kati, tunapata fursa ya kubadilisha vigezo maalum katika usanidi wa nginx kwa kutumia vigezo vya mazingira na kufanya ukaguzi fulani kwenye hati (kwa mfano, URL isiyo sahihi).

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}

Uwezekano mkubwa zaidi swali linatengenezwa: -Kwa nini usitumie moduli zilizotengenezwa tayari? Kila kitu tayari kimefanyika hapo! Kwa mfano, var AWS = need('aws-sdk') na hakuna haja ya kuandika "baiskeli" iliyo na uthibitishaji wa S3!

Hebu tuendelee kwenye hasara

Kwangu, kutokuwa na uwezo wa kuagiza moduli za JS za nje ikawa jambo lisilofurahisha, lakini linalotarajiwa. Imefafanuliwa katika mfano hapo juu need('crypto') ni moduli za kujengwa na zinahitaji kazi kwao tu. Pia hakuna njia ya kutumia tena nambari kutoka kwa hati na lazima uinakili na ubandike kwenye faili tofauti. Natumai kuwa siku moja utendakazi huu utatekelezwa.

Ukandamizaji lazima pia uzime kwa mradi wa sasa wa Nginx gzip off;

Kwa sababu hakuna moduli ya gzip katika NJS na haiwezekani kuiunganisha; kwa hivyo, hakuna njia ya kufanya kazi na data iliyoshinikwa. Kweli, hii sio minus kwa kesi hii. Hakuna maandishi mengi, na faili zilizohamishwa tayari zimesisitizwa na ukandamizaji wa ziada hautawasaidia sana. Pia, hii si huduma iliyopakiwa au muhimu kiasi kwamba unapaswa kujisumbua na kutoa maudhui kwa milisekunde chache haraka.

Kutatua hati huchukua muda mrefu na kunawezekana tu kupitia "prints" katika error.log. Kulingana na maelezo ya kiwango cha kuweka kumbukumbu, onya au kosa, inawezekana kutumia mbinu 3 r.log, r.warn, r.error kwa mtiririko huo. Ninajaribu kurekebisha maandishi kadhaa kwenye Chrome (v8) au zana ya koni ya njs, lakini sio kila kitu kinaweza kukaguliwa hapo. Wakati wa kurekebisha msimbo, aka majaribio ya kufanya kazi, historia inaonekana kama hii:

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

na kunaweza kuwa na mamia ya mlolongo kama huo.

Msimbo wa uandishi kwa kutumia maswali madogo na vigeuzo kwao hubadilika kuwa tangle iliyochanganyika. Wakati mwingine unaanza kukimbilia madirisha tofauti ya IDE kujaribu kubaini mlolongo wa vitendo vya nambari yako. Sio ngumu, lakini wakati mwingine inakera sana.

Hakuna usaidizi kamili kwa ES6.

Kunaweza kuwa na mapungufu mengine, lakini sijakutana na kitu kingine chochote. Shiriki maelezo ikiwa una uzoefu mbaya wa kutumia NJS.

Hitimisho

NJS ni mkalimani mwepesi wa chanzo-wazi ambao hukuruhusu kutekeleza hati mbalimbali za JavaScript katika Nginx. Wakati wa maendeleo yake, tahadhari kubwa ililipwa kwa utendaji. Bila shaka, bado kuna mengi yanayokosekana, lakini mradi huo unaendelezwa na timu ndogo na wanaongeza kikamilifu vipengele vipya na kurekebisha mende. Natumai kuwa siku moja NJS itakuruhusu kuunganisha moduli za nje, ambazo zitafanya utendaji wa Nginx kuwa karibu ukomo. Lakini kuna NGINX Plus na uwezekano mkubwa hakutakuwa na vipengele!

Hazina yenye msimbo kamili wa makala

njs-pypi na usaidizi wa AWS Sign v4

Maelezo ya maagizo ya moduli ya ngx_http_js_moduli

Hazina rasmi ya NJS ΠΈ nyaraka

Mifano ya kutumia NJS kutoka kwa Dmitry Volintsev

njs - uandishi asilia wa JavaScript katika nginx / Hotuba ya Dmitry Volnyev katika Saint HighLoad++ 2019

NJS katika uzalishaji / Hotuba ya Vasily Soshnikov katika HighLoad++ 2019

Kusaini na Kuthibitisha Maombi ya REST katika AWS

Chanzo: mapenzi.com