Nire PyPI biltegia egin nuen baimenarekin eta S3rekin. Nginx-en

Artikulu honetan NJS-ekin dudan esperientzia partekatu nahiko nuke, Nginx Inc-ek garatutako Nginx-erako JavaScript interpretearekin, bere gaitasun nagusiak adibide erreal bat erabiliz deskribatuz. NJS Nginx-en funtzionaltasuna zabaltzeko aukera ematen duen JavaScript azpimultzo bat da. Galderara zergatik zure interpretea??? Dmitri Volyntsevek erantzun zuen zehatz-mehatz. Laburbilduz: NJS nginx modukoa da, eta JavaScript aurrerakorragoa da, "natiboa" eta GCrik gabea, Lua ez bezala.

Aspaldi…

Nire azken lanean, gitlab heredatu nuen docker-compose, dind eta beste gozagarri batzuekin CI/CD kanalizazio mota ugarirekin, kaniko errailetara transferitu zirenak. Lehen CI-n erabiltzen ziren irudiak jatorrizko forman mugitu ziren. Ondo funtzionatu zuten gure gitlab IPa aldatu eta CI kalabaza bihurtu zen egunera arte. Arazoa zen CI-n parte hartu zuen docker irudietako batek git zuela, eta horrek Python moduluak ssh bidez ateratzen zituen. Sshrako gako pribatu bat behar duzu eta... irudian zegoen known_hosts-ekin batera. Eta edozein CIk huts egin du gakoen egiaztapen-errore batekin, benetako IParen eta known_hosts-en zehaztutakoaren arteko desegokitzeagatik. Irudi berri bat azkar muntatu zen lehendik zeuden Dockfiles-etatik eta aukera gehitu zen StrictHostKeyChecking no. Baina gustu txarra geratu zen eta liburuak PyPI biltegi pribatu batera eramateko nahia zegoen. Hobari gehigarri bat, PyPI pribatura aldatu ondoren, kanalizazio sinpleagoa eta requirements.txt-en deskribapen normala zen.

Hautua egin da, jaunak!

Hodeietan eta Kubernetes-en dena exekutatzen dugu, eta, azkenean, kanpoko biltegiratzearekin estaturik gabeko edukiontzi bat zen zerbitzu txiki bat lortu nahi genuen. Tira, S3 erabiltzen dugunez, lehentasuna eman zitzaion. Eta, ahal bada, gitlab-en autentifikazioarekin (beharrezkoa izanez gero, zuk zeuk gehi dezakezu).

Bilaketa azkar batek hainbat emaitza eman zituen: s3pypi, pypicloud eta arbientzako html fitxategiak "eskuz" sortzeko aukera bat. Azken aukera berez desagertu zen.

s3pypi: S3 hostinga erabiltzeko cli bat da. Fitxategiak igotzen ditugu, html-a sortzen dugu eta ontzi berean igotzen dugu. Etxean erabiltzeko egokia.

pypicloud: Proiektu interesgarria iruditu zitzaidan, baina dokumentazioa irakurri ondoren etsita geratu nintzen. Dokumentazio ona eta zure beharretara egokitzeko zabaltzeko gaitasuna izan arren, errealitatean erredundantea eta konfiguratzen zaila izan zen. Kodea zure zereginetara egokitzeko, orduko kalkuluen arabera, 3-5 egun beharko lirateke. Zerbitzuak datu-base bat ere behar du. Beste ezer aurkitzen ez bagenuen utzi genuen.

Bilaketa sakonago batek Nginx-entzako modulu bat lortu zuen, ngx_aws_auth. Bere proben emaitza nabigatzailean bistaratzen den XML izan zen, eta horrek S3 ontziaren edukia erakusten zuen. Bilaketa garaiko azken konpromisoa duela urtebete izan zen. Biltegiak abandonatuta zirudien.

Iturburura joan eta irakurriz PEP-503 Ohartu nintzen XML HTML bihur daitekeela hegan eta pip-era eman. Nginx eta S3-ri buruz apur bat gehiago Googlen ibili ondoren, Nginx-erako JS-n idatzitako S3-n autentifikazioaren adibide bat topatu nuen. Horrela ezagutu nuen NJS.

Adibide hau oinarritzat hartuta, ordubete beranduago nire arakatzailean ngx_aws_auth modulua erabiltzean XML bera ikusi nuen, baina dena JSn idatzita zegoen jada.

Asko gustatu zait nginx irtenbidea. Lehenik eta behin, dokumentazio ona eta adibide asko, bigarrenik, Nginx-en ontasun guztiak fitxategiekin lan egiteko (kutxatik kanpo), hirugarrenik, Nginx-en konfigurazioak idazten dakienak zer den jakin ahal izango du. Minimalismoa ere abantaila bat da niretzat, Python edo Gorekin alderatuta (hutsetik idatzita badago), zer esanik ez nexus.

TL;DR 2 egun igaro ondoren, PyPi-ren probako bertsioa jada erabiltzen zen CIn.

