Mən PyPI repozitoriyamı avtorizasiya və S3 ilə yaratdım. Nginx-də

Bu yazıda mən Nginx Inc tərəfindən hazırlanmış Nginx üçün JavaScript tərcüməçisi olan NJS ilə təcrübəmi bölüşmək istəyirəm və onun əsas xüsusiyyətlərini real nümunədən istifadə edərək təsvir etmək istəyirəm. NJS, Nginx-in funksionallığını genişləndirməyə imkan verən JavaScript alt dəstidir. Sual niyə tərcüməçiniz??? Dmitri Volyntsev ətraflı cavab verdi. Qısacası: NJS nginx yoludur və JavaScript Luadan fərqli olaraq daha mütərəqqi, "doğma" və GC-sizdir.

Uzun müddət əvvəl ...

Son işimdə mənə docker-compose, dind və kaniko relslərinə köçürülmüş digər ləzzətləri olan bir sıra rəngarəng CI / CD boru kəmərləri ilə gitlabı miras aldım. Əvvəllər CI-də istifadə edilən şəkillər orijinal formada köçürülüb. Gitlabımız IP-ni dəyişdirənə və CI balqabaq halına gələnə qədər onlar düzgün işlədilər. Problem onda idi ki, CI-də iştirak edən docker təsvirlərindən birində Python modullarını ssh vasitəsilə çəkən git var. ssh şəxsi açara ehtiyac duyur və ... o, məlum_hosts ilə birlikdə şəkildə idi. Və hər hansı bir CI real IP ilə məlum_hosts-da göstərilən arasında uyğunsuzluq səbəbindən əsas yoxlama xətası ilə başa çatdı. Mövcud Dockfiles-dən tez bir zamanda yeni bir şəkil yığıldı və seçim əlavə edildi StrictHostKeyChecking no. Ancaq xoşagəlməz dad qaldı və faylları şəxsi PyPI deposuna köçürmək istəyi var idi. Şəxsi PyPI-yə keçdikdən sonra əlavə bonus daha sadə boru kəməri və tələblər.txt-in normal təsviri idi.

Seçim edildi, cənablar!

Biz hər şeyi buludlarda və Kubernetlərdə fırladıq və sonda xarici yaddaşa malik vətəndaşlığı olmayan konteyner olan kiçik bir xidmət əldə etmək istədik. Yaxşı, S3 istifadə etdiyimiz üçün prioritet bunun arxasında idi. Mümkünsə, gitlab-da autentifikasiya ilə (lazım olduqda özünüz əlavə edə bilərsiniz).

Sürətli axtarış s3pypi, pypicloud və şalgam üçün "əllə" html faylı yaratmaq üçün bir neçə nəticə verdi. Son seçim öz-özünə yox oldu.

s3pypi: Bu, S3 hostinqindən istifadə üçün clidir. Faylları yükləyin, html yaradın və onları eyni kovaya yükləyin. Ev istifadəsi üçün uyğundur.

pypicloud: Maraqlı layihə kimi görünürdü, lakin sənədləri oxuduqdan sonra əsəbiləşdi. Yaxşı sənədlərə və tapşırıqlarınıza uyğun olaraq genişləndirmə imkanlarına baxmayaraq, əslində lazımsız və konfiqurasiya etmək çətin olduğu ortaya çıxdı. Tapşırıqlarınız üçün kodun düzəldilməsi, o zamankı hesablamalara görə, 3-5 gün çəkəcək. Xidmətə həmçinin məlumat bazası lazımdır. Başqa bir şey tapmadıqca onu tərk etdik.

Daha dərin axtarış nəticəsində Nginx, ngx_aws_auth modulu tapıldı. Onun sınağının nəticəsi S3 vedrəsinin məzmununu göstərən brauzerdə XML göstərildi. Axtarış zamanı sonuncu əməliyyat bir il əvvəl olub. Anbar tərk edilmiş görünürdü.

Orijinal mənbəyə müraciət edib oxumaq PEP-503 XML-in tez HTML-ə çevrilə və pip-ə verilə biləcəyini başa düşdü. Nginx və S3-ə görə bir az daha çox axtararkən Nginx üçün JS-də yazılmış S3-də autentifikasiya nümunəsinə rast gəldim. Mən NJS ilə belə tanış oldum.

Bu nümunəni əsas götürərək, bir saat sonra brauzerimdə ngx_aws_auth modulundan istifadə edərkən eyni XML-i müşahidə etdim, lakin hər şey artıq JS-də yazılmışdı.

Nginx həllini çox bəyəndim. Birincisi, yaxşı sənədlər və çoxlu nümunələr, ikincisi, biz fayllarla işləmək üçün bütün Nginx yaxşılıqlarını alırıq (qutudan kənar), üçüncüsü, Nginx konfiqurasiyalarını necə yazmağı bilən hər kəs nəyin nə olduğunu anlaya biləcək. Python və ya Go (əgər sıfırdan yazılmışsa) ilə müqayisədə minimalizm mənim üçün bir artıdır, nexusdan danışmırıq.

TL;DR 2 gündən sonra PyPi-nin sınaq versiyası artıq CI-də istifadə edilmişdir.

Necə işləyir?

Modul Nginx-də yüklənir ngx_http_js_module, rəsmi docker təsvirinə daxil edilmişdir. Direktivdən istifadə edərək skriptimizi idxal edirik js_importNginx konfiqurasiyasına. Funksiya çağırışı direktiv tərəfindən həyata keçirilir js_content. Direktiv dəyişənləri təyin etmək üçün istifadə olunur js_set, arqument kimi yalnız skriptdə təsvir olunan funksiyanı qəbul edir. Lakin biz Nginx istifadə edərək yalnız NJS-də alt sorğuları yerinə yetirə bilərik, sizin üçün heç bir XMLHttpRequest yox. Bunun üçün müvafiq yer Nginx konfiqurasiyasına əlavə edilməlidir. Və skriptdə bu yerə bir subreket (alt sorğu) təsvir edilməlidir. Nginx konfiqurasiyasından funksiyaya daxil olmaq üçün funksiya adı skriptin özündə ixrac edilməlidir. 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}

Brauzerdə tələb olunduqda http://localhost:8080/ daxil oluruq location /hansı direktivdə js_content funksiyanı çağırır request skriptimizdə təsvir edilmişdir script.js. Öz növbəsində, funksiyada request üçün alt sorğu edilir location = /sub-query, arqumentdən əldə edilən metodla (cari GET nümunəsində). (r), bu funksiyanı çağırarkən gizli şəkildə ötürülür. Subsorğu cavabının işlənməsi funksiyada həyata keçiriləcək call_back.

S3 cəhd edilir

Şəxsi S3 yaddaşına sorğu göndərmək üçün bizə lazımdır:

ACCESS_KEY

SECRET_KEY

S3_BUCKET

İstifadə olunan http metodundan, cari tarix/saat, S3_NAME və URI, SECRET_KEY istifadə edərək imzalanan (HMAC_SHA1) müəyyən növ sətir yaradılır. Növbəti sətir, kimi AWS $ACCESS_KEY:$HASH, Authorization başlığında istifadə edilə bilər. Əvvəlki addımda sətir yaratmaq üçün istifadə edilən eyni tarix/saat başlığa əlavə edilməlidir X-amz-date. Kodda bu belə görünür:

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 avtorizasiya nümunəsi, köhnəlmiş statusa köçürüldü)

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}

Haqqında bir az izahat _subrequest_uri: bu, ilkin uri-dən asılı olaraq S3-ə sorğu yaradan dəyişəndir. Əgər "kök"ün məzmununu əldə etmək lazımdırsa, bu halda ayırıcı ilə uri-sorğu təşkil etməlisiniz. delimiter, kataloqlara uyğun olan bütün CommonPrefixes xml elementlərinin siyahısını qaytaracaq (PyPI vəziyyətində, bütün paketlərin siyahısı). Müəyyən bir kataloqda (bütün paket versiyalarının siyahısı) məzmunun siyahısını əldə etmək lazımdırsa, uri sorğusu qovluğun (paketin) adı olan bir prefiks sahəsini ehtiva etməlidir / xətti ilə bitməlidir. Əks halda, məsələn, kataloqun məzmununu tələb edərkən toqquşmalar mümkündür. aiohttp-request və aiohttp-requests qovluqları var və əgər sorğu müəyyən edirsə /?prefix=aiohttp-request, sonra cavabda hər iki kataloqun məzmunu olacaq. Əgər sonunda kəsik varsa, /?prefix=aiohttp-request/, onda yalnız istədiyiniz kataloq qaytarılacaq. Bir fayl tələb etsək, nəticədə əldə edilən uri orijinaldan fərqlənməməlidir.

Yadda saxlayın, Nginx-i yenidən başladın. Brauzerdə Nginx-in ünvanını daxil edirik, sorğunun nəticəsi XML olacaq, məsələn:

Kataloq siyahısı

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

Kataloqlar siyahısından yalnız elementlər lazım olacaq CommonPrefixes.

Brauzerdə bizə lazım olan kataloqu ünvanımıza əlavə etməklə, onun məzmununu da XML şəklində əldə edəcəyik:

Kataloqdakı faylları sadalayın

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

Fayllar siyahısından yalnız elementləri götürün Key.

Məzmun növü başlığını mətn/html ilə əvəz etdikdən sonra yaranan XML-i təhlil etmək və onu HTML kimi qaytarmaq qalır.

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

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

PyPI sınanır

Biz yoxlayırıq ki, heç bir yerdə və açıq-aydın işləyən paketlərdə heç nə pozulmur.

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

Liblərimizlə təkrar edirik.

# Создаем для тестов новое окружение
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-də paket yaratmaq və yükləmək belə görünür:

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

İdentifikasiyası

Gitlab-da xarici xidmətlərin autentifikasiyası/avtorizasiyası üçün JWT-dən istifadə etmək mümkündür. Nginx-də auth_request direktivindən istifadə edərək, identifikasiya məlumatlarını skriptdə funksiya çağırışı olan subsorğuya yönləndirəcəyik. Skriptdə Gitlab url-ə daha bir alt sorğu ediləcək və autentifikasiya məlumatları düzgün göstərilibsə, Gitlab 200 kodunu qaytaracaq və paketin endirilməsinə/endirilməsinə icazə veriləcək. Niyə tək bir alt sorğudan istifadə etmirsiniz və məlumatları dərhal Gitlab-a göndərmirsiniz? Çünki o zaman hər dəfə avtorizasiyada bəzi dəyişikliklər etdikdə Nginx konfiqurasiya faylını redaktə etməli olacaqsınız və bu olduqca yorucu bir işdir. Həmçinin, Kubernetes-də yalnız oxumaq üçün olan kök fayl sistemi siyasəti istifadə olunursa, bu, nginx.conf-u konfiqurasiya vasitəsilə əvəz edərkən daha da mürəkkəblik yaradır. Həcmlərin (pvc) və yalnız oxunan kök fayl sisteminin qoşulmasını qadağan edən siyasətlərdən istifadə edərkən konfiqurasiya xəritəsi vasitəsilə Nginx-i konfiqurasiya etmək tamamilə qeyri-mümkün olur (bu da olur).

NJS aralıqdan istifadə edərək, mühit dəyişənlərindən istifadə edərək nginx konfiqurasiyasında göstərilən parametrləri dəyişdirmək və skriptdə bəzi yoxlamalar etmək imkanı əldə edirik (məsələn, səhv göstərilən 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}

Çox güman ki, sual yaranır: - Niyə hazır modullardan istifadə etməyək? Orada hər şey artıq edilib! Məsələn, var AWS = require('aws-sdk') və S3 autentifikasiyası ilə velosiped yazmayın!

