Kuring nyieun gudang PyPI sorangan kalawan otorisasina sarta S3. Dina Nginx

Dina tulisan ieu kuring hoyong bagikeun pangalaman kuring sareng NJS, juru basa JavaScript pikeun Nginx dikembangkeun ku Nginx Inc, ngajelaskeun kamampuan utami na nganggo conto anu nyata. NJS mangrupikeun sawaréh tina JavaScript anu ngamungkinkeun anjeun manjangkeun fungsionalitas Nginx. Pikeun patarosan naha juru basa sorangan??? Dmitry Volyntsev ngajawab di jéntré. Pondokna: NJS nyaéta nginx-way, sareng JavaScript langkung kutang, "asli" sareng tanpa GC, teu sapertos Lua.

Lami-lami ...

Dina pakasaban panungtungan kuring, kuring diwariskeun gitlab kalawan jumlah motley CI / pipelines CD kalawan docker-ngarang, dind na nikmat séjén, nu dipindahkeun ka rel kaniko. Gambar anu saacanna dianggo dina CI dipindahkeun dina bentuk aslina. Aranjeunna damel leres dugi ka dinten nalika IP gitlab kami robih sareng CI janten waluh. Masalahna éta salah sahiji gambar docker anu milu dina CI miboga git, nu ditarik modul Python via ssh. Pikeun ssh anjeun peryogi konci pribadi sareng ... éta dina gambar sareng known_hosts. Sareng naon waé CI gagal kalayan kasalahan verifikasi konci kusabab teu cocog antara IP nyata sareng anu ditunjuk dina known_hosts. Hiji gambar anyar ieu gancang dirakit ti Dockfiles aya na pilihan ieu ditambahkeun StrictHostKeyChecking no. Tapi rasa goréng tetep sarta aya kahayang pikeun mindahkeun libs ka gudang PyPI swasta. Hiji bonus tambahan, sanggeus pindah ka PyPI swasta, éta pipa basajan tur pedaran normal tina requirements.txt

Pilihan geus dijieun, Gentlemen!

Urang ngajalankeun sagalana dina awan na Kubernetes, sarta dina tungtungna urang hayang meunang layanan leutik nu éta wadah stateless kalawan gudang éksternal. Nya, saprak urang nganggo S3, prioritasna dipasihkeun. Sareng, upami mungkin, kalayan auténtikasi di gitlab (anjeun tiasa nambihanana nyalira upami diperyogikeun).

A pilarian rusuh yielded sababaraha hasil: s3pypi, pypicloud sarta hiji pilihan jeung "manual" kreasi file html pikeun turnips. Pilihan panungtungan ngiles ku sorangan.

s3pypi: Ieu cli pikeun ngagunakeun S3 hosting. Kami unggah file, ngahasilkeun html sareng unggah kana ember anu sami. Cocog jeung pamakéan imah.

pypicloud: Ieu seemed kawas hiji proyék metot, Tapi sanggeus maca dokuméntasi kuring kuciwa. Sanaos dokuméntasi anu saé sareng kamampuan pikeun dilegakeun pikeun nyocogkeun ka kabutuhan anjeun, kanyataanna tétéla kaleuleuwihan sareng sesah dikonpigurasikeun. Koréksi kodeu pikeun nyocogkeun tugas anjeun, numutkeun perkiraan dina waktos éta, bakal nyandak 3-5 dinten. Palayanan ogé peryogi pangkalan data. Urang ninggalkeun eta bisi urang teu manggihan nanaon sejenna.

Pilarian anu langkung jero ngahasilkeun modul pikeun Nginx, ngx_aws_auth. Hasil tina tés nya éta XML dipintonkeun dina browser, nu nembongkeun eusi ember S3. Komitmen terakhir dina waktos milarian éta sataun katukang. Repository katingal ditinggalkeun.

Ku jalan ka sumberna jeung macana PEP-503 Kuring sadar yen XML bisa dirobah jadi HTML dina laleur jeung dibikeun ka pip. Saatos googling langkung seueur ngeunaan Nginx sareng S3, kuring mendakan conto auténtikasi dina S3 anu ditulis dina JS pikeun Nginx. Éta kumaha kuring patepung NJS.

Nyandak conto ieu salaku dadasar, sajam engké kuring nempo dina panyungsi kuring XML sarua salaku nalika ngagunakeun modul ngx_aws_auth, tapi sagalana geus ditulis dina JS.

Abdi resep pisan kana solusi nginx. Anu mimiti, dokuméntasi anu saé sareng seueur conto, kadua, urang nampi sagala kaéndahan Nginx pikeun damel sareng file (out of the box), katilu, saha waé anu terang kumaha nyerat configs pikeun Nginx bakal tiasa terang naon naon. Minimalism oge tambah keur kuring, dibandingkeun Python atanapi Go (lamun ditulis ti scratch), teu nyebut nexus.

TL;DR Saatos 2 dinten, versi uji PyPi parantos dianggo dina CI.

Kumaha carana sangkan eta pagawean?

