Ръководство за CI/CD в GitLab за (почти) абсолютни начинаещи

Или как да се снабдите с красиви значки за проекта си за един вечер лесно кодиране

Сигурно всеки разработчик, който има поне един проект на хартия, по определен момент усеща нуждата от красиви значки със статуси, покритие на кода, версии на пакети в nuget… И мен този импулс ме доведе до написването на тази статия. В процеса на подготовка за нея създадох такава красота в един от моите проекти:

Ръководство за CI/CD в GitLab за (почти) абсолютни начинаещи

В статията ще се разгледа основната настройка на непрекъсната интеграция и доставка за проект на библиотека от класове на .Net Core в GitLab, с публикуване на документацията в GitLab Pages и изпращане на събраните пакети в личен фид в Azure DevOps.

Като среда за разработка беше използван VS Code с разширение GitLab Workflow (за валидиране на файла с настройки директно от средата за разработка).

Кратко въведение

CD — това ли е, когато просто пуснеш, а на клиента вече всичко е паднало?

Какво е CI/CD и защо е нужно — лесно може да се намери в Google. Пълната документация за настройка на пайплайни в GitLab също не е трудна за намиране. Ето тук аз накратко и по възможност без грешки ще опиша процеса на работа на системата от височина на птичи полет:разработчикът изпраща комит в репозитория, създава merge request през сайта,

  • или по какъвто и да е друг начин явно или неявно стартира пайплайна от конфигурацията се избират всички задачи, чиито условия позволяват да бъдат стартирани в този контекст,,
  • задачите се организират в съответствие с етапите си,
  • етапите се изпълняват последователно — т.е.
  • паралелно се изпълняват всички задачи на този етап, ако етапът завърши с неуспех (т.е. ако поне една от задачите на етапа завърши с неуспех) — пайплайнът спира (
  • почти винагиако всички етапи са завършени успешно, пайплайнът се счита за успешно преминал.),
  • По този начин имаме:

пайплайн — набор от задачи, организирани в етапи, в който може да се събира, тества, опакова код, да се разгръща готовото изпълнение в облачен сервис и т.н.,

  • етап (
  • ) — единица организация на пайплайна, съдържаща 1+ задача,и да разпространявате обновления в него преди разпространението в каналитезадача (
  • job) — единица работа в пайплайна. Състои се от скрипт (задължително), условия за стартиране, настройки за публикуване/кеширане на артефакти и много други.) — единица работа в пайплайна. Състои се от скрипт (задължителен), условия за стартиране, настройки за публикуване/кеширане на артефакти и много други.

Съответно, задачата при настройка на CI/CD е да създадем набор от задачи, които реализират всичките необходими действия за изграждане, тестване и публикуване на код и артефакти.

Преди да започнем: защо?

  • Защо GitLab?

Защото, когато се появи необходимостта да създам частни репозитории за пет-проекти, в GitHub те бяха платени, а аз — алчен. Репозиториите станаха безплатни, но все още не е достатъчна причина за мен да премина на GitHub.

  • Защо не Azure DevOps Pipelines?

Защото настройването там е елементарно — дори не са необходими познания по командния ред. Интеграцията с външни доставчици на git става с няколко кликвания, импортът на SSH ключове за изпращане на комити в репозитория — също, пайплайнът лесно се настройва дори без шаблон.

Изходна позиция: какво имаме и какво искаме

Имаме:

  • репозитория в GitLab.

Искаме:

  • автоматично изграждане и тестване за всяко merge request,
  • изграждане на пакети за всяко merge request и пуш в master при условие, че съобщението за комита съдържа определена стринга,
  • изпращане на събраните пакети в частен фид в Azure DevOps,
  • изграждане на документация и публикуване в GitLab Pages,
  • бейджи!11

Описаните изисквания органично се вписват в следния модел на пайплайн:

  • Етап 1 — изграждане
    • Събираме кода, изходните файлове публикуваме като артефакти
  • Етап 2 — тестване
    • Получаваме артефактите от етапа на изграждане, провеждаме тестове, събираме данни за покритие на кода
  • Етап 3 — изпращане
    • Задача 1 — събираме nuget пакет и го изпращаме в Azure DevOps
    • Задача 2 — събираме сайт от xmldoc в изходния код и публикуваме в GitLab Pages

