Ek het my PyPI-bewaarplek gemaak met magtiging en S3. Op Nginx

In hierdie artikel wil ek my ervaring deel met NJS, 'n JavaScript-tolk vir Nginx wat deur Nginx Inc ontwikkel is, wat die hoofkenmerke daarvan beskryf deur 'n regte voorbeeld te gebruik. NJS is 'n subset van JavaScript wat jou toelaat om die funksionaliteit van Nginx uit te brei. Vraag hoekom jou tolk??? Dmitri Volyntsev het in detail geantwoord. Kortom: NJS is die nginx-manier, en JavaScript is meer progressief, "inheems" en sonder GC, anders as Lua.

Lank gelede…

By my laaste werk het ek gitlab geërf met 'n aantal bont CI / CD pyplyne met docker-compose, dind en ander lekkernye wat na die kaniko rails oorgedra is. Die beelde wat voorheen in CI gebruik is, het in hul oorspronklike vorm geskuif. Hulle het behoorlik gewerk tot die dag toe ons gitlab sy IP verander het en CI in 'n pampoen verander het. Die probleem was dat een van die docker-beelde wat aan CI deelgeneem het, git bevat het, wat Python-modules via ssh getrek het. ssh benodig 'n private sleutel en ... dit was in die beeld saam met bekende_gashere. En enige CI het geëindig met 'n sleutelverifikasiefout as gevolg van 'n wanverhouding tussen die werklike IP en die gespesifiseer in bekend_gasheer. 'n Nuwe prent is vinnig saamgestel uit die bestaande Dockfiles en die opsie is bygevoeg StrictHostKeyChecking no. Maar die onaangename nasmaak het gebly en daar was 'n begeerte om die lêers na 'n privaat PyPI-bewaarplek oor te dra. 'n Bykomende bonus, nadat oorgeskakel is na private PyPI, was 'n eenvoudiger pyplyn en 'n normale beskrywing van requirements.txt

Die keuse is gemaak, here!

Ons draai alles in die wolke en Kubernetes, en op die ou end wou ons 'n klein diens kry wat 'n staatlose houer met eksterne berging was. Wel, aangesien ons S3 gebruik, was die prioriteit daaragter. En, indien moontlik, met verifikasie in gitlab (jy kan dit self byvoeg indien nodig).

'n Vinnige soektog het verskeie resultate vir s3pypi, pypicloud en 'n "handmatige" html-lêerskeppingsopsie vir raap opgelewer. Die laaste opsie het vanself verdwyn.

s3pypi: Dit is die cli vir die gebruik van S3-hosting. Laai lêers op, genereer html en laai dit op na dieselfde emmer. Geskik vir tuisgebruik.

pypicloud: Het na 'n interessante projek gelyk, maar nadat ek die dokumente gelees het, het dit frustrerend geraak. Ten spyte van die goeie dokumentasie en die moontlikheid om uit te brei om jou take te pas, het dit in werklikheid oortollig geblyk en moeilik om te konfigureer. Om die kode vir jou take reg te stel, volgens die destydse skattings, sal 3-5 dae neem. Die diens benodig ook 'n databasis. Ons het dit gelos vir ingeval ons niks anders kry nie.

'n Meer in-diepte soektog het 'n module vir Nginx, ngx_aws_auth, opgelewer. Die resultaat van sy toetsing was XML wat in die blaaier vertoon is, wat die inhoud van die S3-emmer gewys het. Die laaste pleeg, ten tyde van die soektog, was 'n jaar gelede. Die bewaarplek het verlate gelyk.

Blaai na die oorspronklike bron en lees PEP-503 besef dat XML onmiddellik na HTML omgeskakel kan word en aan pip gegee kan word. Deur 'n bietjie meer te google volgens Nginx en S3, het ek afgekom op 'n voorbeeld van verifikasie in S3 geskryf in JS vir Nginx. Dis hoe ek NJS leer ken het.

Met hierdie voorbeeld as basis, het ek 'n uur later dieselfde XML in my blaaier waargeneem as toe ek die ngx_aws_auth-module gebruik het, maar alles was reeds in JS geskryf.

Ek het baie van die nginx-oplossing gehou. Eerstens, goeie dokumentasie en baie voorbeelde, tweedens kry ons al die Nginx-goedjies om met lêers te werk (uit die boks), derdens, enigiemand wat weet hoe om Nginx-konfigurasies te skryf, sal kan uitvind wat is wat. Minimalisme is ook vir my 'n pluspunt, in vergelyking met Python of Go (indien van nuuts af geskryf), om nie eens te praat van nexus nie.

TL;DR Na 2 dae is die toetsweergawe van PyPi reeds in CI gebruik.

Hoe werk dit?