Nola funtzionatzen du?

Modulua Nginx-en kargatzen da ngx_http_js_module, docker irudi ofizialean sartuta. Zuzentaraua erabiliz gure script-a inportatzen dugu js_importNginx konfiguraziora. Zuzentarau batek deitzen dio funtzioari js_content. Zuzentaraua aldagaiak ezartzeko erabiltzen da js_set, argumentu gisa scriptean deskribatutako funtzioa soilik hartzen duena. Baina NJS-n azpikontsultak Nginx erabiliz soilik exekutatu ditzakegu, ez edozein XMLHttpRequest. Horretarako, dagokion kokapena gehitu behar zaio Nginx konfigurazioari. Eta gidoiak kokapen honetarako azpieskaera bat deskribatu behar du. Nginx konfiguraziotik funtzio bat atzitu ahal izateko, funtzioaren izena scriptean bertan esportatu behar da 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}

Arakatzailean eskatuta http://localhost:8080/ sartzen gara location /zeinetan zuzentaraua js_content funtzio bati deitzen dio request gure gidoian azaltzen da script.js. Aldiz, funtzioan request azpikontsulta bat egiten zaio location = /sub-query, argumentutik lortutako metodo batekin (uneko GET adibidean). (r), inplizituki pasatu da funtzio hau deitzen denean. Azpieskaeraren erantzuna funtzioan prozesatuko da call_back.

S3 probatzen

S3 biltegiratze pribaturako eskaera bat egiteko, behar dugu:

ACCESS_KEY

SECRET_KEY

S3_BUCKET

Erabilitako http metodotik, uneko data/ordutik, S3_NAME eta URItik, kate mota jakin bat sortzen da, zeina (HMAC_SHA1) SECRET_KEY erabiliz sinatzen dena. Hurrengoa bezalako lerro bat da AWS $ACCESS_KEY:$HASH, baimenaren goiburuan erabil daiteke. Aurreko urratsean katea sortzeko erabili zen data/ordu bera gehitu behar zaio goiburuari X-amz-date. Kodean honela ikusten da:

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 baimenaren adibidea, zaharkitutako egoerara aldatu da)

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}

Azalpen txiki bat buruz _subrequest_uri: hasierako uriaren arabera S3-ri eskaera bat eratzen dion aldagaia da. "Root"-aren edukia lortu behar baduzu, mugatzailea adieraziz uri eskaera sortu behar duzu. delimiter, CommonPrefixes xml elementu guztien zerrenda itzuliko duena, direktorioei dagozkienak (PyPIren kasuan, pakete guztien zerrenda). Direktorio zehatz bateko edukien zerrenda lortu behar baduzu (paketeen bertsio guztien zerrenda), orduan uri eskaerak aurrizki-eremu bat izan behar du direktorioaren (paketearen) izena nahitaez barra batekin amaituta /. Bestela, talkak gerta daitezke direktorio baten edukiak eskatzean, adibidez. Aiohttp-request eta aiohttp-requests direktorioa daude eta eskaerak zehazten badu /?prefix=aiohttp-request, orduan erantzunak bi direktorioetako edukia izango du. Amaieran barra bat badago, /?prefix=aiohttp-request/, orduan erantzunak beharrezkoa den direktorioa bakarrik edukiko du. Eta fitxategi bat eskatzen badugu, ondoriozko uriak ez luke jatorrizkoaren desberdina izan behar.

Gorde eta berrabiarazi Nginx. Arakatzailean gure Nginx-en helbidea sartzen dugu, eskaeraren emaitza XML izango da, adibidez:

Direktorioen zerrenda

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

Direktorioen zerrendatik elementuak bakarrik beharko dituzu CommonPrefixes.

Nabigatzailean gure helbidean behar dugun direktorioa gehituz gero, bere edukia XML formatuan ere jasoko dugu:

Direktorio bateko fitxategien zerrenda

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

Fitxategien zerrendatik elementuak bakarrik hartuko ditugu Key.