Да започнем!

Събираме конфигурацията

Подготвяме акаунтите

  1. Създаваме акаунт в Microsoft Azure

  2. Преминаваме към Azure DevOps

  3. Създаваме нов проект

    1. Име — произволно
    2. Видимост — произволна
      Ръководство за CI/CD в GitLab за (почти) абсолютни начинаещи

  4. Когато натиснете бутона Create, проектът ще бъде създаден и ще последва пренос на страницата му. На тази страница можете да изключите ненужните функции, като отидете в настройките на проекта (долната връзка в списъка отляво -> Overview -> блок Azure DevOps Services)
    Ръководство за CI/CD в GitLab за (почти) абсолютни начинаещи

  5. Преминаваме в Atrifacts, натискаме Create feed

    1. Въвеждаме името на източника
    2. Избираме видимост
    3. Снемаме галочката Include packages from common public sources, за да не се превърне източникът в сметище на клони от nuget
      Ръководство за CI/CD в GitLab за (почти) абсолютни начинаещи

  6. Натискаме Connect to feed, избираме Visual Studio, от блока Machine Setup копираме Source
    Ръководство за CI/CD в GitLab за (почти) абсолютни начинаещи

  7. Отиваме в настройките на акаунта, избираме Personal Access Token
    Ръководство за CI/CD в GitLab за (почти) абсолютни начинаещи

  8. Създаваме нов токен за достъп

    1. Име — произволно
    2. Организация — текущата
    3. Срокът на действие – максимум 1 година
    4. Обхват (scope) – Packaging/Read & Write
      Ръководство за CI/CD в GitLab за (почти) абсолютни начинаещи

  9. Копираме създадения токен – след затварянето на модалния прозорец стойността ще бъде недостъпна

  10. Влизаме в настройките на репозитория в GitLab, избираме настройките на CI/CD
    Ръководство за CI/CD в GitLab за (почти) абсолютни начинаещи

  11. Разширяваме блока Variables, добавяме нова

    1. Име – произволно без интервали (ще бъде достъпно в командната обвивка)
    2. Стойност – токен за достъп от п. 9
    3. Избираме Mask variable
      Ръководство за CI/CD в GitLab за (почти) абсолютни начинаещи

На този етап предварителната настройка е завършена.

Подготвяме основата на конфигурацията

По подразбиране, за настройка на CI/CD в GitLab се използва файл .gitlab-ci.yml от корена на репозитория. Може да се настрои произволен път до този файл в настройките на репозитория, но в този случай това не е необходимо.

Както се вижда от разширението, файлът съдържа конфигурация във формат YAML. В документацията подробно е описано какви ключове могат да бъдат на върховото ниво на конфигурацията и във всеки от вложените нива.

Първо добавяме в конфигурационния файл връзка към docker-образа, в който ще се изпълняват задачите. За това намираме страницата на образите .Net Core в Docker Hub. В GitHub има подробно ръководство за това как да изберем образ за различни задачи. За изграждане ни подхожда образ с .Net Core 3.1, затова смело добавяме първия ред в конфигурацията

image: mcr.microsoft.com/dotnet/core/sdk:3.1

Сега при стартиране на пайплайна от хранилището на образи Microsoft ще бъде изтеглен посоченият образ, в който ще се изпълняват всички задачи от конфигурацията.

Следващата стъпка – добавяне на и да разпространявате обновления в него преди разпространението в каналитеетапи. По подразбиране GitLab определя 5 етапа:

  • .pre – изпълнява се преди всички етапи,
  • .post – изпълнява се след всички етапи,
  • build – първият след .pre етап,
  • test – втори етап,
  • deploy – трети етап.

Ничего не пречи да ги обявим явно. Поредността на етапите влияе на реда, в който те се изпълняват. За пълнота на изложението, добавяме в конфигурацията:

stages:
  - build
  - test
  - deploy

За отстраняване на проблеми е разумно да се получи информация за средата, в която се изпълняват задачите. Добавяме глобален набор от команди, които ще се изпълняват преди всяка задача, с помощта на before_script:

before_script:
  - $PSVersionTable.PSVersion
  - dotnet --version
  - nuget help | select-string Version

Остава да добавим поне една задача, за да може пайплайнът да се стартира при изпращане на комити. Засега ще добавим празна задача за демонстрация:

dummy job:
  script:
    - echo ok

Стартираме валидирането, получаваме съобщение, че всичко е наред, комитваме, пушваме, гледаме на сайта резултатите… И получаваме грешка в скрипта — bash: .PSVersion: команда не е намерена. Какво?

Всичко е логично — по подразбиране runner-ите (отговарящи за изпълнението на задачи и предоставяни от GitLab) използват bash за изпълнение на команди. Можем да поправим това, като явно посочим в описанието на задачата, какви тагове трябва да има изпълняващият пайплайн рънър:

dummy job on windows:
  script:
    - echo ok
  tags:
    - windows

Отлично! Сега пайплайнът се изпълнява.

Внимателният читател, повтаряйки посочените стъпки, ще забележи, че задачата е изпълнена в етапа test, въпреки че не сме посочили етап. Как може да се предположи, test являва се етап по подразбиране.

Да продължим с изграждането на скелета на конфигурацията, добавяйки всички задачи, описани по-горе:

build job:
  script:
    - echo "building..."
  tags:
    - windows
  stage: build

test and cover job:
  script:
    - echo "running tests and coverage analysis..."
  tags:
    - windows
  stage: test

pack and deploy job:
  script:
    - echo "packing and pushing to nuget..."
  tags:
    - windows
  stage: deploy

pages:
  script:
    - echo "creating docs..."
  tags:
    - windows
  stage: deploy

Получихме не особено функционален, но все пак коректен пайплайн.

Настройка на тригери

Поради факта, че за нито една от задачите не са посочени филтри за задействане, пайплайнът ще напълно изпълнява при всяко изпращане на комити в хранилището. Тъй като това не е желаното поведение в общия случай, ще настроим филтри за задействане на задачите.

Филтрите могат да се настройват в два формата: only/except и rules. В кратце, only/except позволява настройка на филтри по тригери (merge_request, например — настройва задачата да се изпълнява при всяко създаване на искане за сливане и при всяко изпращане на комити в клон, който е източник в искането за сливане) и имената на клонове (в т.ч. с използване на регулярни изрази); rules позволява настройка на набор от условия и, по избор, промяна на условията за изпълнение на задачата в зависимост от успеха на предишни задачи (when в GitLab CI/CD).

Нека си припомним набора от изисквания — изграждане и тестване само за merge request, опаковане и изпращане в Azure DevOps — за merge request и пушове в master, генериране на документация — за пушове в master.

За начало, нека настроим задачата за изграждане на код, добавяйки правило за задействане само при merge request:

build job:
  # snip
  only:
    - merge_request

Сега нека настроим задачата за опаковане на задействания за merge request и добавяне на комити в master:

опакуйте и разверните задачу:
  # пропущено
  только:
    - merge_request
    - master

Както се вижда, всичко е просто и директно.

Също така, можете да настроите задачата да се задейства само ако е създаден merge request с определена целева или изходна клонка:

  правила:
    - ако: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"

В условията могат да се използват переменные, изброени тук; правилата rules не са съвместими с правилата only/except.

Настройване на запазването на артефакти

По време на изпълнението на задачата сборна задача ще бъдат създадени артефакти от сборката, които могат да бъдат повторно използвани в следващите задачи. За целта трябва да добавите пътища в конфигурацията на задачата, файлове, които трябва да бъдат запазени и повторно използвани в следващите задачи, в ключа артефакти:

сборна задача:
  # пропуснато
  артефакти:
    пътища:
      - path/to/build/artifacts
      - another/path
      - MyCoolLib.*/bin/Release/*

Пътищата поддържат wildcards, което определено улеснява задаването им.

Ако задачата създава артефакти, всяка следваща задача ще може да получи достъп до тях — те ще се намират на същите пътища спрямо корена на хранилището, по които са били събрани от изходната задача. Артефактите също така са достъпни за изтегляне на сайта.

Сега, когато имаме готов (и проверен) скелет на конфигурацията, можем да преминем към самото писане на сценарии за задачите.

Пишем сценарии

Възможно е, преди много време, в далечна галактика, сборът на проекти (включително и на .net) от командния ред да е бил болка. Сега обаче, сборът, тестването и публикуването на проект може да стане с 3 команди:

dotnet build
dotnet test
dotnet pack

Естествено, има някои нюанси, поради които малко ще сложим командите.

  1. Искаме release версия, а не debug, така че добавяме -c Release
  2. При тестването искаме да съберем данни за покритие на кода, затова е необходимо да добавим анализатор за покритие в тестовите библиотеки:
    1. Във всички тестови библиотеки трябва да добавите пакета coverlet.msbuild: dotnet add package coverlet.msbuild от папката на проекта
    2. В командата за стартиране на тестовете ще добавим /p:CollectCoverage=true
    3. В конфигурацията на задачата за тестване добавяме ключ за получаване на резултатите от покритие (вж. по-долу)
  3. При опаковането на кода в nuget пакети задаваме изходната директория за пакетите: -o .

Събираме данни за покритие на кода

Coverlet след стартиране на тестовете извежда в конзолата статистиката относно изпълнението:

Изчисляване на резултата от покритие...
  Генериране на доклад 'C:Usersxxxsourcereposmy-projectmyProject.testscoverage.json'

+-------------+--------+--------+--------+
| Модул      | Линия | Клон   | Метод  |
+-------------+--------+--------+--------+
| проект 1   | 83,24% | 66,66% | 92,1%  |
+-------------+--------+--------+--------+
| проект 2   | 87,5%  | 50%    | 100%   |
+-------------+--------+--------+--------+
| проект 3   | 100%   | 83,33% | 100%   |
+-------------+--------+--------+--------+

+---------+--------+--------+--------+
|         | Линия | Клон   | Метод  |
+---------+--------+--------+--------+
| Общо    | 84,27% | 65,76% | 92,94% |
+---------+--------+--------+--------+
| Средно  | 90,24% | 66,66% | 97,36% |
+---------+--------+--------+--------+

GitLab позволява да зададете регулярно изразяване за получаване на статистика, която по-късно може да бъде получена под формата на значка. Регулярното изразяване се задава в настройките на задачата с ключа покритие; в израза трябва да присъства capture-група, стойността на която ще бъде предадена в значката:

тест и покритие:
  # пропускам
  покритие: \/|s*Общото*|s*(d+[,.]d+%)\/

Тук получаваме статистика от реда с общото покритие по линии.

Публикуваме пакети и документация

И двете действия са зададени на последния етап на пайплайна – след като сборката и тестовете приключиха, е време да споделим с света нашите разработки.

Първо ще разгледаме публикуването на източника на пакети:

  1. Ако в проекта няма файл за конфигурация nuget (nuget.config), ще създадем нов: dotnet new nugetconfig

    Защо: в образа може да е забранен достъпът за запис към глобалните (потребителски и машинни) конфигурации. За да избегнем грешки, просто ще създадем нова локална конфигурация и ще работим с нея.

  2. Добавяме нов източник на пакети в локалната конфигурация: nuget sources add -name <name> -source <url> -username <organization> -password <gitlab variable> -configfile nuget.config -StorePasswordInClearText
    1. name — локалното име на източника, не принципиално
    2. url — URL на източника от етапа "Готвим акаунтите", п. 6
    3. organization — име на организацията в Azure DevOps
    4. gitlab variable — име на променливата с токена за достъп, добавена в GitLab ("Готвим акаунтите", п. 11). Разбира се, в формата $variableName
    5. -StorePasswordInClearText — хак за избягване на грешката за отказ на достъп (не съм първият, който е настъпил тези грабли)
    6. В случай на грешки, може да е полезно да добавите -verbosity detailed
  3. Изпращаме пакета в източника: nuget push -source <name> -skipduplicate -apikey <key> *.nupkg
    1. Изпращаме всички пакети от текущата директория, затова *.nupkg.
    2. name — от горната стъпка.
    3. key — произволен ред. В Azure DevOps в прозореца Connect to feed винаги дават пример за реда az.
    4. -skipduplicate — при опит за изпращане на вече съществуващ пакет без този ключ, източникът ще върне грешка 409 Конфликт; с ключа, изпращането ще бъде пропуснато.

Сега ще настроим създаването на документация:

  1. Първо, в репозитория, в клона master, инициализираме проекта docfx. За целта от корена трябва да изпълним командата docfx init и в интерактивен режим ще зададем основните параметри за генериране на документацията. Подробно описание на минималната настройка на проекта тук..
    1. При настройка е важно да укажем изходната директория ..public — GitLab по подразбиране взема съдържанието на папката public в корена на репозитория като източник за Pages. Тъй като проектът ще бъде разположен в подпапка на репозитория, добавяме в пътя изход на ниво нагоре.
  2. Ще изпратим промените в GitLab.
  3. В конфигурацията на pipeline добавяме задача pages (резервирана дума за задачи за публикуване на сайтове в GitLab Pages):
    1. Скрипт:
      1. nuget install docfx.console -version 2.51.0 — ще инсталира docfx; версията е посочена за гарантиране на коректността на пътищата на инсталиране на пакета.
      2. .docfx.console.2.51.0toolsdocfx.exe .docfx_projectdocfx.json — генерираме документацията
    2. Узел artifacts:

pages:
  # snip
  artifacts:
    paths:
      - public

Личен отстъп за docfx

Преди при настройка на проекта, посочвах източника на кода за документацията като файл на решението. Основен недостатък — документацията се генерира и за тестовите проекти. В случай, че това не е нужно, може да се зададе такава стойност на узла metadata.src:

{
  "metadata": [
    {
      "src": [
        {
          "src": "..\/",
          "files": [
            "**\/*.csproj"
          ],
          "exclude":[
            "*.tests*\/**"
          ]
        }
      ],
      \/\/ --- snip ---
    },
    \/\/ --- snip ---
  ],
  \/\/ --- snip ---
}

  1. metadata.src.src: "..\/" — излизаме на ниво нагоре спрямо разположението docfx.json, тъй като в шаблоните не работи търсене нагоре по дървото на директории.
  2. metadata.src.files: ["**\/*.csproj"] — глобален шаблон, събираме всички C# проекти от всички директории.
  3. metadata.src.exclude: ["*.tests*\/**"] — глобален шаблон, изключваме всичко от папки с .tests в името

Промеждутъчен резултат

Тази проста конфигурация може да бъде съставена буквално за половин час и две чаши кафе, която ще позволи при всяко искане за сливане и изпращане в master да се проверява, че кодът се компилира и тестовете минават, да събира нов пакет, да актуализира документацията и да радва окото с красиви значки в README на проекта.

Финален .gitlab-ci.yml

image: mcr.microsoft.com/dotnet/core/sdk:3.1

before_script:
  - $PSVersionTable.PSVersion
  - dotnet --version
  - nuget help | select-string Version

stages:
  - build
  - test
  - deploy

build job:
  stage: build
  script:
    - dotnet build -c Release
  tags:
    - windows
  only:
    - merge_requests
    - master
  artifacts:
    paths:
      - your/path/to/binaries

test and cover job:
  stage: test
  tags:
    - windows
  script:
    - dotnet test -c Release /p:CollectCoverage=true
  coverage: /|s*Totals*|s*(d+[,.]d+%)
  only:
    - merge_requests
    - master

pack and deploy job:
  stage: deploy
  tags:
    - windows
  script:
    - dotnet pack -c Release -o .
    - dotnet new nugetconfig
    - nuget sources add -name feedName -source https://pkgs.dev.azure.com/your-organization/_packaging/your-feed/nuget/v3/index.json -username your-organization -password $nugetFeedToken -configfile nuget.config -StorePasswordInClearText
    - nuget push -source feedName -skipduplicate -apikey az *.nupkg
  only:
    - master

pages:
  tags:
    - windows
  stage: deploy
  script:
    - nuget install docfx.console -version 2.51.0
    - $env:path = "$env:path;$($(get-location).Path)"
    - .docfx.console.2.51.0toolsdocfx.exe .docfxdocfx.json
  artifacts:
    paths:
      - public
  only:
    - master

Между другому за бейджовете

Всичко беше започнало заради тях!

Бейджове с статуси на пайплайна и покритие на кода са налични в GitLab в настройките CI/CD в блока Gtntral pipelines:

Ръководство за CI/CD в GitLab за (почти) абсолютни начинаещи

Бейджът с линк към документацията го създадох на платформата Shields.io — там всичко е достатъчно директно, може да се създаде собствен бейдж и да се получава чрез запитване.

![Пример с Shields.io](https://img.shields.io/badge/custom-badge-blue)

Ръководство за CI/CD в GitLab за (почти) абсолютни начинаещи

Azure DevOps Artifacts също така позволява създаването на бейджове за пакети с посочена актуална версия. За целта в източника на сайта Azure DevOps трябва да кликнете на Create badge на избрания пакет и да копирате markdown-разметката:

Ръководство за CI/CD в GitLab за (почти) абсолютни начинаещи

Ръководство за CI/CD в GitLab за (почти) абсолютни начинаещи

Добавяме елегантност

Изтъкваме общите фрагменти на конфигурацията

Докато пишех конфигурацията и търсех в документацията, се натъкнах на интересна възможност в YAML — повторно използване на фрагменти.

Както се вижда от настройките на задачите, всички те изискват наличието на таг windows на ранъра и сработват при изпращане в master/създаване на запитване за сливане (с изключение на документацията). Нека добавим това към фрагмента, който ще повторно използваме:

.common_tags: &common_tags
  tags:
    - windows
.common_only: &common_only
  only:
    - merge_requests
    - master

И сега в описанието на задачата можем да вмъкнем обявения по-рано фрагмент:

build job:
  <<: *common_tags
  <<: *common_only

Имената на фрагментите трябва да започват с точка, за да не бъдат интерпретирани като задача.

Версиониране на пакети

При създаване на пакет компилаторът проверява командните редове и при тяхното отсъствие — файловете на проектите; като намери възел Version, взема стойността му като версия на създавания пакет. Така че, за да създадете пакет с нова версия, трябва или да я обновите във файла на проекта, или да я предадете като аргумент на командния ред.

Нека добавим още едно пожелание — да направим младшите две числа в версията година и дата на изграждане на пакета, и да добавим предварителни версии. Разбира се, може да добавяте тези данни в проекта и да ги проверявате преди всяко изпращане — но може да направите това и в пайплайна, събирайки версията на пакета от контекста и предавайки го чрез аргумент на командния ред.

Съгласяваме се, че ако в съобщението за комит има строка от вида release (v. / ver. / version) (rev. / revision )?, то ще вземем версията на пакета от тази строка, ще я допълним с текущата дата и ще я предадем като аргумент на командата dotnet pack. При отсъствие на строката — просто няма да създаваме пакет.

Тази задача решава следният скрипт:

# регулярное выражение для поиска строки с версией
$rx = "releases+(v.?|ver.?|version)s*(?<maj>d+)(?<min>.d+)?(?<rel>.d+)?s*((rev.?|revision)?s+(?<rev>[a-zA-Z0-9-_]+))?"
# ищем строку в сообщении коммита, передаваемом в одной из предопределяемых GitLab'ом переменных
$found = $env:CI_COMMIT_MESSAGE -match $rx
# совпадений нет - выходим
if (!$found) { Write-Output "no release info found, aborting"; exit }
# извлекаем мажорную и минорную версии
$maj = $matches['maj']
$min = $matches['min']
# если строка содержит номер релиза - используем его, иначе - текущий год
if ($matches.ContainsKey('rel')) { $rel = $matches['rel'] } else { $rel = ".$(get-date -format "yyyy")" }
# в качестве номера сборки - текущие месяц и день
$bld = $(get-date -format "MMdd")
# если есть данные по пререлизной версии - включаем их в версию
if ($matches.ContainsKey('rev')) { $rev = "-$($matches['rev'])" } else { $rev = '' }
# собираем единую строку версии
$version = "$maj$min$rel.$bld$rev"
# собираем пакеты
dotnet pack -c Release -o . /p:Version=$version

Добавяме скрипта в задачата pack and deploy job и наблюдаваме изграждането на пакети строго при наличие на зададената строка в съобщението за комит.

Общо

След като отделихме около половин до един час за написване на конфигурацията, отстраняване на грешки в локалния PowerShell и, може би, няколко неуспешни стартирания, получихме проста конфигурация за автоматизация на рутинни задачи.

Разбира се, GitLab CI / CD е много по-обширен и многостранен, отколкото може да изглежда след прочитане на това ръководство — това съвсем не е така. Там дори Auto DevOps съществува, позволяващ

автоматично да открива, изгражда, тества, внедрява и следи вашите приложения

Сега в плановете е да конфигурираме пайплайн за разгръщане на приложения в Azure, с използване на Pulumi и автоматично откриване на целевата среда, което ще бъде разгледано в следващата статия.

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster