Мен PyPI репозиторийін авторизациямен және S3 арқылы жасадым. Nginx-те

Бұл мақалада мен Nginx Inc әзірлеген Nginx үшін JavaScript аудармашысы NJS-пен тәжірибеммен бөліскім келеді, оның негізгі мүмкіндіктерін нақты мысал арқылы сипаттаймын. NJS — Nginx функционалдығын кеңейтуге мүмкіндік беретін JavaScript ішкі жиыны. Сұраққа неге сіздің аудармашыңыз??? Дмитрий Волынцев егжей-тегжейлі жауап берді. Қысқаша айтқанда: NJS – nginx жолы, ал JavaScript – Луаға қарағанда прогрессивті, «туған» және GC-сіз.

Ұзақ уақыт бұрын…

Соңғы жұмысымда маған docker-compose, dind және басқа да ләззаттары бар түрлі-түсті CI/CD құбырлары бар gitlab мұраға қалды, олар канико рельстеріне ауыстырылды. Бұрын CI-де қолданылған кескіндер бастапқы түрінде жылжытылды. Олар біздің gitlab IP өзгерген және CI асқабаққа айналған күнге дейін дұрыс жұмыс істеді. Мәселе мынада, CI-ге қатысқан докер кескіндерінің бірінде Python модульдерін ssh арқылы шығаратын git болды. Ssh үшін сізге жеке кілт қажет және... ол белгілі_хосттармен бірге суретте болды. Және кез келген CI нақты IP мен белгілі_хосттарда көрсетілген арасындағы сәйкессіздікке байланысты кілтті тексеру қателігімен сәтсіз аяқталды. Жаңа кескін бұрыннан бар Dockfiles файлдарынан тез жиналды және опция қосылды StrictHostKeyChecking no. Бірақ жағымсыз дәм сақталды және libs жеке PyPI репозиторийіне көшіру ниеті болды. Жеке PyPI-ге ауысқаннан кейін қосымша бонус қарапайым құбыр желісі және талаптар.txt файлының қалыпты сипаттамасы болды.

Таңдау жасалды, мырзалар!

Біз барлығын бұлттарда және Кубернеттерде іске қосамыз және соңында сыртқы жады бар азаматтығы жоқ контейнер болатын шағын қызметті алғымыз келді. Біз S3 пайдаланатындықтан, оған басымдық берілді. Мүмкіндігінше, gitlab-те аутентификациямен (қажет болса, оны өзіңіз қосуға болады).

Жылдам іздеу бірнеше нәтиже берді: s3pypi, pypicloud және репа үшін html файлдарын «қолмен» жасау опциясы. Соңғы опция өздігінен жоғалып кетті.

s3pypi: Бұл S3 хостингін пайдалануға арналған cli. Біз файлдарды жүктейміз, html жасаймыз және оны бір шелекке жүктейміз. Үйде қолдануға жарамды.

pypicloud: Бұл қызықты жоба сияқты көрінді, бірақ құжаттаманы оқығаннан кейін көңілім қалды. Жақсы құжаттамаға және қажеттіліктеріңізге сәйкес кеңейту мүмкіндігіне қарамастан, іс жүзінде ол артық және конфигурациялау қиын болып шықты. Тапсырмаларыңызға сәйкес кодты түзету, сол кездегі бағалаулар бойынша, 3-5 күнді алады. Сондай-ақ қызметке мәліметтер базасы қажет. Басқа ештеңе таппаған жағдайда оны қалдырдық.

Неғұрлым тереңірек іздеу Nginx, ngx_aws_auth модулін берді. Оның тестілеуінің нәтижесі браузерде S3 шелегінің мазмұнын көрсететін XML көрсетілді. Іздеу кезіндегі соңғы әрекет бір жыл бұрын болған. Репозиторий қараусыз қалды.

Дереккөзге барып, оқу арқылы ПЭП-503 Мен XML-ді HTML-ге жылдам түрлендіруге және пипке беруге болатынын түсіндім. Nginx және S3 туралы көбірек іздегеннен кейін мен Nginx үшін JS тілінде жазылған S3-де аутентификация үлгісін таптым. Мен NJS компаниясымен осылай таныстым.

Осы мысалды негізге ала отырып, бір сағаттан кейін мен браузерімде ngx_aws_auth модулін пайдаланған кездегідей XML-ді көрдім, бірақ бәрі JS-де жазылған.

Маған nginx шешімі қатты ұнады. Біріншіден, жақсы құжаттама және көптеген мысалдар, екіншіден, біз файлдармен жұмыс істеу үшін Nginx-тің барлық артықшылықтарын аламыз (қораптан тыс), үшіншіден, Nginx үшін конфигурацияларды қалай жазу керектігін білетін кез келген адам не екенін анықтай алады. Nexus туралы айтпағанда, Python немесе Go (егер нөлден бастап жазылса) салыстырғанда минимализм мен үшін плюс.

TL;DR 2 күннен кейін PyPi сынақ нұсқасы CI жүйесінде бұрыннан қолданылған.

Бұл қалай жұмыс істейді?

Модуль Nginx-ке жүктеледі ngx_http_js_module, ресми докер кескініне енгізілген. Директиваны пайдаланып сценарийімізді импорттаймыз js_importNginx конфигурациясына. Функция директива арқылы шақырылады js_content. Директива айнымалы мәндерді орнату үшін қолданылады js_set, ол аргумент ретінде сценарийде сипатталған функцияны ғана қабылдайды. Бірақ біз NJS ішіндегі ішкі сұрауларды кез келген XMLHttpRequest емес, тек Nginx арқылы орындай аламыз. Ол үшін сәйкес орынды Nginx конфигурациясына қосу керек. Және сценарий осы орынға қосымша сұрауды сипаттауы керек. Nginx конфигурациясынан функцияға қол жеткізу үшін функция атауы сценарийдің өзінде экспортталуы керек 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}

Браузерде сұралған кезде http://localhost:8080/ кіреміз location /онда директивада js_content функцияны шақырады request біздің сценарийде сипатталған script.js. Өз кезегінде, функцияда request үшін ішкі сұрау жасалады location = /sub-query, аргументтен алынған әдіспен (ағымдағы GET мысалында). (r), бұл функция шақырылғанда жасырын түрде жіберіледі. Қосымша сұраудың жауабы функцияда өңделеді call_back.

S3 әрекеті

Жеке S3 жадына сұрау салу үшін бізге қажет:

ACCESS_KEY

SECRET_KEY

S3_BUCKET

Пайдаланылған http әдісінен, ағымдағы күн/уақыт, S3_NAME және URI, SECRET_KEY арқылы қол қойылған (HMAC_SHA1) жолдың белгілі бір түрі жасалады. Келесі сияқты сызық AWS $ACCESS_KEY:$HASH, авторизация тақырыбында пайдалануға болады. Алдыңғы қадамда жолды жасау үшін пайдаланылған күн/уақыт тақырыпқа қосылуы керек X-amz-date. Кодта ол келесідей көрінеді:

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 авторизациясының мысалы, ескірген күйге өзгертілді)

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}

туралы шағын түсініктеме _subrequest_uri: бұл бастапқы uri-ге байланысты S3 сұранысын құрайтын айнымалы. Егер сізге «түбірдің» мазмұнын алу қажет болса, онда бөлгішті көрсететін uri сұрауын жасау керек. delimiter, ол каталогтарға сәйкес барлық CommonPrefixes xml элементтерінің тізімін қайтарады (PyPI жағдайында, барлық бумалардың тізімі). Егер сізге белгілі бір каталогтағы мазмұндар тізімін (барлық бума нұсқаларының тізімі) алу қажет болса, uri сұрауында міндетті түрде қиғаш сызықпен аяқталатын каталог (бума) аты бар префикс өрісі болуы керек. Әйтпесе, мысалы, каталогтың мазмұнын сұрау кезінде соқтығысуы мүмкін. aiohttp-request және aiohttp-requests каталогтары бар және сұрауда көрсетілген болса /?prefix=aiohttp-request, содан кейін жауап екі каталогтың мазмұнын қамтиды. Соңында қиғаш сызық болса, /?prefix=aiohttp-request/, содан кейін жауап тек қажетті каталогты қамтиды. Ал егер файлды сұрайтын болсақ, онда алынған uri бастапқыдан ерекшеленбеуі керек.

Nginx файлын сақтаңыз және қайта іске қосыңыз. Браузерде біз Nginx мекенжайын енгіземіз, сұраудың нәтижесі XML болады, мысалы:

Анықтамалар тізімі

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

Каталогтар тізімінен сізге тек элементтер қажет болады CommonPrefixes.

Браузердегі мекенжайымызға қажет каталогты қосу арқылы біз оның мазмұнын XML форматында аламыз:

Каталогтағы файлдар тізімі

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

Файлдар тізімінен біз тек элементтерді аламыз Key.

Алынған XML файлын талдау және алдымен Content-Type тақырыбын мәтін/html-ге ауыстырып, 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="/kk/${reValue.groups.v}">${a_text}</a>`);
    }
  }

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

PyPI қолданылуда

Біз жұмыс істейтін пакеттерде ештеңе бұзылмайтынын тексереміз.

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

Біз өз қолымызбен қайталаймыз.

# Создаем для тестов новое окружение
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 жүйесінде буманы жасау және жүктеу келесідей болады:

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

Түпнұсқалық растама

Gitlab жүйесінде сыртқы қызметтерді аутентификация/авторизациялау үшін JWT пайдалануға болады. Nginx жүйесіндегі auth_request директивасын пайдаланып, түпнұсқалық растама деректерін сценарийде функция шақыруын қамтитын қосалқы сұрауға қайта бағыттаймыз. Сценарий Gitlab url мекенжайына тағы бір қосалқы сұрау жасайды және аутентификация деректері дұрыс көрсетілсе, Gitlab 200 кодын қайтарады және пакетті жүктеп салуға/жүктеп алуға рұқсат етіледі. Неліктен бір ішкі сұрауды пайдаланбасқа және деректерді дереу Gitlab-ке жібермеске? Өйткені біз авторизацияға кез келген өзгерістер енгізген сайын Nginx конфигурация файлын өңдеуіміз керек және бұл өте жалықтыратын тапсырма. Сондай-ақ, егер Kubernetes тек оқуға арналған түбірлік файлдық жүйе саясатын пайдаланса, nginx.conf файлын конфигурация картасы арқылы ауыстырған кезде бұл одан да күрделірек болады. Бір уақытта томдарды (pvc) және тек оқуға арналған түбірлік файлдық жүйені қосуға тыйым салатын саясаттарды пайдалану кезінде Nginx конфигурациясы арқылы конфигурациялау мүлдем мүмкін емес (бұл да болады).

NJS аралық нұсқасын пайдалану арқылы біз қоршаған ортаның айнымалы мәндерін пайдаланып nginx конфигурациясында көрсетілген параметрлерді өзгерту және сценарийде кейбір тексерулерді орындау мүмкіндігін аламыз (мысалы, қате көрсетілген 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}

Сірә, сұрақ туындауы мүмкін: -Неге дайын модульдерді қолданбасқа? Онда бәрі қазірдің өзінде жасалды! Мысалы, var AWS = require('aws-sdk') және S3 аутентификациясы бар "велосипед" жазудың қажеті жоқ!

Кемшіліктерге көшейік

Мен үшін сыртқы JS модульдерін импорттау мүмкін еместігі жағымсыз, бірақ күтілетін мүмкіндік болды. Жоғарыдағы мысалда сипатталған талап('crypto') болып табылады кірістірілген модульдер және олар үшін тек жұмыстарды талап етеді. Сондай-ақ сценарийлердегі кодты қайта пайдалану мүмкіндігі жоқ және оны әртүрлі файлдарға көшіріп, қою керек. Бір күні бұл функция жүзеге асады деп үміттенемін.

Nginx-тегі ағымдағы жоба үшін қысуды да өшіру керек gzip off;

NJS жүйесінде gzip модулі жоқ және оны қосу мүмкін емес, сондықтан қысылған деректермен жұмыс істеу мүмкін емес. Рас, бұл іс жүзінде минус емес. Мәтін көп емес және тасымалданған файлдар қазірдің өзінде қысылған және қосымша қысу оларға көп көмектеспейді. Сондай-ақ, бұл мазмұнды бірнеше миллисекундқа жылдам жеткізуге алаңдайтындай жүктелетін немесе маңызды қызмет емес.

Скриптті жөндеу ұзақ уақыт алады және error.log ішіндегі «басып шығару» арқылы ғана мүмкін болады. Орнатылған тіркеу деңгейі ақпаратына, ескерту немесе қатеге байланысты сәйкесінше r.log, r.warn, r.error 3 әдісін қолдануға болады. Мен Chrome (v8) немесе njs консоль құралында кейбір сценарийлерді жөндеуге тырысамын, бірақ ол жерде барлығын тексеру мүмкін емес. Кодты жөндеу кезінде, яғни функционалды тестілеу, тарих келесідей көрінеді:

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

және мұндай тізбектер жүздеген болуы мүмкін.

Ішкі сұраулар мен оларға арналған айнымалы мәндерді пайдаланып код жазу шиеленіскен шиеленіске айналады. Кейде сіз кодыңыздың әрекеттер тізбегін анықтауға тырысып, әртүрлі IDE терезелерінде жүгіре бастайсыз. Бұл қиын емес, бірақ кейде бұл өте тітіркендіргіш.

ES6 үшін толық қолдау жоқ.

Басқа да кемшіліктер болуы мүмкін, бірақ мен басқа ештеңеге тап болған жоқпын. NJS пайдалану тәжірибесіңіз болса, ақпаратты бөлісіңіз.

қорытынды

NJS - Nginx жүйесінде әртүрлі JavaScript сценарийлерін енгізуге мүмкіндік беретін жеңіл ашық бастапқы аудармашы. Оны әзірлеу барысында өнімділікке үлкен көңіл бөлінді. Әрине, әлі де көп нәрсе жетіспейді, бірақ жобаны шағын топ әзірлеуде және олар белсенді түрде жаңа мүмкіндіктерді қосып, қателерді түзетеді. Бір күні NJS сізге Nginx функционалдығын шексіз дерлік ететін сыртқы модульдерді қосуға мүмкіндік береді деп үміттенемін. Бірақ NGINX Plus бар және мүмкіндіктері болмайды!

Мақаланың толық коды бар репозиторий

AWS Sign v4 қолдауымен njs-pypi

ngx_http_js_module модулінің директиваларының сипаттамасы

Ресми NJS репозиторийі и құжаттама

Дмитрий Волинцевтің NJS пайдалану мысалдары

njs - nginx жүйесіндегі жергілікті JavaScript сценарийі / Дмитрий Вольныевтің Saint HighLoad++ 2019 бағдарламасындағы сөзі

NJS өндірісте / Василий Сошниковтың HighLoad++ 2019 бағдарламасындағы сөзі

AWS жүйесінде REST сұрауларына қол қою және аутентификация

Ақпарат көзі: www.habr.com