Gihimo nako ang akong PyPI repository nga adunay pagtugot ug S3. Sa Nginx

Niini nga artikulo gusto nakong ipaambit ang akong kasinatian sa NJS, usa ka JavaScript interpreter alang sa Nginx nga gimugna sa Nginx Inc, nga naghulagway sa mga nag-unang kapabilidad niini gamit ang tinuod nga pananglitan. Ang NJS usa ka subset sa JavaScript nga nagtugot kanimo sa pagpalapad sa pagpaandar sa Nginx. Sa pangutana ngano imong kaugalingong interpreter??? Dmitry Volyntsev mitubag sa detalye. Sa laktud: Ang NJS kay nginx-way, ug ang JavaScript mas progresibo, "native" ug walay GC, dili sama ni Lua.

Kaniadto pa…

Sa akong katapusan nga trabaho, napanunod nako ang gitlab nga adunay daghang mga motley CI / CD pipelines nga adunay docker-compose, dind ug uban pang mga kalipayan, nga gibalhin sa mga riles sa kaniko. Ang mga hulagway nga gigamit kaniadto sa CI gibalhin sa ilang orihinal nga porma. Nagtrabaho sila sa husto hangtod sa adlaw nga ang among gitlab IP nausab ug ang CI nahimo nga kalabasa. Ang problema mao nga ang usa sa mga docker nga mga imahe nga miapil sa CI adunay git, nga nagbira sa mga module sa Python pinaagi sa ssh. Alang sa ssh kinahanglan nimo ang usa ka pribado nga yawe ug ... kini naa sa imahe kauban ang mga nailhan nga_host. Ug ang bisan unsang CI napakyas sa usa ka yawe nga sayup sa pag-verify tungod sa usa ka dili pagtugma tali sa tinuod nga IP ug sa usa nga gipiho sa nailhan_hosts. Ang usa ka bag-ong imahe dali nga na-assemble gikan sa naglungtad nga Dockfiles ug ang kapilian gidugang StrictHostKeyChecking no. Apan ang dili maayo nga lami nagpabilin ug adunay tinguha nga ibalhin ang mga libs sa usa ka pribadong PyPI repository. Usa ka dugang nga bonus, pagkahuman sa pagbalhin sa pribado nga PyPI, usa ka mas simple nga pipeline ug usa ka normal nga paghulagway sa mga kinahanglanon.txt

Ang pagpili nahimo na, mga ginoo!

Gipadagan namo ang tanan sa mga panganod ug Kubernetes, ug sa katapusan gusto namong makakuha og gamay nga serbisyo nga walay estado nga sudlanan nga adunay external storage. Aw, tungod kay gigamit namon ang S3, gihatagan kini ug prayoridad. Ug, kung mahimo, uban ang panghimatuud sa gitlab (mahimo nimong idugang kini sa imong kaugalingon kung kinahanglan).

Ang usa ka dali nga pagpangita nakahatag daghang mga resulta: s3pypi, pypicloud ug usa ka kapilian nga adunay "manwal" nga paghimo sa mga file sa html alang sa mga turnip. Ang katapusan nga kapilian nawala sa iyang kaugalingon.

s3pypi: Kini usa ka cli alang sa paggamit sa S3 hosting. Gi-upload namo ang mga file, gimugna ang html ug gi-upload kini sa samang balde. Angayan alang sa paggamit sa balay.

pypicloud: Ingon og usa ka makapaikag nga proyekto, apan pagkahuman sa pagbasa sa dokumentasyon nasagmuyo ako. Bisan pa sa maayo nga dokumentasyon ug ang abilidad sa pagpalapad aron mohaum sa imong mga panginahanglan, sa pagkatinuod kini nahimong sobra ug lisud i-configure. Ang pagtul-id sa code aron mohaum sa imong mga buluhaton, sumala sa mga banabana niadtong panahona, mokabat ug 3-5 ka adlaw. Ang serbisyo kinahanglan usab usa ka database. Gibiyaan namo kini sa kaso nga wala nay laing nakit-an.

