Tegin oma PyPI hoidla autoriseerimise ja S3-ga. Nginxis

Selles artiklis tahaksin jagada oma kogemusi NJS-iga, Nginxi JavaScripti tõlgiga, mille on välja töötanud Nginx Inc, kirjeldades selle peamisi võimalusi reaalse näite abil. NJS on JavaScripti alamhulk, mis võimaldab teil laiendada Nginxi funktsioone. Küsimusele miks oma tõlk??? Dmitri Volyntsev vastas üksikasjalikult. Lühidalt: NJS on nginxi moodi ja JavaScript on erinevalt Luast progressiivsem, "native" ja ilma GCta.

Kaua aega tagasi…

Viimasel töökohal sain päranduseks gitlabi koos mitmete kirjude CI/CD torujuhtmetega koos docker-compose, dindi ja muude naudingutega, mis kanti üle kaniko rööbastele. Varem CI-s kasutatud pildid teisaldati nende algsel kujul. Nad töötasid korralikult kuni päevani, mil meie gitlabi IP muutus ja CI muutus kõrvitsaks. Probleem oli selles, et ühel CI-s osalenud dockeri kujutisel oli git, mis tõmbas Pythoni mooduleid ssh-i kaudu. Ssh jaoks on vaja privaatvõtit ja... see oli pildil koos teadaolevate_hostidega. Ja mis tahes CI ebaõnnestus võtme kinnitamise vea tõttu, kuna tegelik IP ja teadaolevad_hostid määratud IP ei sobinud. Olemasolevatest dokkifailidest koostati kiiresti uus pilt ja lisati võimalus StrictHostKeyChecking no. Kuid halb maik jäi alles ja tekkis soov libsid kolida privaatsesse PyPI repositooriumisse. Täiendavaks boonuseks pärast privaatsele PyPI-le üleminekut oli lihtsam konveier ja nõuete tavaline kirjeldus.txt

Valik on tehtud, härrased!

Käitame kõike pilvedes ja Kubernetes ning lõpuks tahtsime saada väikese teenuse, mis oleks välise salvestusruumiga olekuta konteiner. Noh, kuna me kasutame S3, siis oli see prioriteet. Ja võimalusel gitlabis autentimisega (vajadusel saab ise lisada).

Kiire otsing andis mitu tulemust: s3pypi, pypicloud ja kaalika jaoks html-failide käsitsi loomise võimalus. Viimane variant kadus iseenesest.

s3pypi: see on kliki S3 hostimise kasutamiseks. Laadime failid üles, genereerime html-i ja laadime selle samasse ämbrisse. Sobib koduseks kasutamiseks.

pypicloud: See tundus huvitav projekt, kuid pärast dokumentatsiooni lugemist olin pettunud. Vaatamata heale dokumentatsioonile ja võimalusele vastavalt oma vajadustele laiendada, osutus see tegelikkuses üleliigseks ja raskesti konfigureeritavaks. Koodi korrigeerimine oma ülesannete järgi oleks tollaste hinnangute kohaselt võtnud 3–5 päeva. Teenus vajab ka andmebaasi. Jätsime selle juhuks, kui midagi muud ei leia.

Põhjalikum otsing andis Nginxi jaoks mooduli ngx_aws_auth. Tema testimise tulemuseks oli brauseris kuvatud XML, mis näitas S3 ämbri sisu. Viimane kohustus läbiotsimise ajal oli aasta tagasi. Hoidla näis olevat mahajäetud.

Lähtudes allikast ja lugedes PEP-503 Sain aru, et XML-i saab käigupealt HTML-iks teisendada ja pipi anda. Pärast Nginxi ja S3 kohta veidi rohkem guugeldamist leidsin S3 autentimise näite, mis oli kirjutatud JS-is Nginxi jaoks. Nii tutvusin NJS-iga.

Kui võtta aluseks see näide, siis tund aega hiljem nägin oma brauseris sama XML-i, mis moodulit ngx_aws_auth kasutades, aga kõik oli juba JS-is kirjas.

Mulle väga meeldis nginxi lahendus. Esiteks, hea dokumentatsioon ja palju näiteid, teiseks saame kõik Nginxi headused failidega töötamiseks (kastist välja), kolmandaks saab igaüks, kes teab, kuidas Nginxi jaoks konfiguratsioone kirjutada, aru saada, mis on mis. Minimalism on minu jaoks ka pluss, võrreldes Pythoni või Go-ga (kui nullist kirjutada), rääkimata nexusest.

TL;DR 2 päeva pärast kasutati PyPi testversiooni juba CI-s.

