Elkészítettem a saját PyPI-tárolómat jogosultsággal és S3-mal. Az Nginx-en

Ebben a cikkben szeretném megosztani tapasztalataimat az NJS-ről, az Nginx Inc által kifejlesztett JavaScript-tolmácsról az Nginx számára, és egy valós példán keresztül leírom főbb képességeit. Az NJS a JavaScript egy részhalmaza, amely lehetővé teszi az Nginx funkcióinak kiterjesztését. A kérdésre miért a saját tolmácsod??? Dmitrij Volyncev részletesen válaszolt. Röviden: az NJS nginx-módú, a JavaScript pedig progresszívebb, „natív” és GC nélküli, ellentétben a Luával.

Régen…

Legutóbbi munkahelyemen megörököltem a gitlabot számos tarka CI/CD pipeline-nal, docker-compose, dind és egyéb élvezetekkel, amelyek átkerültek a kaniko railekre. A korábban a CI-ben használt képek eredeti formájukban kerültek áthelyezésre. Egészen addig a napig rendesen működtek, amikor a gitlab IP-nk megváltozott, és a CI tökké változott. A probléma az volt, hogy a CI-ben részt vevő docker-képek egyikén volt a git, amely ssh-n keresztül húzta a Python modulokat. Az ssh-hez szükséged van egy privát kulcsra, és... ez benne volt a képben az ismert_gazdagépekkel együtt. És minden CI meghiúsult kulcsellenőrzési hibával a valódi IP és az ismert_gazdagépekben megadott IP közötti eltérés miatt. A meglévő Dockfájlokból gyorsan összeállítottak egy új képet, és hozzáadták az opciót StrictHostKeyChecking no. De a rossz ízek megmaradtak, és felmerült a vágy, hogy a libeket egy privát PyPI-tárolóba helyezzék át. A privát PyPI-re váltás után további bónusz volt egy egyszerűbb folyamat és a követelmények normál leírása.txt

A választás megtörtént, Uraim!

Mindent a felhőkben és a Kubernetesben futtatunk, és a végén szerettünk volna egy kis szolgáltatást kapni, ami egy állapot nélküli konténer külső tárolóval. Nos, mivel mi S3-at használunk, ez kapott elsőbbséget. És ha lehetséges, gitlab hitelesítéssel (szükség esetén saját magad is hozzáadhatod).

Egy gyors keresés több eredményt is hozott: s3pypi, pypicloud és egy opció a html fájlok „kézi” létrehozásával a fehérrépa számára. Az utolsó lehetőség magától eltűnt.

s3pypi: Ez egy klip az S3 tárhely használatához. Feltöltjük a fájlokat, legeneráljuk a html-t és feltöltjük ugyanabba a tárolóba. Otthoni használatra alkalmas.

pypicloud: Érdekes projektnek tűnt, de a dokumentáció elolvasása után csalódott voltam. A jó dokumentáció és az igényeknek megfelelő bővíthetőség ellenére a valóságban redundánsnak és nehezen konfigurálhatónak bizonyult. A kód kijavítása az Ön feladataihoz az akkori becslések szerint 3-5 napot vett volna igénybe. A szolgáltatáshoz adatbázisra is szükség van. Meghagytuk, hátha mást nem találunk.

Egy alaposabb keresés eredménye egy modul az Nginx számára, ngx_aws_auth. Tesztének eredménye a böngészőben megjelenített XML volt, amely az S3 vödör tartalmát mutatta. A keresés idején az utolsó elkövetés egy éve volt. A tároló elhagyatottnak tűnt.

A forráshoz menve és elolvasva PEP-503 Rájöttem, hogy az XML-t menet közben is lehet konvertálni HTML-be és megadni a pip-nek. Miután kicsit többet gugliztam az Nginxről és az S3-ról, találkoztam egy hitelesítési példával az S3-ban, JS for Nginx-hez írva. Így ismerkedtem meg NJS-vel.

Ezt a példát véve alapul, egy órával később ugyanazt az XML-t láttam a böngészőmben, mint az ngx_aws_auth modul használatakor, de már minden JS-ben meg volt írva.

Nagyon tetszett az nginx megoldás. Először is, jó dokumentáció és sok példa, másodszor, megkapjuk az Nginx összes finomságát a fájlokkal való munkavégzéshez (a dobozból), harmadszor, aki tudja, hogyan kell konfigurációkat írni az Nginxhez, képes lesz rájönni, hogy mi az. A minimalizmus is plusz számomra a Pythonhoz vagy a Gohoz képest (ha nulláról írják), a nexusról nem is beszélve.

TL;DR 2 nap elteltével a PyPi tesztverzióját már használták a CI-ben.

