Би зөвшөөрөл болон S3-тай өөрийн PyPI репозитор хийсэн. Nginx дээр

Энэ нийтлэлд би Nginx Inc-ийн бүтээсэн Nginx-д зориулсан JavaScript орчуулагч NJS-тэй ажиллаж байсан туршлагаа хуваалцахыг хүсч, түүний үндсэн чадавхийг бодит жишээн дээр тайлбарлахыг хүсч байна. NJS нь Nginx-ийн үйл ажиллагааг өргөтгөх боломжийг олгодог JavaScript-ийн дэд хэсэг юм. гэсэн асуултад яагаад өөрийнхөө орчуулагч??? Дмитрий Волынцев дэлгэрэнгүй хариулав. Товчхондоо: NJS нь nginx арга бөгөөд JavaScript нь Луагаас ялгаатай нь илүү дэвшилтэт, "уугуул" бөгөөд GC-гүй юм.

Удаан хугацааны өмнө…

Сүүлчийн ажил дээрээ би docker-compose, dind болон kaniko rails руу шилжсэн олон өнгийн CI/CD дамжуулах хоолой бүхий gitlab-ийг өвлөн авсан. Өмнө нь CI-д ашиглагдаж байсан зургуудыг анхны хэлбэрт нь шилжүүлсэн. Манай gitlab IP өөрчлөгдөж, CI хулуу болж хувирах хүртэл тэд зөв ажиллаж байсан. Асуудал нь CI-д оролцсон докерийн зургуудын нэг нь git-тэй байсан бөгөөд Python модулиудыг ssh-ээр дамжуулан татдаг байв. ssh-ийн хувьд танд хувийн түлхүүр хэрэгтэй бөгөөд ... энэ нь мэдэгдэж буй_хостуудын хамт зурган дээр байсан. Бодит IP болон мэдэгдэж буй_hosts-д заасан хооронд таарахгүйн улмаас аливаа CI-д түлхүүр баталгаажуулалтын алдаа гарсан. Одоо байгаа Dockfiles-аас шинэ зургийг хурдан угсарч, сонголтыг нэмсэн StrictHostKeyChecking no. Гэвч муу амт хэвээр үлдсэн бөгөөд либерүүдийг хувийн PyPI репозитор руу шилжүүлэх хүсэл байсан. Хувийн PyPI-д шилжсэний дараа нэмэлт урамшуулал бол илүү энгийн дамжуулах хоолой, шаардлага.txt-ийн ердийн тайлбар байв.

Сонголт хийгдлээ, ноёд оо!

Бид бүх зүйлийг үүлэн болон Кубернетес дээр ажиллуулдаг бөгөөд эцэст нь бид гадаад хадгалалт бүхий харьяалалгүй контейнер байсан жижиг үйлчилгээг авахыг хүссэн. За тэгээд бид S3 ашигладаг болохоор эн тэргүүнд тавигдсан. Боломжтой бол gitlab дээр баталгаажуулалттай (шаардлагатай бол та өөрөө нэмж болно).

Шуурхай хайлт хийснээр хэд хэдэн үр дүн гарсан: s3pypi, pypicloud болон манжингийн html файлуудыг "гараар" үүсгэх сонголт. Сүүлийн сонголт нь өөрөө алга болсон.

s3pypi: Энэ бол S3 хостинг ашиглахад зориулагдсан cli юм. Бид файлуудыг байршуулж, html үүсгэж, ижил хувин руу байршуулдаг. Гэрийн хэрэглээнд тохиромжтой.

pypicloud: Энэ нь сонирхолтой төсөл юм шиг санагдаж байсан ч баримт бичгийг уншсаны дараа би урам хугарсан. Хэдийгээр сайн бичиг баримттай, таны хэрэгцээнд нийцүүлэн өргөжүүлэх боломжтой байсан ч бодит байдал дээр энэ нь шаардлагагүй бөгөөд тохируулахад хэцүү болсон. Тухайн үеийн тооцоогоор өөрийн даалгаварт тохируулан кодыг засч залруулахад 3-5 хоног шаардагдана. Үйлчилгээнд мөн мэдээллийн сан хэрэгтэй. Өөр зүйл олдохгүй бол бид үүнийг орхисон.

