Aku nggawe repositori PyPI dhewe kanthi wewenang lan S3. Ing Nginx

Ing artikel iki, aku pengin nuduhake pengalaman karo NJS, juru basa JavaScript kanggo Nginx sing dikembangake dening Nginx Inc, nggambarake kemampuan utamane nggunakake conto nyata. NJS minangka subset saka JavaScript sing ngidini sampeyan nambah fungsi Nginx. Kanggo pitakonan kok interpreter dhewe??? Dmitry Volyntsev mangsuli kanthi rinci. Singkat: NJS iku nginx-way, lan JavaScript luwih progresif, "native" lan tanpa GC, ora kaya Lua.

Jaman biyen…

Ing proyek pungkasan, aku marisi gitlab karo sawetara motley CI / pipelines CD karo docker-nyipta, dind lan ditresnani liyane, kang ditransfer kanggo ril kaniko. Gambar sing sadurunge digunakake ing CI dipindhah ing wangun asline. Padha makarya kanthi bener nganti dina nalika IP gitlab kita diganti lan CI dadi waluh. Masalahe yaiku salah sawijining gambar docker sing melu CI duwe git, sing narik modul Python liwat ssh. Kanggo ssh sampeyan butuh kunci pribadi lan ... ana ing gambar bebarengan karo known_hosts. Lan sembarang CI gagal karo kesalahan verifikasi tombol amarga ora cocog antarane IP nyata lan sing ditemtokake ing known_hosts. Gambar anyar dirakit kanthi cepet saka Dockfiles sing ana lan opsi kasebut ditambahake StrictHostKeyChecking no. Nanging rasa ala tetep lan ana kepinginan kanggo mindhah libs menyang repositori PyPI pribadi. Bonus tambahan, sawise ngalih menyang PyPI pribadi, ana pipa prasaja lan gambaran normal requirement.txt

Pilihan wis digawe, Tuan-tuan!

We mbukak kabeh ing mega lan Kubernetes, lan ing pungkasan kita wanted kanggo njaluk layanan cilik sing wadhah stateless karo panyimpenan external. Inggih, awit kita nggunakake S3, prioritas diwenehi kanggo. Lan, yen bisa, kanthi otentikasi ing gitlab (sampeyan bisa nambah dhewe yen perlu).

Panelusuran cepet ngasilake sawetara asil: s3pypi, pypicloud lan pilihan kanthi "manual" nggawe file html kanggo turnips. Pilihan pungkasan ilang dhewe.

s3pypi: Iki cli kanggo nggunakake S3 hosting. Kita upload file, generate html lan upload menyang ember padha. Cocog kanggo nggunakake omah.

pypicloud: Iku ketoke kaya proyek menarik, nanging sawise maca dokumentasi aku wagol. Senadyan dokumentasi apik lan kemampuan kanggo nggedhekake kanggo cocog karo kabutuhan, ing kasunyatan iku dadi keluwih lan angel kanggo ngatur. Mbenerake kode sing cocog karo tugas sampeyan, miturut prakiraan ing wektu kasebut, butuh 3-5 dina. Layanan kasebut uga mbutuhake database. Kita ninggalake yen ora nemokake apa-apa liyane.

Panelusuran sing luwih jero ngasilake modul kanggo Nginx, ngx_aws_auth. Asil tes kasebut yaiku XML sing ditampilake ing browser, sing nuduhake isi ember S3. Komitmen pungkasan ing wektu panelusuran yaiku setahun kepungkur. Repositori katon ditinggal.

Kanthi pindhah menyang sumber lan maca PEP-503 Aku nyadari yen XML bisa diowahi dadi HTML kanthi cepet lan diwenehi pip. Sawise googling luwih akeh babagan Nginx lan S3, aku nemokake conto otentikasi ing S3 sing ditulis ing JS kanggo Nginx. Mangkene aku ketemu NJS.

Njupuk conto iki minangka basis, jam mengko aku weruh ing browser sandi XML padha nalika nggunakake modul ngx_aws_auth, nanging kabeh wis ditulis ing JS.

Aku seneng banget karo solusi nginx. Kaping pisanan, dokumentasi sing apik lan akeh conto, nomer loro, kita entuk kabeh barang Nginx kanggo nggarap file (saka kothak), katelu, sapa wae sing ngerti carane nulis konfigurasi kanggo Nginx bakal bisa ngerti apa. Minimalism uga plus kanggo kula, dibandhingake Python utawa Go (yen ditulis saka ngeruk), ora kanggo sebutno nexus.

TL;DR Sawise 2 dina, versi test PyPi wis digunakake ing CI.

Carane ora iku bisa?

Modul kasebut dimuat menyang Nginx ngx_http_js_module, kalebu ing gambar docker resmi. Kita ngimpor skrip kita nggunakake arahan js_importmenyang konfigurasi Nginx. Fungsi kasebut diarani direktif js_content. Directive digunakake kanggo nyetel variabel js_set, sing njupuk minangka argumen mung fungsi sing diterangake ing skrip. Nanging kita bisa nglakokake subqueries ing NJS mung nggunakake Nginx, dudu XMLHttpRequest. Kanggo nindakake iki, lokasi sing cocog kudu ditambahake menyang konfigurasi Nginx. Lan script kudu njlΓ¨ntrΓ¨hakΓ© subrequest kanggo lokasi iki. Kanggo bisa ngakses fungsi saka konfigurasi Nginx, jeneng fungsi kudu diekspor ing skrip kasebut 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}

Nalika dijaluk ing browser http://localhost:8080/ kita mlebu location /kang direktif js_content nelpon fungsi request diterangake ing naskah kita script.js. Ing siji, ing fungsi request subquery digawe kanggo location = /sub-query, kanthi metode (ing conto saiki GET) sing dipikolehi saka argumentasi (r), liwati implisit nalika fungsi iki diarani. Tanggapan subrequest bakal diproses ing fungsi kasebut call_back.

Nyoba S3

Kanggo nggawe panjalukan kanggo panyimpenan S3 pribadi, kita kudu:

ACCESS_KEY

SECRET_KEY

S3_BUCKET

Saka cara http sing digunakake, tanggal / wektu saiki, S3_NAME lan URI, jinis string tartamtu digawe, sing ditandatangani (HMAC_SHA1) nggunakake SECRET_KEY. Sabanjure yaiku baris kaya AWS $ACCESS_KEY:$HASH, bisa digunakake ing header wewenang. Tanggal / wektu sing padha digunakake kanggo ngasilake senar ing langkah sadurunge kudu ditambahake menyang header X-amz-date. Ing kode katon kaya iki:

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(Conto wewenang AWS Sign v2, diganti status ora digunakake)

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}

A panjelasan sethitik babagan _subrequest_uri: iki variabel sing, gumantung ing uri awal, mbentuk panjalukan kanggo S3. Yen sampeyan kudu entuk isi "root", sampeyan kudu nggawe panjalukan uri sing nuduhake delimiter delimiter, sing bakal ngasilake dhaptar kabeh unsur xml CommonPrefixes, sing cocog karo direktori (ing kasus PyPI, dhaptar kabeh paket). Yen sampeyan kudu entuk dhaptar isi ing direktori tartamtu (dhaptar kabeh versi paket), banjur panjalukan uri kudu ngemot kolom awalan kanthi jeneng direktori (paket) kudu diakhiri karo garis miring /. Yen ora, tabrakan bisa uga nalika njaluk isi direktori, contone. Ana direktori aiohttp-request lan aiohttp-requests lan yen panjalukan kasebut /?prefix=aiohttp-request, banjur respon bakal ngemot isi loro direktori. Yen ing pungkasan ana garis miring, /?prefix=aiohttp-request/, banjur respon bakal ngemot mung direktori sing dibutuhake. Lan yen kita njaluk file, mula asil uri ngirim ora beda karo asline.

Simpen lan miwiti maneh Nginx. Ing browser kita ngetik alamat Nginx kita, asil panyuwunan bakal XML, contone:

Dhaptar direktori

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

Saka dhaptar direktori sampeyan mung butuh unsur CommonPrefixes.

Kanthi nambahake direktori sing kita butuhake menyang alamat kita ing browser, kita uga bakal nampa isine ing wangun XML:

Dhaptar file ing direktori

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

Saka dhaptar file kita bakal njupuk mung unsur Key.

Kabeh sing isih ana yaiku ngurai XML sing diasilake lan dikirim minangka HTML, kanthi ngganti header Content-Type kanthi teks/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="/jw/${reValue.groups.v}">${a_text}</a>`);
    }
  }

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

Nyoba PyPI

Kita mriksa manawa ora ana sing rusak ing paket sing bisa digunakake.

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

Kita mbaleni karo libs kita.

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

Ing CI, nggawe lan mbukak paket katon kaya iki:

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

Otentikasi

Ing Gitlab bisa nggunakake JWT kanggo otentikasi / wewenang layanan eksternal. Nggunakake arahan auth_request ing Nginx, kita bakal ngarahake data otentikasi menyang subrequest sing ngemot panggilan fungsi ing skrip. Skrip bakal nggawe subrequest liyane menyang url Gitlab lan yen data otentikasi kasebut kanthi bener, banjur Gitlab bakal ngasilake kode 200 lan upload / download paket bakal diidini. Napa ora nggunakake siji subquery lan langsung ngirim data menyang Gitlab? Amarga banjur kita kudu ngowahi file konfigurasi Nginx saben-saben kita nggawe owah-owahan ing wewenang, lan iki minangka tugas sing rada mboseni. Uga, yen Kubernetes nggunakake kabijakan sistem file oyod mung diwaca, mula iki bakal nambah kerumitan nalika ngganti nginx.conf liwat configmap. Lan pancen mokal kanggo ngatur Nginx liwat configmap nalika nggunakake kabijakan sing nglarang sambungan volume (pvc) lan sistem file root mung diwaca (iki uga kedadeyan).

Nggunakake penengah NJS, kita entuk kesempatan kanggo ngganti paramèter sing ditemtokake ing konfigurasi nginx nggunakake variabel lingkungan lan nindakake sawetara mriksa ing skrip (contone, URL sing ora ditemtokake kanthi bener).

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}

Paling kamungkinan pitakonan punika brewing: -Apa ora nggunakake modul siap-digawe? Kabeh wis rampung ana! Contone, var AWS = mbutuhake ('aws-sdk') lan ora perlu kanggo nulis "mancal" karo bukti asli S3!

Ayo dadi pindhah menyang cons

Kanggo kula, ora bisa ngimpor modul JS eksternal dadi fitur sing ora nyenengake, nanging samesthine. Diterangake ing conto ing ndhuwur require('crypto') punika mbangun-in modul lan mung mbutuhake karya kanggo wong-wong mau. Ora ana cara kanggo nggunakake maneh kode saka skrip lan sampeyan kudu nyalin lan nempel menyang file sing beda. Muga-muga ing sawijining dina fungsi iki bakal ditindakake.

Kompresi uga kudu dipateni kanggo proyek saiki ing Nginx gzip off;

Amarga ora ana modul gzip ing NJS lan ora bisa disambungake; mula, ora ana cara kanggo nggarap data sing dikompres. Bener, iki dudu minus kanggo kasus iki. Ora akeh teks, lan file sing ditransfer wis dikompres lan kompresi tambahan ora bakal mbantu. Uga, iki dudu layanan sing dimuat utawa kritis sing sampeyan kudu repot ngirim konten sawetara milidetik luwih cepet.

Debugging script njupuk wektu dawa lan mung bisa liwat "prints" ing error.log. Gumantung ing info tingkat logging pesawat, ngelekake utawa kesalahan, iku bisa nggunakake mungguh 3 cara r.log, r.warn, r.error. Aku nyoba debug sawetara skrip ing Chrome (v8) utawa alat console njs, nanging ora kabeh bisa dicenthang ana. Nalika kode debugging, alias testing fungsional, riwayat katon kaya iki:

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

lan ana bisa atusan urutan kuwi.

Nulis kode nggunakake subquery lan variabel kanggo wong-wong mau dadi kusut kusut. Kadhangkala sampeyan miwiti cepet-cepet ngubengi windows IDE sing beda-beda nyoba ngerteni urutan tumindak kode sampeyan. Ora angel, nanging kadhangkala banget ngganggu.

Ora ana dhukungan lengkap kanggo ES6.

Bisa uga ana sawetara kekurangan liyane, nanging aku ora nemoni apa-apa liyane. Nuduhake informasi yen sampeyan duwe pengalaman negatif nggunakake NJS.

kesimpulan

NJS minangka interpreter open-source sing entheng sing ngidini sampeyan ngetrapake macem-macem skrip JavaScript ing Nginx. Sajrone pembangunan, manungsa waΓ© gedhe wis mbayar kanggo kinerja. Mesthine, isih akeh sing ilang, nanging proyek kasebut dikembangake dening tim cilik lan aktif nambah fitur-fitur anyar lan ndandani bug. Muga-muga ing sawijining dina NJS bakal ngidini sampeyan nyambungake modul eksternal, sing bakal nggawe fungsi Nginx meh ora ana watesan. Nanging ana NGINX Plus lan kemungkinan ora ana fitur!

Repositori kanthi kode lengkap kanggo artikel kasebut

njs-pypi kanthi dhukungan AWS Sign v4

Katrangan arahan modul ngx_http_js_module

Repositori NJS resmi ΠΈ dokumentasi

Conto nggunakake NJS saka Dmitry Volintsev

njs - skrip JavaScript asli ing nginx / Wicara dening Dmitry Volnyev ing Saint HighLoad++ 2019

NJS ing produksi / Wicara dening Vasily Soshnikov ing HighLoad++ 2019

Ndhaptar lan Authenticating Panjaluk REST ing AWS

Source: www.habr.com