Hogyan működik?

A modul betöltődik az Nginxbe ngx_http_js_module, szerepel a hivatalos docker képben. A szkriptünket a direktíva segítségével importáljuk js_importNginx konfigurációhoz. A függvényt egy direktíva hívja meg js_content. A direktíva változók beállítására szolgál js_set, amely csak a szkriptben leírt függvényt veszi argumentumnak. De az NJS-ben csak az Nginx használatával hajthatunk végre segédlekérdezéseket, nem bármilyen XMLHttpRequest használatával. Ehhez hozzá kell adni a megfelelő helyet az Nginx konfigurációhoz. És a szkriptnek le kell írnia egy alkérést erre a helyre. Ahhoz, hogy az Nginx konfigurációból hozzáférhessen egy függvényhez, a függvény nevét magában a szkriptben kell exportálni. 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}

Ha kéri a böngészőben http://localhost:8080/ bejutunk location /amelyben az irányelv js_content függvényt hív meg request forgatókönyvünkben leírtuk script.js. Viszont a függvényben request részlekérdezés történik location = /sub-query, az argumentumból nyert metódussal (az aktuális példában GET). (r), implicit módon átadva a függvény meghívásakor. A részkérelem választ a függvény feldolgozza call_back.

S3 kipróbálás

A privát S3 tárhely igényléséhez a következőkre van szükségünk:

ACCESS_KEY

SECRET_KEY

S3_BUCKET

A használt http metódusból, az aktuális dátum/idő, S3_NAME és URI alapján egy bizonyos típusú karakterlánc generálódik, amelyet a SECRET_KEY segítségével írnak alá (HMAC_SHA1). Következő egy sor hasonló AWS $ACCESS_KEY:$HASH, használható az engedélyezési fejlécben. Ugyanazt a dátumot/időt kell hozzáadni a fejléchez, amelyet az előző lépésben a karakterlánc létrehozásához használtunk X-amz-date. Kódban így néz ki:

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 engedélyezési példa, elavult állapotra módosult)

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}

Egy kis magyarázat róla _subrequest_uri: ez egy olyan változó, amely a kezdeti uri-tól függően kérést hoz létre az S3 felé. Ha meg kell szereznie a "root" tartalmát, akkor létre kell hoznia egy uri kérést, amely jelzi a határolót delimiter, amely a CommonPrefixes xml összes elemének listáját adja vissza, a könyvtáraknak megfelelően (PyPI esetén az összes csomag listáját). Ha egy adott könyvtár tartalmi listáját kell beszereznie (az összes csomagverzió listája), akkor az uri kérésnek tartalmaznia kell egy előtag mezőt, amelyben a könyvtár (csomag) neve szükségszerűen perjelre végződik. Ellenkező esetben ütközések lehetségesek például egy könyvtár tartalmának lekérésekor. Vannak aiohttp-request és aiohttp-request könyvtárak, és ha a kérés megadja /?prefix=aiohttp-request, akkor a válasz mindkét könyvtár tartalmát fogja tartalmazni. Ha perjel van a végén, /?prefix=aiohttp-request/, akkor a válasz csak a szükséges könyvtárat tartalmazza. Ha pedig fájlt kérünk, akkor a kapott uri nem térhet el az eredetitől.

Mentse és indítsa újra az Nginxet. A böngészőben megadjuk az Nginxünk címét, a kérés eredménye XML lesz, például:

Könyvtárak listája

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

A könyvtárak listájából csak az elemekre lesz szüksége CommonPrefixes.

Ha a böngészőben hozzáadjuk a címünkhöz a szükséges könyvtárat, akkor annak tartalmát is megkapjuk XML formában:

Fájlok listája egy könyvtárban

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

A fájlok listájából csak elemeket veszünk ki Key.

Már csak az eredményül kapott XML-t kell elemezni, és HTML-ként elküldeni, miután először a Content-Type fejlécet text/html-re cseréltük.

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

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

PyPI kipróbálása

Ellenőrizzük, hogy az ismerten működő csomagokon sehol sem törik-e el semmi.

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

Ismételjük a libseinkkel.

# Создаем для тестов новое окружение
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-ben a csomag létrehozása és betöltése így néz ki:

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

Hitelesítés