Илүү нарийвчилсан хайлтаар Nginx ngx_aws_auth модулийг оллоо. Түүний туршилтын үр дүн нь S3 хувингийн агуулгыг харуулсан хөтөч дээр XML-г харуулсан. Хамгийн сүүлд эрэн сурвалжлах ажиллагаа жилийн өмнө болсон. Хадгалах газар хаягдсан харагдсан.

Эх сурвалж руу нь очиж уншсанаар PEP-503 XML-г шууд HTML рүү хөрвүүлж, pip-д өгөх боломжтой гэдгийг би ойлгосон. Nginx болон S3-ийн талаар бага зэрэг хайсны дараа би Nginx-д зориулсан JS дээр бичигдсэн S3 дээрх баталгаажуулалтын жишээг олж харлаа. Ингээд л би NJS-тэй танилцсан.

Энэ жишээг үндэслэн нэг цагийн дараа би хөтөч дээрээ ngx_aws_auth модулийг ашиглахтай ижил XML-г харсан боловч бүх зүйл JS дээр бичигдсэн байсан.

Надад nginx шийдэл үнэхээр таалагдсан. Нэгдүгээрт, сайн баримт бичиг, олон жишээ, хоёрдугаарт, бид Nginx-ийн файлуудтай ажиллахад зориулсан бүх давуу талыг олж авдаг (хайрцагнаас гарсан), гуравдугаарт, Nginx-ийн тохиргоог хэрхэн бичихийг мэддэг хэн бүхэн юу болохыг олж мэдэх боломжтой болно. Минимализм нь Python эсвэл Go (хэрэв эхнээс нь бичсэн бол) -тай харьцуулахад миний хувьд нэмэлт зүйл юм.

TL;DR 2 хоногийн дараа PyPi-ийн туршилтын хувилбарыг CI-д аль хэдийн ашигласан.

энэ нь хэрхэн ажилладаг вэ?

Модуль нь Nginx-д ачаалагдсан ngx_http_js_module, албан ёсны докерын зурагт орсон. Бид удирдамжийг ашиглан скриптээ импортолдог js_importNginx тохиргоонд. Уг функцийг удирдамжаар дууддаг js_content. Заавар нь хувьсагчдыг тохируулахад хэрэглэгддэг js_set, энэ нь зөвхөн скриптэд тодорхойлсон функцийг аргумент болгон авдаг. Гэхдээ бид зөвхөн Nginx ашиглан NJS дээр дэд асуулга ажиллуулж чадна, ямар ч XMLHttpRequest биш. Үүнийг хийхийн тулд 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="/mn/${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 тохиргооны файлыг засварлах шаардлагатай болдог бөгөөд энэ нь нэлээд уйтгартай ажил юм. Түүнчлэн, хэрэв Кубернетес зөвхөн уншигдах үндсэн файлын системийн бодлогыг ашигладаг бол nginx.conf-г тохиргооны зургаар солих үед энэ нь илүү төвөгтэй байдлыг нэмэгдүүлдэг. Эзлэхүүн (pvc) болон зөвхөн уншигдах үндсэн файлын системийн холболтыг хориглох бодлогыг нэгэн зэрэг ашиглахын зэрэгцээ Nginx-ийг configmap-ээр тохируулах нь туйлын боломжгүй болно (энэ нь бас тохиолддог).

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 модулиудыг импортлох боломжгүй болсон нь тааламжгүй, гэхдээ хүлээгдэж буй шинж чанар болсон. Дээрх жишээнд тайлбарласан нь require('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