Gəlin mənfi cəhətlərə keçək

Mənim üçün xarici JS modullarını idxal edə bilməmək pis, lakin gözlənilən xüsusiyyətə çevrildi. Yuxarıdakı nümunədə təsvir olunan tələb('kripto'), bu quraşdırılmış modullar və yalnız onlar üçün iş tələb edir. Skriptlərdən kodu təkrar istifadə etməyin heç bir yolu da yoxdur və siz onu kopyalayıb müxtəlif fayllara yapışdırmalısınız. Ümid edirəm ki, nə vaxtsa bu funksionallıq tətbiq olunacaq.

Həmçinin, Nginx-də mövcud layihə üçün sıxılma deaktiv edilməlidir. gzip off;

Çünki NJS-də gzip modulu yoxdur və onu birləşdirmək mümkün deyil, buna görə sıxılmış məlumatlarla işləmək üçün heç bir yol yoxdur. Düzdür, bu, bu iş üçün xüsusilə mənfi deyil. Çox mətn yoxdur və köçürülmüş fayllar artıq sıxılmışdır və əlavə sıxılma onlara çox kömək etməyəcək. Məzmunun bir neçə millisaniyə daha tez qaytarılması ilə məşğul olmaq o qədər də məşğul və ya kritik xidmət deyil.

Skriptin sazlanması uzun müddətdir və yalnız error.log-dakı "çaplar" vasitəsilə mümkündür. Quraşdırılmış giriş səviyyəsi məlumatından, xəbərdarlıq və ya səhvdən asılı olaraq, müvafiq olaraq 3 üsuldan istifadə etmək mümkündür r.log, r.warn, r.error. Mən Chrome (v8) və ya njs konsol alətində bəzi skriptləri sazlamağa çalışıram, lakin orada hər şeyi yoxlamaq mümkün deyil. Kodları sazlayarkən, aka funksional test, tarix bu kimi görünür:

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

və yüzlərlə belə ardıcıllıq ola bilər.

Onlar üçün alt sorğular və dəyişənlərdən istifadə edərək kod yazmaq dolaşıq dolaşıqlığa çevrilir. Bəzən kodunuzdakı hərəkətlərin ardıcıllığını anlamağa çalışaraq müxtəlif IDE pəncərələri ətrafında tələsməyə başlayırsınız. Bu çətin deyil, lakin bəzən çox bezdiricidir.

ES6 üçün tam dəstək yoxdur.

Başqa çatışmazlıqlar ola bilər, amma başqa heç nəyə rast gəlməmişəm. NJS ilə mənfi təcrübəniz varsa, məlumatı paylaşın.

Nəticə

NJS, Nginx-də müxtəlif JavaScript skriptlərini tətbiq etməyə imkan verən yüngül açıq mənbəli tərcüməçidir. Onun inkişafı zamanı performansa böyük diqqət yetirildi. Əlbəttə ki, hələ də çox şey yoxdur, lakin layihə kiçik bir komanda tərəfindən hazırlanır və onlar fəal şəkildə yeni funksiyalar əlavə edir və səhvləri düzəldirlər. Ümid edirəm ki, nə vaxtsa NJS sizə Nginx-in funksionallığını demək olar ki, qeyri-məhdud edəcək xarici modulları birləşdirməyə imkan verəcək. Ancaq NGINX Plus var və çox güman ki, heç bir xüsusiyyət olmayacaq!

Məqalənin tam kodu ilə anbar

AWS Sign v4 dəstəyi ilə njs-pypi

ngx_http_js_module modul direktivlərinin təsviri

Rəsmi NJS Repository и sənədlər

Dmitri Volintsevdən NJS-dən istifadə nümunələri

njs - nginx-də yerli JavaScript skripti / Dmitri Volnievin Saint HighLoad++ 2019-da çıxışı

NJS istehsalda / HighLoad++ 2019-da Vasili Soşnikovun təqdimatı

AWS-də REST sorğularının imzalanması və doğrulanması

Mənbə: www.habr.com