Kuidas see toimib?

Moodul laaditakse Nginxi ngx_http_js_module, mis sisaldub ametlikus dokkimispildis. Impordime oma skripti direktiivi abil js_importNginxi konfiguratsioonile. Funktsiooni kutsutakse välja käskkirjaga js_content. Muutujate määramiseks kasutatakse direktiivi js_set, mis võtab argumendina ainult skriptis kirjeldatud funktsiooni. Kuid alampäringuid saame NJS-is täita ainult Nginxi, mitte ühegi XMLHttpRequesti abil. Selleks tuleb Nginxi konfiguratsiooni lisada vastav asukoht. Ja skript peab kirjeldama alampäringut sellele asukohale. Funktsioonile juurde pääsemiseks Nginxi konfiguratsioonist tuleb funktsiooni nimi eksportida skripti endasse 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}

Kui seda brauseris nõutakse http://localhost:8080/ me siseneme location /milles direktiiv js_content kutsub funktsiooni request kirjeldatud meie skriptis script.js. Omakorda funktsioonis request tehakse alampäring location = /sub-query, argumendist saadud meetodiga (praeguses näites GET). (r), edastatakse selle funktsiooni kutsumisel kaudselt. Alampäringu vastust töödeldakse funktsioonis call_back.

Proovin S3

Privaatse S3 salvestusruumi päringu esitamiseks vajame:

ACCESS_KEY

SECRET_KEY

S3_BUCKET

Kasutatud http-meetodist, praegusest kuupäevast/kellaajast S3_NAME ja URI-st genereeritakse teatud tüüpi string, mis allkirjastatakse (HMAC_SHA1) kasutades SECRET_KEY. Järgmine on rida nagu AWS $ACCESS_KEY:$HASH, saab kasutada volituse päises. Päisesse tuleb lisada sama kuupäev/kellaaeg, mida kasutati stringi genereerimiseks eelmises etapis X-amz-date. Koodis näeb see välja selline:

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 autoriseerimise näide, muudetud aegunud olekuks)

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}

Väike selgitus selle kohta _subrequest_uri: see on muutuja, mis sõltuvalt algsest uri-st moodustab päringu S3-le. Kui teil on vaja hankida "juure" sisu, peate looma uri päringu, mis näitab eraldajat delimiter, mis tagastab loendi kõigist CommonPrefixes xml-i elementidest, mis vastavad kataloogidele (PyPI puhul kõigi pakettide loend). Kui teil on vaja hankida konkreetse kataloogi sisuloend (kõigi paketiversioonide loend), peab uri päring sisaldama prefiksivälja, mille kataloogi (paketi) nimi peab tingimata lõppema kaldkriipsuga /. Vastasel juhul on kokkupõrked võimalikud näiteks kataloogi sisu taotlemisel. Seal on kataloogid aiohttp-request ja aiohttp-requests ning kui päring seda täpsustab /?prefix=aiohttp-request, siis sisaldab vastus mõlema kataloogi sisu. Kui lõpus on kaldkriips, /?prefix=aiohttp-request/, siis sisaldab vastus ainult vajalikku kataloogi. Ja kui taotleme faili, ei tohiks saadud uri algsest erineda.

Salvestage ja taaskäivitage Nginx. Brauserisse sisestame oma Nginxi aadressi, päringu tulemuseks on näiteks XML:

Kataloogide loend

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

Kataloogide loendist vajate ainult elemente CommonPrefixes.

Lisades brauseris oma aadressile vajaliku kataloogi, saame ka selle sisu XML-vormingus:

Kataloogis olevate failide loend

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

Failide loendist võtame ainult elemendid Key.

Jääb üle vaid analüüsida saadud XML ja saata see välja HTML-ina, olles esmalt asendanud sisutüübi päise tekstiga/html-iga.

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

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

Proovin PyPI-d

Kontrollime, et teadaolevalt toimivatel pakenditel midagi ei puruneks.

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

Kordame oma libsidega.

# Создаем для тестов новое окружение
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-s näeb paketi loomine ja laadimine välja järgmine:

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

Autentimine