Ang usa ka mas lawom nga pagpangita naghatag usa ka module alang sa Nginx, ngx_aws_auth. Ang resulta sa iyang pagsulay mao ang XML nga gipakita sa browser, nga nagpakita sa sulod sa S3 bucket. Ang katapusang commit sa panahon sa pagpangita usa ka tuig na ang milabay. Ang repository morag gibiyaan.

Pinaagi sa pag-adto sa tinubdan ug pagbasa PEP-503 Nakaamgo ko nga ang XML mahimong ma-convert sa HTML sa langaw ug ihatag sa pip. Pagkahuman sa pag-googling labi pa bahin sa Nginx ug S3, nakit-an nako ang usa ka pananglitan sa pag-authenticate sa S3 nga gisulat sa JS para sa Nginx. Mao to nakaila nako si NJS.

Gikuha kini nga pananglitan ingon usa ka sukaranan, usa ka oras ang milabay nakita nako sa akong browser ang parehas nga XML sama sa paggamit sa module nga ngx_aws_auth, apan ang tanan nasulat na sa JS.

Ganahan kaayo ko sa nginx solution. Una, maayo nga dokumentasyon ug daghang mga pananglitan, ikaduha, makuha namon ang tanan nga mga kaayohan sa Nginx alang sa pagtrabaho sa mga file (gikan sa kahon), ikatulo, bisan kinsa nga nahibal-an kung giunsa pagsulat ang mga configs para sa Nginx mahibal-an kung unsa. Ang Minimalism usa usab ka plus alang kanako, kon itandi sa Python o Go (kon gisulat gikan sa scratch), wala pay labot ang nexus.

TL;DR Human sa 2 ka adlaw, ang test version sa PyPi gigamit na sa CI.

Unsang paagi kini sa trabaho?

Ang module gikarga sa Nginx ngx_http_js_module, gilakip sa opisyal nga docker nga imahe. Gi-import namo ang among script gamit ang direktiba js_importsa pag-configure sa Nginx. Ang function gitawag pinaagi sa usa ka direktiba js_content. Ang direktiba gigamit sa pagtakda sa mga variable js_set, nga gikuha isip argumento lamang ang function nga gihulagway sa script. Apan mahimo naton ipatuman ang mga subquery sa NJS gamit lamang ang Nginx, dili bisan unsang XMLHttpRequest. Aron mahimo kini, ang katugbang nga lokasyon kinahanglan idugang sa pagsumpo sa Nginx. Ug ang script kinahanglan maghulagway sa usa ka subrequest niini nga lokasyon. Aron maka-access sa usa ka function gikan sa Nginx config, ang function name kinahanglan nga ma-export sa script mismo 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}

Kung gihangyo sa browser http://localhost:8080/ kita mosulod location /diin ang direktiba js_content nagtawag ug function request gihulagway sa among script script.js. Sa baylo, sa function request usa ka subquery ang gihimo sa location = /sub-query, nga adunay pamaagi (sa kasamtangan nga pananglitan GET) nga nakuha gikan sa argumento (r), gipasa sa dili klaro kung kini nga function gitawag. Ang tubag sa subrequest iproseso sa function call_back.

Pagsulay sa S3

Aron makapangayo sa pribadong storage sa S3, kinahanglan namo:

ACCESS_KEY

SECRET_KEY

S3_BUCKET

Gikan sa gigamit nga http nga pamaagi, ang kasamtangang petsa/oras, S3_NAME ug URI, usa ka matang sa string ang namugna, nga gipirmahan (HMAC_SHA1) gamit ang SECRET_KEY. Sunod mao ang usa ka linya sama sa AWS $ACCESS_KEY:$HASH, mahimong gamiton sa ulohan sa pagtugot. Ang parehas nga petsa/oras nga gigamit sa paghimo sa string sa miaging lakang kinahanglan idugang sa header X-amz-date. Sa code kini tan-awon sama niini:

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 authorization example, giusab ngadto sa deprecated 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}

Usa ka gamay nga katin-awan bahin sa _subrequest_uri: kini usa ka variable nga, depende sa inisyal nga uri, nagporma usa ka hangyo sa S3. Kung kinahanglan nimo nga makuha ang sulud sa "gamut", kinahanglan nimo nga maghimo usa ka hangyo sa uri nga nagpaila sa delimiter delimiter, nga ibalik ang usa ka lista sa tanan nga CommonPrefixes xml nga mga elemento, nga katumbas sa mga direktoryo (sa kaso sa PyPI, usa ka lista sa tanan nga mga pakete). Kung kinahanglan nimo nga makakuha usa ka lista sa mga sulud sa usa ka piho nga direktoryo (lista sa tanan nga mga bersyon sa pakete), nan ang hangyo sa uri kinahanglan adunay sulud nga prefix nga adunay ngalan sa direktoryo (package) nga kinahanglan matapos sa usa ka slash /. Kung dili, ang mga pagbangga posible kung mangayo sa sulud sa usa ka direktoryo, pananglitan. Adunay mga direktoryo nga aiohttp-hangyo ug aiohttp-hangyo ug kung ang hangyo nagtino /?prefix=aiohttp-request, unya ang tubag maglangkob sa mga sulud sa duha nga mga direktoryo. Kung adunay slash sa katapusan, /?prefix=aiohttp-request/, unya ang tubag maglangkob lamang sa gikinahanglan nga direktoryo. Ug kung mangayo kami usa ka file, nan ang resulta nga uri kinahanglan dili lahi sa orihinal.

I-save ug i-restart ang Nginx. Sa browser atong gisulod ang adres sa atong Nginx, ang resulta sa hangyo mao ang XML, pananglitan:

Listahan sa mga direktoryo

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

Gikan sa lista sa mga direktoryo kinahanglan ra nimo ang mga elemento CommonPrefixes.

Pinaagi sa pagdugang sa direktoryo nga kinahanglan namon sa among adres sa browser, madawat usab namon ang sulud niini sa porma nga XML:

Listahan sa mga file sa usa ka direktoryo

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

Gikan sa lista sa mga file kuhaon ra ang mga elemento Key.

Ang nahabilin mao ang pag-parse sa resulta nga XML ug ipadala kini isip HTML, nga gipulihan una ang Content-Type header sa text/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="/ceb/${reValue.groups.v}">${a_text}</a>`);
    }
  }

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

Gisulayan ang PyPI

Gisusi namon nga wala’y maguba bisan diin sa mga pakete nga nahibal-an nga nagtrabaho.

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

Gisubli namo ang among 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

Sa CI, ang paghimo ug pagkarga sa usa ka pakete ingon niini:

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

Pagpanghimatuud

Sa Gitlab posible nga gamiton ang JWT para sa authentication/authorization sa external services. Gamit ang auth_request direktiba sa Nginx, among i-redirect ang authentication data sa usa ka subrequest nga adunay function call sa script. Ang script maghimo ug lain nga subrequest sa Gitlab url ug kung ang datos sa pag-authenticate husto nga gitakda, unya ang Gitlab ibalik ang code 200 ug ang pag-upload / pag-download sa package tugutan. Ngano nga dili mogamit usa ka subquery ug ipadala dayon ang datos sa Gitlab? Tungod kay kinahanglan naton i-edit ang file sa pag-configure sa Nginx matag higayon nga maghimo kami bisan unsang mga pagbag-o sa pagtugot, ug kini usa ka labi ka kapoy nga buluhaton. Usab, kung ang Kubernetes naggamit sa usa ka read-only nga root filesystem nga polisiya, nan kini makadugang sa mas komplikado kung ilisan ang nginx.conf pinaagi sa configmap. Ug kini mahimong hingpit nga imposible nga i-configure ang Nginx pinaagi sa configmap samtang dungan nga naggamit sa mga palisiya nga nagdili sa koneksyon sa mga volume (pvc) ug read-only root filesystem (kini usab mahitabo).

Gamit ang intermediate sa NJS, nakuha namon ang higayon nga usbon ang mga espesipikong mga parameter sa nginx config gamit ang mga variable sa palibot ug maghimo pipila nga mga pagsusi sa script (pananglitan, usa ka sayup nga gipiho nga 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}

Lagmit ang pangutana mao ang paghimog serbesa: -Nganong dili mogamit ug andam nga mga modulo? Ang tanan nahimo na didto! Pananglitan, var AWS = require('aws-sdk') ug dili na kinahanglan nga magsulat og "bike" nga adunay S3 authentication!

Mopadayon kita sa mga kontra

Alang kanako, ang kawalay katakus sa pag-import sa gawas nga JS modules nahimong dili maayo, apan gipaabut nga bahin. Gihulagway sa panig-ingnan sa ibabaw nagkinahanglan('crypto') mao ang gitukod-sa modules ug nagkinahanglan lamang ug mga buhat alang kanila. Wala usab'y paagi nga magamit pag-usab ang code gikan sa mga script ug kinahanglan nimo nga kopyahon ug idikit kini sa lainlaing mga file. Nanghinaut ko nga sa umaabot nga kini nga pagpaandar ipatuman.

Ang compression kinahanglan usab nga ma-disable alang sa kasamtangan nga proyekto sa Nginx gzip off;

Tungod kay walay gzip module sa NJS ug imposible nga makonektar kini; busa, walay paagi sa pagtrabaho uban sa compressed data. Tinuod, dili gyud kini minus alang sa kini nga kaso. Wala’y daghang teksto, ug ang gibalhin nga mga file na-compress na ug ang dugang nga pag-compress dili kaayo makatabang kanila. Usab, dili kini usa ka puno o kritikal nga serbisyo nga kinahanglan nimo nga hagoon sa paghatud sa sulud sa pipila ka mga millisecond nga mas paspas.

Ang pag-debug sa script nagkinahanglan og taas nga panahon ug posible lamang pinaagi sa "mga print" sa error.log. Depende sa gitakda nga impormasyon sa lebel sa pag-log, pahimangno o sayup, posible nga gamiton ang 3 nga mga pamaagi r.log, r.warn, r.error matag usa. Gisulayan nako nga i-debug ang pipila ka mga script sa Chrome (v8) o ang tool sa njs console, apan dili tanan masusi didto. Kung ang debugging code, aka functional testing, ang kasaysayan ingon niini:

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

ug adunay mahimo nga gatusan sa ingon nga mga han-ay.

Ang pagsulat sa kodigo gamit ang mga subquery ug mga baryable alang kanila nahimong usa ka tangled tangle. Usahay magsugod ka sa pagdali sa lainlaing IDE windows nga naningkamot nga mahibal-an ang han-ay sa mga aksyon sa imong code. Dili kini lisud, apan usahay kini makalagot kaayo.

Walay hingpit nga suporta alang sa ES6.

Mahimong adunay uban nga mga kakulangan, apan wala ako'y lain nga nasugatan. Ipaambit ang impormasyon kung ikaw adunay negatibo nga kasinatian sa paggamit sa NJS.

konklusyon

Ang NJS usa ka gaan nga open-source interpreter nga nagtugot kanimo sa pagpatuman sa lainlaing mga script sa JavaScript sa Nginx. Sa panahon sa pag-uswag niini, dako nga pagtagad ang gihatag sa pasundayag. Siyempre, daghan pa ang kulang, apan ang proyekto gipalambo sa usa ka gamay nga grupo ug sila aktibo nga nagdugang bag-ong mga bahin ug nag-ayo sa mga bug. Nanghinaut ko nga sa umaabot nga adlaw ang NJS magtugot kanimo sa pagkonektar sa mga eksternal nga module, nga maghimo sa Nginx nga halos walay limitasyon. Apan adunay NGINX Plus ug lagmit nga wala’y mga bahin!

Ang repository nga adunay bug-os nga code alang sa artikulo

njs-pypi nga adunay suporta sa AWS Sign v4

Deskripsyon sa mga direktiba sa ngx_http_js_module module

Opisyal nga NJS repository ΠΈ dokumentasyon

Mga pananglitan sa paggamit sa NJS gikan sa Dmitry Volintsev

njs - lumad nga JavaScript scripting sa nginx / Pagpamulong ni Dmitry Volnyev sa Saint HighLoad++ 2019

NJS sa produksiyon / Pakigpulong ni Vasily Soshnikov sa HighLoad++ 2019

Pagpirma ug Pagpamatuod sa REST nga mga Pangayo sa AWS

Source: www.habr.com