Modulna dimuat kana Nginx ngx_http_js_module, kaasup dina gambar docker resmi. Urang ngimpor naskah kami nganggo diréktif js_importkana konfigurasi Nginx. Fungsina disebut ku diréktif js_content. Diréktif dipaké pikeun nyetel variabel js_set, nu nyokot salaku argumen ngan fungsi dijelaskeun dina naskah. Tapi urang tiasa ngaéksekusi subqueries di NJS ngan nganggo Nginx, sanés XMLHttpRequest. Jang ngalampahkeun ieu, lokasi saluyu kudu ditambahkeun kana konfigurasi Nginx. Sareng naskah kedah ngajelaskeun subrequest ka lokasi ieu. Pikeun tiasa ngaksés fungsi tina konfigurasi Nginx, nami fungsina kedah diékspor dina naskah éta sorangan 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 dipénta dina browser http://localhost:8080/ urang asup kana location /di mana diréktif js_content nelepon hiji fungsi request dijelaskeun dina naskah urang script.js. Sabalikna, dina fungsi request subquery a dijieun pikeun location = /sub-query, kalayan padika (dina conto ayeuna GET) dicandak tina argumen (r), diliwatan implisit lamun fungsi ieu disebut. Respon subrequest bakal diolah dina fungsi call_back.

Nyobian S3

Pikeun ngadamel pamundut ka panyimpenan S3 swasta, urang peryogi:

ACCESS_KEY

SECRET_KEY

S3_BUCKET

Tina metodeu http anu dianggo, tanggal / waktos ayeuna, S3_NAME sareng URI, ngahasilkeun jinis string anu tangtu, anu ditandatanganan (HMAC_SHA1) nganggo SECRET_KEY. Salajengna nyaéta garis kawas AWS $ACCESS_KEY:$HASH, bisa dipaké dina lulugu otorisasina. Tanggal / waktos anu sami anu dianggo pikeun ngahasilkeun senar dina léngkah anu saacanna kedah ditambah kana lulugu X-amz-date. Dina kode sigana kieu:

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 otorisasina AWS Sign v2, dirobih janten status anu teu dianggo)

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 katerangan saeutik ngeunaan _subrequest_uri: ieu variabel anu, gumantung kana uri awal, ngabentuk pamundut ka S3. Upami anjeun kedah kéngingkeun eusi "root", maka anjeun kedah nyiptakeun pamundut uri anu nunjukkeun pembatas. delimiter, anu bakal ngabalikeun daptar sadaya elemen xml CommonPrefixes, pakait sareng diréktori (dina kasus PyPI, daptar sadaya bungkusan). Upami anjeun kedah kéngingkeun daptar eusi dina diréktori khusus (daptar sadaya versi pakét), maka pamundut uri kedah ngandung widang awalan kalayan nami diréktori (pakét) kedah ditungtungan ku garis miring /. Upami teu kitu, tabrakan tiasa lumangsung nalika menta eusi diréktori, contona. Aya directories aiohttp-request jeung aiohttp-requests sarta lamun pamundut nu nangtukeun /?prefix=aiohttp-request, lajeng respon bakal ngandung eusi duanana directories. Mun aya garis miring di tungtung, /?prefix=aiohttp-request/, lajeng respon bakal ngandung ukur diréktori diperlukeun. Sareng upami urang nyuhunkeun file, maka uri anu hasilna henteu kedah béda ti anu asli.

Simpen sareng balikan deui Nginx. Dina panyungsi urang lebetkeun alamat Nginx urang, hasil pamundut bakal XML, contona:

Daptar diréktori

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

Tina daptar diréktori anjeun ngan ukur peryogi unsur-unsurna CommonPrefixes.

Ku nambihan diréktori anu urang peryogikeun kana alamatna dina browser, urang ogé bakal nampi eusina dina bentuk XML:

Daptar file dina diréktori

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

Tina daptar file kami ngan ukur nyandak elemen Key.