Module is in Nginx gelaai ngx_http_js_module, is ingesluit in die amptelike docker-beeld. Ons voer ons skrif in deur die opdrag te gebruik js_importna die Nginx-konfigurasie. Die funksie-oproep word deur die opdrag uitgevoer js_content. Die richtlijn word gebruik om veranderlikes te stel js_set, wat slegs die funksie wat in die skrif beskryf word as 'n argument neem. Maar ons kan slegs subversoeke in NJS uitvoer met behulp van Nginx, nie enige XMLHttpRequest vir jou nie. Om dit te doen, moet die toepaslike ligging by die Nginx-konfigurasie gevoeg word. En in die skrif moet 'n subversoek (subversoek) na hierdie ligging beskryf word. Om toegang tot die funksie vanaf die Nginx-konfigurasie te kry, moet die funksienaam in die skrip self uitgevoer word 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}

Wanneer dit in 'n blaaier versoek word http://localhost:8080/ ons val in location /waarin die richtlijn js_content roep 'n funksie request beskryf in ons draaiboek script.js. Op sy beurt, in die funksie request 'n subnavraag gemaak word na location = /sub-query, met die metode (in die huidige AOO-voorbeeld) verkry uit die argument (r), implisiet geslaag wanneer hierdie funksie opgeroep word. Die verwerking van die subversoekreaksie sal in die funksie uitgevoer word call_back.

Probeer S3

Om 'n versoek aan 'n privaat S3-berging te rig, benodig ons:

ACCESS_KEY

SECRET_KEY

S3_BUCKET

Van die http-metode wat gebruik word, die huidige datum/tyd, S3_NAME en URI, word 'n sekere soort string gegenereer, wat onderteken word (HMAC_SHA1) met SECRET_KEY. Volgende reël, soos AWS $ACCESS_KEY:$HASH, kan in die Magtiging-opskrif gebruik word. Dieselfde datum/tyd wat gebruik is om die string in die vorige stap te genereer, moet by die kopskrif gevoeg word X-amz-date. In kode lyk dit so:

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-magtigingsvoorbeeld, geskuif na verouderde status)

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}

'n Klein verduideliking oor _subrequest_uri: dit is 'n veranderlike wat, afhangende van die aanvanklike uri, 'n versoek na S3 genereer. As jy die inhoud van die "wortel" moet kry, moet jy in hierdie geval 'n uri-versoek met 'n skeier vorm delimiter, wat 'n lys van alle CommonPrefixes xml-elemente sal terugstuur, wat ooreenstem met gidse (in die geval van PyPI, 'n lys van alle pakkette). As jy 'n lys van inhoud in 'n spesifieke gids moet kry ('n lys van alle pakketweergawes), dan moet die uri-versoek 'n voorvoegselveld bevat met die naam van die gids (pakket) moet eindig met 'n skuinsstreep /. Andersins is botsings moontlik wanneer byvoorbeeld die inhoud van 'n gids versoek word. Daar is gidse aiohttp-versoek en aiohttp-versoeke en as die versoek spesifiseer /?prefix=aiohttp-request, dan sal die antwoord die inhoud van beide gidse bevat. As daar 'n streep aan die einde is, /?prefix=aiohttp-request/, dan sal slegs die verlangde gids teruggestuur word. En as ons 'n lêer aanvra, moet die resulterende uri nie van die oorspronklike een verskil nie.

Stoor, herbegin Nginx. In die blaaier voer ons die adres van ons Nginx in, die resultaat van die versoek sal XML wees, byvoorbeeld:

Gidslys

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

Uit die lys van gidse sal slegs elemente benodig word CommonPrefixes.

Deur die gids wat ons benodig in die blaaier by ons adres te voeg, sal ons die inhoud daarvan ook in die vorm van XML kry:

Lys lêers in 'n gids

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

Neem slegs elemente uit die lys lêers Key.

Dit bly om die resulterende XML te ontleed en dit as HTML terug te gee, nadat die inhoudtipe-kopskrif met teks/html vervang is.

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

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

Probeer PyPI

Ons kyk dat nêrens en niks breek op duidelik werkende pakkette nie.

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

Ons herhaal met ons libs.

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

In CI lyk die skep en laai van 'n pakket soos volg:

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

verifikasie