Sortutako XML-a analizatzea eta HTML gisa bidaltzea besterik ez da geratzen, lehenik eduki-mota goiburua testu/html-rekin ordezkatuta.

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="/eu/${reValue.groups.v}">${a_text}</a>`);
    }
  }

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

PyPI probatzen

Funtzionatzen duten paketeetan ezer apurtzen ez dela egiaztatzen dugu.

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

Gure librekin errepikatzen dugu.

# Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ для тСстов Π½ΠΎΠ²ΠΎΠ΅ ΠΎΠΊΡ€ΡƒΠΆΠ΅Π½ΠΈΠ΅
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-n, pakete bat sortzea eta kargatzea honelakoa da:

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

autentifikazio

Gitlab-en JWT erabil daiteke kanpoko zerbitzuen autentifikazio/baimenetarako. Nginx-en auth_request zuzentaraua erabiliz, autentifikazio-datuak scriptean funtzio-dei bat duen azpieskaera batera birbideratuko ditugu. Scriptak beste azpieskaera bat egingo dio Gitlab-en URLari eta autentifikazio-datuak behar bezala zehaztu badira, Gitlab-ek 200 kodea itzuliko du eta paketea igo/deskargatu ahal izango da. Zergatik ez erabili azpikontsulta bat eta berehala bidali datuak Gitlab-era? Orduan Nginx konfigurazio-fitxategia editatu beharko dugulako baimenean aldaketaren bat egiten dugun bakoitzean, eta nahiko lan neketsua da hau. Gainera, Kubernetes-ek irakurtzeko soilik den erro-fitxategi-sistemaren politika erabiltzen badu, horrek are konplexutasun handiagoa gehitzen du nginx.conf konfigmap bidez ordezkatzean. Eta guztiz ezinezkoa bihurtzen da Nginx konfigurazioaren bidez konfiguratzea bolumenen konexioa debekatzen duten politikak (pvc) eta irakurtzeko soilik den erro-fitxategi-sistema (hau ere gertatzen da).

NJS bitartekoa erabiliz, nginx konfigurazioan zehaztutako parametroak aldatzeko aukera izango dugu ingurune-aldagaiak erabiliz eta scriptean egiaztapen batzuk egiteko (adibidez, gaizki zehaztutako URL bat).

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}

Seguruenik galdera sortzen ari da: -Zergatik ez erabili prest egindako moduluak? Han dena eginda dago jada! Adibidez, var AWS = require('aws-sdk') eta ez dago S3 autentifikazioarekin "bizikleta" bat idatzi beharrik!

Goazen txarretara

Niretzat, kanpoko JS moduluak inportatzeko ezintasuna ezaugarri desatsegina bihurtu zen, baina espero zena. Goiko adibidean deskribatuta require('crypto') da txertatutako moduluak eta obrak bakarrik eskatzen dituzte. Ez dago scriptetako kodea berrerabiltzeko modurik ere eta fitxategi ezberdinetan kopiatu eta itsatsi behar duzu. Espero dut noizbait funtzionalitate hau ezarriko dela.

Konpresioa ere desgaitu behar da Nginx-en uneko proiekturako gzip off;

NJSn gzip modulurik ez dagoelako eta konektatzea ezinezkoa delako; beraz, ez dago datu konprimituekin lan egiteko modurik. Egia da, hori ez da kasu honetan negatiboa. Ez dago testu asko, eta transferitutako fitxategiak dagoeneko konprimituta daude eta konpresio gehigarriak ez die asko lagunduko. Gainera, hau ez da hain zerbitzu kargatua edo kritikoa, non edukia milisegundo batzuk azkarrago bidaltzearekin kezkatu behar duzun.

Script-a arazteak denbora luzea behar du eta error.log-eko "inprimatze" bidez soilik da posible. Ezarritako erregistro-mailaren informazioa, abisua edo errorearen arabera, 3 metodo erabil daitezke r.log, r.warn, r.error hurrenez hurren. Chrome-n (v8) edo njs kontsola tresnan script batzuk arakatzen saiatzen naiz, baina ezin da dena egiaztatu bertan. Kodea arazketan, edo proba funtzionalak, historiak honelako itxura du:

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

eta ehunka egon daitezke halako sekuentzia.

Azpikontsultak eta aldagaiak erabiliz kodea idaztea nahaspilatu bihurtzen da. Batzuetan, IDE leiho ezberdinetan barrena hasten zara zure kodearen ekintzen sekuentzia irudikatu nahian. Ez da zaila, baina batzuetan oso gogaikarria da.

Ez dago ES6rako laguntza osoa.

Beste gabezia batzuk egon daitezke, baina ez dut beste ezer topatu. Partekatu informazioa NJS erabiliz esperientzia negatiboa baduzu.

Ondorioa

NJS kode irekiko interprete arina da, Nginx-en hainbat JavaScript script inplementatzeko aukera ematen duena. Bere garapenean, errendimenduari arreta handia jarri zitzaion. Jakina, oraindik asko falta da, baina proiektua talde txiki batek garatzen ari da eta aktiboki funtzio berriak gehitzen eta akatsak konpontzen ari dira. Espero dut noizbait NJS-k kanpoko moduluak konektatzeko aukera emango dizula, eta horrek Nginx funtzionaltasuna ia mugagabea izango du. Baina NGINX Plus dago eta ziurrenik ez da funtziorik izango!

Artikuluaren kode osoa duen biltegia

njs-pypi AWS Sign v4 laguntzarekin

ngx_http_js_module moduluaren zuzentarauen deskribapena

NJS biltegi ofiziala ΠΈ dokumentazioa

Dmitry Volintsev-en NJS erabiltzeko adibideak

njs - nginx-en JavaScript script natiboa / Dmitry Volnyev-en hitzaldia Saint HighLoad++ 2019-n

NJS ekoizpenean / Vasily Soshnikov-en hitzaldia HighLoad++ 2019-n

AWS-n REST eskaerak sinatzea eta autentifikatzea

Iturria: www.habr.com