Sadaya anu tetep nyaéta pikeun nga-parse XML anu hasilna sareng ngirimkeunana salaku HTML, saatos mimiti ngagentos header Content-Type sareng téks / 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="/su/${reValue.groups.v}">${a_text}</a>`);
    }
  }

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

Nyobian PyPI

Kami pariksa yén teu aya anu ngarecah di mana waé dina bungkusan anu dipikanyaho dianggo.

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

Urang ulang kalawan libs urang.

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

Dina CI, nyiptakeun sareng ngamuat pakét sapertos kieu:

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

Konfirmasi

Dina Gitlab kasebut nyaéta dimungkinkeun pikeun ngagunakeun JWT pikeun auténtikasi / otorisasina jasa éksternal. Ngagunakeun diréktif auth_request di Nginx, urang bakal alihan data auténtikasi kana subrequest ngandung panggero fungsi dina naskah. Skrip bakal nyieun subrequest sejen ka url Gitlab sarta lamun data auténtikasi ieu dieusian leres, lajeng Gitlab bakal balik kode 200 sarta unggah / ngundeur pakét bakal diwenangkeun. Naha henteu nganggo hiji subquery sareng langsung ngirim data ka Gitlab? Kusabab teras urang kedah ngédit file konfigurasi Nginx unggal waktos urang ngarobih otorisasina, sareng ieu mangrupikeun tugas anu rada pikasieuneun. Ogé, upami Kubernetes nganggo kabijakan sistem file akar anu dibaca wungkul, maka ieu nambihan langkung rumit nalika ngagentos nginx.conf via configmap. Sareng janten teu mungkin pikeun ngonpigurasikeun Nginx via configmap bari sakaligus nganggo kawijakan anu ngalarang sambungan volume (pvc) sareng sistem file akar anu dibaca wungkul (ieu ogé kajadian).

Ngagunakeun NJS panengah, urang meunang kasempetan pikeun ngarobah parameter dieusian dina nginx config ngagunakeun variabel lingkungan sarta ngalakukeun sababaraha cék dina naskah (contona, URL salah dieusian).

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 dipikaresep patarosan brewing: -Naha teu make modul siap-dijieun? Sagalana geus rengse aya! Contona, var AWS = merlukeun ('aws-sdk') jeung teu perlu nulis "sapédah" kalawan auténtikasi S3!

Hayu urang ngaléngkah ka kontra

Pikeun kuring, henteu mampuh ngimpor modul JS éksternal janten pikaresepeun, tapi fitur anu dipiharep. Dijelaskeun dina conto di luhur merlukeun('crypto') nyaeta ngawangun-di modul sarta merlukeun ukur gawéna pikeun aranjeunna. Teu aya deui jalan pikeun ngagunakeun deui kode tina skrip sareng anjeun kedah nyalin sareng nempelkeun kana file anu béda. Kuring miharep yén someday fungsionalitas ieu bakal dilaksanakeun.

Komprési ogé kedah ditumpurkeun pikeun proyék ayeuna di Nginx gzip off;

Kusabab teu aya modul gzip di NJS sareng teu mungkin pikeun nyambungkeunana; ku kituna, teu aya jalan pikeun damel sareng data anu dikomprés. Leres, ieu sanés minus pikeun kasus ieu. Teu aya seueur téks, sareng file anu ditransfer parantos dikomprés sareng komprési tambahan moal ngabantosan aranjeunna. Ogé, ieu sanés jasa anu sarat atanapi kritis anu anjeun kedah ganggu ngirimkeun kontén sababaraha milidetik langkung gancang.

Debugging naskah butuh waktu lila sarta ngan mungkin ngaliwatan "prints" dina error.log. Gumantung kana set logging inpo tingkat, ngingetkeun atawa kasalahan, kasebut nyaéta dimungkinkeun pikeun ngagunakeun 3 métode r.log, r.warn, r.error mungguh. Kuring nyoba debug sababaraha Aksara dina Chrome (v8) atawa alat konsol njs, tapi teu sagalana bisa dipariksa aya. Nalika kode debugging, alias tés fungsional, sajarah sapertos kieu:

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

tur tiasa aya ratusan runtuyan sapertos.

Nulis kode ngagunakeun subqueries sarta variabel pikeun aranjeunna robah jadi kusut kusut. Kadang-kadang anjeun mimiti rushing sabudeureun IDE béda windows nyoba angka kaluar runtuyan lampah kode Anjeun. Henteu hese, tapi sakapeung sok ngaganggu.

Henteu aya dukungan lengkep pikeun ES6.

Meureun aya sababaraha shortcomings séjén, tapi kuring teu encountered nanaon sejenna. Bagikeun inpormasi upami anjeun gaduh pangalaman négatip nganggo NJS.

kacindekan

NJS mangrupikeun juru basa open-source anu ringan anu ngamungkinkeun anjeun ngalaksanakeun sababaraha skrip JavaScript dina Nginx. Salila perkembangannya, perhatian hébat ieu dibayar ka kinerja. Tangtosna, masih seueur anu leungit, tapi proyékna dikembangkeun ku tim alit sareng aranjeunna aktip nambihan fitur-fitur anyar sareng ngalereskeun bug. Kuring miharep yén someday NJS bakal ngidinan Anjeun pikeun nyambungkeun modul éksternal, nu bakal nyieun fungsionalitas Nginx ampir taya. Tapi aya NGINX Plus sareng paling dipikaresep moal aya fitur!

Repository sareng kode lengkep pikeun tulisan

njs-pypi kalayan dukungan AWS Sign v4

Katerangan ngeunaan diréktif modul ngx_http_js_module

Repository NJS resmi и dokuméntasi

Conto ngagunakeun NJS ti Dmitry Volintsev

njs - skrip JavaScript asli di nginx / Biantara ku Dmitry Volnyev di Saint HighLoad++ 2019

NJS dina produksi / Biantara ku Vasily Soshnikov di HighLoad++ 2019

Ngadaptar sareng Ngabuktoskeun Permintaan REST di AWS

sumber: www.habr.com