In Gitlab is dit moontlik om JWT te gebruik om eksterne dienste te verifieer/magtig. Deur die auth_request-instruksie in Nginx te gebruik, sal ons die verifikasiedata herlei na 'n subversoek wat 'n funksie-oproep in die skrif bevat. In die skrif sal nog een subversoek na die Gitlab-url gerig word, en as die verifikasiedata korrek gespesifiseer is, sal Gitlab die kode 200 terugstuur en die aflaai/aflaai van die pakket sal toegelaat word. Hoekom nie net 'n enkele subversoek gebruik en die data dadelik na Gitlab stoot nie? Want dan sal jy die Nginx-konfigurasielêer moet wysig elke keer as ons 'n paar veranderinge in magtiging het, en dit is 'n taamlike droewige taak. Ook, as die leesalleen-wortellêerstelselbeleid in Kubernetes gebruik word, voeg dit selfs meer kompleksiteit by wanneer nginx.conf deur configmap vervang word. En dit word absoluut onmoontlik om Nginx deur configmap op te stel terwyl jy beleide gebruik wat die verbinding van volumes (pvc) en leesalleen-wortellêerstelsel verbied (dit gebeur ook).

Deur die NJS-intermediêre te gebruik, kry ons die vermoë om die gespesifiseerde parameters in die nginx-konfigurasie te verander deur omgewingsveranderlikes te gebruik en 'n paar kontroles in die skrif te doen (byvoorbeeld 'n verkeerd gespesifiseerde 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}

Heel waarskynlik is die vraag aan die broei: - Hoekom nie klaargemaakte modules gebruik nie? Alles is reeds daar gedoen! Byvoorbeeld, var AWS = require('aws-sdk') en moenie 'n fiets met S3-verifikasie skryf nie!

Kom ons gaan oor na die nadele

Vir my het die onvermoë om eksterne JS-modules in te voer 'n nare maar verwagte kenmerk geword. Beskryf in die voorbeeld hierbo vereis ('crypto'), hierdie ingeboude modules en vereis slegs werke vir hulle. Daar is ook geen manier om die kode vanaf skrifte te hergebruik nie en jy moet dit kopieer en in verskillende lêers plak. Ek hoop dat hierdie funksionaliteit eendag geïmplementeer sal word.

Ook, vir die huidige projek in Nginx, moet kompressie gedeaktiveer word. gzip off;

Omdat daar geen gzip-module in NJS is nie en dit onmoontlik is om dit te koppel, is daar dus geen manier om met saamgeperste data te werk nie. Dit is weliswaar nie 'n besondere minus vir hierdie geval nie. Daar is nie veel teks nie, en die oorgedrade lêers is reeds saamgepers en bykomende kompressie sal hulle nie veel help nie. Dit is ook nie so besige of kritieke diens om 'n paar millisekondes vinniger te steur aan die terugkeer van inhoud nie.

Ontfouting van die skrip is lank en is slegs moontlik deur "afdrukke" in die error.log. Afhangende van die gestelde logvlakinligting, waarskuwing of fout, is dit moontlik om onderskeidelik 3 metodes r.log, r.warn, r.error te gebruik. Ek probeer om 'n paar skrifte in Chrome (v8) of die njs-konsole-instrument te ontfout, maar nie alles kan daar nagegaan word nie. By die ontfouting van kode, oftewel funksionele toetsing, lyk geskiedenis so iets:

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

en daar kan honderde sulke rye wees.

Die skryf van kode deur subnavrae en veranderlikes daarvoor te gebruik, verander in 'n verstrengelde warboel. Soms begin jy deur verskillende IDE-vensters jaag om die volgorde van aksies in jou kode uit te vind. Dit is nie moeilik nie, maar soms baie irriterend.

Daar is geen volledige ondersteuning vir ES6 nie.

Daar is dalk 'n paar ander tekortkominge, maar ek het niks anders teëgekom nie. Deel die inligting as jy 'n negatiewe ervaring met NJS het.

Gevolgtrekking

NJS is 'n liggewig oopbron-tolk waarmee u verskeie JavaScript-skrifte in Nginx kan implementeer. Tydens die ontwikkeling daarvan is groot aandag aan prestasie gegee. Natuurlik ontbreek nog baie dinge daarin, maar die projek word deur 'n klein span ontwikkel en hulle voeg aktief nuwe kenmerke by en maak foute reg. Ek hoop dat NJS jou eendag sal toelaat om eksterne modules aan te sluit, wat die funksionaliteit van Nginx byna onbeperk sal maak. Maar daar is NGINX Plus en daar sal waarskynlik geen kenmerke wees nie!

Bewaarplek met die volledige kode vir die artikel

njs-pypi met AWS Sign v4-ondersteuning

Beskrywing van ngx_http_js_module module riglyne

Amptelike NJS-bewaarplek и dokumentasie

Voorbeelde van die gebruik van NJS van Dmitri Volintsev

njs - inheemse JavaScript scripting in nginx / Toespraak deur Dmitri Volniev by Saint HighLoad++ 2019

NJS in produksie / Aanbieding deur Vasily Soshnikov by HighLoad++ 2019

Ondertekening en stawing van REST-versoeke in AWS

Bron: will.com