Gitlabis on võimalik välisteenuste autentimiseks/volitamiseks kasutada JWT-d. Kasutades Nginxi käsku auth_request, suuname autentimisandmed ümber alampäringule, mis sisaldab skriptis funktsioonikutset. Skript teeb Gitlabi url-ile uue alampäringu ja kui autentimisandmed olid õigesti määratud, tagastab Gitlab koodi 200 ja paketi üles-/allalaadimine on lubatud. Miks mitte kasutada ühte alampäringut ja saata andmed kohe Gitlabi? Sest siis peame Nginxi konfiguratsioonifaili redigeerima iga kord, kui teeme volituses muudatusi, ja see on üsna tüütu ülesanne. Samuti, kui Kubernetes kasutab kirjutuskaitstud juurfailisüsteemi poliitikat, muudab see faili nginx.conf asendamisel configmapi kaudu veelgi keerukamaks. Ja Nginxi konfigureerimine konfiguratsioonikaardi kaudu muutub täiesti võimatuks, kasutades samaaegselt köidete (pvc) ja kirjutuskaitstud juurfailisüsteemi ühendamist keelavaid poliitikaid (seda juhtub ka).

NJS-i vaheainet kasutades saame võimaluse keskkonnamuutujate abil muuta nginxi konfiguratsioonis määratud parameetreid ja teha skriptis mõningaid kontrolle (näiteks valesti määratud 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}

Tõenäoliselt kerkib esile küsimus: -Miks mitte kasutada valmismooduleid? Seal on kõik juba tehtud! Näiteks var AWS = nõuda('aws-sdk') ja pole vaja S3 autentimisega "ratast" kirjutada!

Liigume edasi miinuste juurde

Minu jaoks sai ebameeldivaks, kuid oodatud funktsiooniks suutmatus importida väliseid JS-mooduleid. Ülalolevas näites kirjeldatud request('crypto') on sisseehitatud moodulid ja nõuavad ainult neile mõeldud töid. Samuti ei saa skriptidest pärit koodi uuesti kasutada ning peate selle kopeerima ja kleepima erinevatesse failidesse. Loodan, et kunagi see funktsioon rakendub.

Samuti tuleb Nginxi praeguse projekti jaoks tihendamine keelata gzip off;

Kuna NJS-is pole gzip-moodulit ja seda on võimatu ühendada, pole tihendatud andmetega võimalik töötada. Tõsi, see ei ole selle juhtumi puhul tegelikult miinus. Teksti on vähe ja ülekantud failid on juba tihendatud ning täiendav pakkimine neid palju ei aita. Samuti pole see nii koormatud või kriitilise tähtsusega teenus, et peaks vaeva nägema mõne millisekundi kiirema sisu edastamisega.

Skripti silumine võtab kaua aega ja on võimalik ainult faili error.log "printide" kaudu. Olenevalt seadistatud logimistaseme infost, hoiatusest või veast on võimalik kasutada vastavalt 3 meetodit r.log, r.warn, r.error. Proovin mõnda skripti siluda Chrome'is (v8) või njs-i konsoolitööriistas, kuid kõike ei saa seal kontrollida. Koodi silumisel ehk funktsionaalse testimise korral näeb ajalugu välja umbes selline:

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

ja selliseid jadasid võib olla sadu.

Koodi kirjutamine alampäringute ja nende jaoks muutujate abil muutub sasipuntraks. Mõnikord hakkate tormama erinevates IDE-akendes, püüdes oma koodi toimingute jada välja selgitada. See ei ole raske, kuid mõnikord on see väga tüütu.

ES6 jaoks puudub täielik tugi.

Võib esineda muid puudujääke, kuid ma pole midagi muud kohanud. Jagage teavet, kui teil on NJS-i kasutamisega negatiivne kogemus.

Järeldus

NJS on kerge avatud lähtekoodiga tõlk, mis võimaldab teil Nginxis rakendada erinevaid JavaScripti skripte. Selle väljatöötamisel pöörati suurt tähelepanu jõudlusele. Loomulikult on veel palju puudu, kuid projekti arendab väike meeskond ning nad lisavad aktiivselt uusi funktsioone ja parandavad vigu. Loodan, et kunagi võimaldab NJS ühendada väliseid mooduleid, mis muudab Nginxi funktsionaalsuse peaaegu piiramatuks. Kuid NGINX Plus on olemas ja tõenäoliselt pole funktsioone!

Hoidla artikli täieliku koodiga

njs-pypi koos AWS Sign v4 toega

Mooduli ngx_http_js_direktiivide kirjeldus

Ametlik NJS-i hoidla и dokumentatsiooni

NJS-i kasutamise näited Dmitri Volintsevilt

njs – nginxi natiivne JavaScripti skriptimine / Dmitri Volnjevi kõne Saint HighLoad++ 2019. aastal

NJS tootmises / Vassili Sošnikovi kõne HighLoad++ 2019. aastal

REST-i taotluste allkirjastamine ja autentimine AWS-is

Allikas: www.habr.com