A Gitlabban lehetőség van a JWT-re külső szolgáltatások hitelesítésére/engedélyezésére. Az Nginx auth_request direktívájával átirányítjuk a hitelesítési adatokat egy olyan alkérelemre, amely egy függvényhívást tartalmaz a szkriptben. A szkript újabb alkérelmet küld a Gitlab URL-hez, és ha a hitelesítési adatok helyesen lettek megadva, akkor a Gitlab 200-as kódot ad vissza, és a csomag feltöltése/letöltése engedélyezett lesz. Miért nem használ egyetlen segédlekérdezést, és azonnal elküldi az adatokat a Gitlabnak? Mert akkor minden alkalommal szerkesztenünk kell az Nginx konfigurációs fájlt, amikor bármilyen változtatást végzünk az engedélyezésben, és ez meglehetősen fárasztó feladat. Továbbá, ha a Kubernetes csak olvasható gyökérfájlrendszer-házirendet használ, akkor ez még bonyolultabbá teszi az nginx.conf configmap segítségével történő lecserélését. És teljesen lehetetlenné válik az Nginx konfigurálása configmap segítségével, miközben egyidejűleg olyan házirendeket használnak, amelyek tiltják a kötetek (pvc) és a csak olvasható gyökérfájlrendszer csatlakoztatását (ez is előfordul).

Az NJS intermediate használatával lehetőséget kapunk az nginx konfigurációban a megadott paraméterek megváltoztatására a környezeti változók segítségével, és néhány ellenőrzést elvégezhetünk a szkriptben (például egy hibásan megadott URL).

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}

Valószínűleg felmerül a kérdés: -Miért nem használunk kész modulokat? Ott már minden megtörtént! Például var AWS = request('aws-sdk'), és nem kell S3 hitelesítéssel „bike”-t írni!

Térjünk át a hátrányokra

Számomra kellemetlen, de elvárható jellemzővé vált a külső JS modulok importálhatatlansága. A fenti példában leírva a request('crypto') az beépített modulok és csak nekik kell munkákat. Arra sincs mód, hogy a szkriptekből származó kódot újra felhasználjuk, és másolni kell, majd be kell illeszteni különböző fájlokba. Remélem, egyszer ez a funkció megvalósul.

A tömörítést az Nginx jelenlegi projektjénél is le kell tiltani gzip off;

Mivel az NJS-ben nincs gzip modul, és nem is lehet csatlakoztatni, ezért nincs mód tömörített adatokkal dolgozni. Igaz, ez nem igazán mínusz ebben az esetben. Nincs sok szöveg, és az átvitt fájlok már tömörítettek, és a további tömörítés nem sokat segít rajtuk. Ezenkívül ez nem annyira terhelt vagy kritikus szolgáltatás, hogy néhány ezredmásodperccel gyorsabban kelljen tartalomszolgáltatással bajlódnia.

A szkript hibakeresése sokáig tart, és csak az error.log „nyomtatásain” keresztül lehetséges. A beállított naplózási szintű információtól, figyelmeztetéstől vagy hibától függően 3 r.log, r.warn, r.error módszer használható. Megpróbálok hibakeresni néhány szkriptet Chrome-ban (v8) vagy az njs konzol eszközben, de ott nem lehet mindent ellenőrizni. A kód hibakeresésekor, más néven funkcionális teszteléskor az előzmények valahogy így néznek ki:

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

és több száz ilyen sorozat lehet.

Az allekérdezések és a hozzájuk tartozó változók használatával kódot írni kusza gubanc lesz. Néha elkezdesz rohanni a különböző IDE-ablakok körül, és megpróbálod kitalálni a kód műveleteinek sorrendjét. Nem nehéz, de néha nagyon bosszantó.

Az ES6-hoz nincs teljes körű támogatás.

Lehetnek más hiányosságok is, de mással nem találkoztam. Oszd meg az információkat, ha negatív tapasztalataid vannak az NJS használatával kapcsolatban.

Következtetés

Az NJS egy könnyű, nyílt forráskódú értelmező, amely lehetővé teszi különféle JavaScript-szkriptek megvalósítását az Nginxben. A fejlesztés során nagy figyelmet fordítottak a teljesítményre. Persze még sok hiányzik, de a projektet egy kis csapat fejleszti, és aktívan bővítik új funkciókkal, javítják a hibákat. Remélem, hogy egyszer az NJS lehetővé teszi külső modulok csatlakoztatását, ami szinte korlátlanná teszi az Nginx funkcionalitását. De van NGINX Plus, és valószínűleg nem lesznek funkciók!

Tárhely a cikk teljes kódjával

njs-pypi AWS Sign v4 támogatással

Az ngx_http_js_module modul direktíváinak leírása

Hivatalos NJS adattár и a dokumentáció

Példák az NJS használatára Dmitrij Volincevtől

njs – natív JavaScript szkriptelés az nginxben / Dmitrij Volnyev beszéde a Saint HighLoad++ 2019-ben

NJS gyártásban / Vaszilij Szosnyikov beszéde a HighLoad++ 2019-ben

REST-kérelmek aláírása és hitelesítése az AWS-ben

Forrás: will.com