- Мета Проєкту
- Подальші плани
- TODO
- Структура директорій
- Вихідний код
- Програмні засоби
- Модель процесу розробки
- Git Flow
- Середовище Dev
- Середовище Stage
- Середовище Production
- Опис CI/CD
- Створення файлів конфігурацій та kubernetes deployment з шаблонів
- Підключення Kubernetes кластеру до GitLab
- Опис Ansible ролі
rancher-cluster
- Ansible роль 'rancher-cluster'
- Actions:
- ./tasks/rancher-cluster.yml:
- ./tasks/rancher-control.yml:
- ./tasks/rancher-nodes.yml:
- ./tasks/rancher-server.yml:
- ./tasks/ubuntu20/base-packages.yml:
- ./tasks/ubuntu20/docker.yml:
- ./tasks/ubuntu20/hosts.yml:
- ./tasks/ubuntu20/pip.yml:
- ./tasks/ubuntu20/rancher-dirs.yml:
- ./tasks/ubuntu20/timesync.yml:
- ./tasks/ubuntu20/upgrade.yml:
- Tags:
- Variables:
- TODO:
- Author Information
- Actions:
- Приклад Inventory для ролі
rancher-cluster
- Опис ресурсів Terraform
- Як створювати та оновлювати документацію по Проєкту
Мені, як Devops спеціалісту, було потрібно вдосконалити наступні навички:
-
Вивчення підходів та інструментів для автоматичного створення документації по інфраструктурі. Максимально автоматизувати цей процес.
-
Підібрати баланс кількість/якість/розуміння по тому як подавати інформацію про інфраструктуру та CI/CD
-
Робота с Docker, збирання образів
-
Робота з on-line сервісом GitLab:
- GitLab CI/CD, gitlab-runner
- GitLab private Docker registry
- GitLab Terraform state files registry
- інтегрування з кластерами Kubernetes
-
Робота з Ansible
-
Робота з Terraform
-
Робота з хмарними сервісами AWS: EKS, Route53, VPC, IAM, ALB, EC2,
-
Створення на базі bare-metal серверів та гіпервізору Proxmox VE інфраструктури для хостингу Kubernetes
-
AWS Cloud Watch, AWS Cloud Trail
-
Google Cloud Platform
-
ELK Stack або Loki
-
Istio Service Mesh
-
Prometheus
-
Grafana
-
системи documentation as code на кшталт Hugo або Docuzaurus
-
інтегрування Hashicorp Vault з Gitlab, Rancher, AWS EKS
-
Jenkins, JenkinsX
-
FluxCD
- Додати в terraform
aws_acm_certificate
для https termination сабдоменів - Рішення для Ingress EKS:
- Nginx Ingress + External DNS
- AWS Application Load Balancer
- AWS Application Load Balancer + External DNS
- Додати в EKS Kubernetes Extentions
- Persistent Volume Claim в AWS EKS
- Створити в terraform декілька IAM з різним рівнем для декількох розробників
- Доробити CI/CD deploy на production
./src
– містить вихідні коди мікросервісів проєкт./tools
– допоміжні застосунки для проєкту- Docker файли для утиліт, які можуть використовуватись на комп`ютері розробника в docker compose
- bash-скрипти для автоматичного парсінгу та створення
документації -
ansible-autodoc
,terraform-docs
,merge-markdown
- рендерінг jinja2-templates за допомогою
jinja2-renderer
./ci
– містить yaml-файли, які використовуються в.gitlab-ci.yml
. Кожен файл відповідає за окремий сервіс. Всі файли мають однакову структуру та генеруються з шаблону.gitlab
– містить налаштування для з`єднання з кластерами./img
– містить всі зображення, яки використовують в документації./templates
– містить шаблони jinja2 для створення yaml-файлів для.gitlab-ci.yml
та kubernetes deployment./kubernetes
– містить згенеровані з шаблонів yaml-файли для kubernetes deployment./docs
– містить конфігураційні файли дляterraform-docs
,merge-markdown
та для окремих файлів документації, які не генеруються автоматично./ansible
– містить ansible-роль для створення кластера Kubernetes за допомогою Rancher./terraform
– містить ресурси Terraform для створення AWS EKS
Основою цього тестового Проєкту є досить вдалий код від Google (він дійсно працює) — Online Boutique. Це "хмарний" веб-додаток для електронної комерції, де користувачі можуть переглядати товари, додавати їх у кошик та купувати. Online Boutique складається з 11 мікросервісів, які створені на різних мовах програмування.
Мікросервіс | Мова програмування | Опис |
---|---|---|
frontend: ./src/frontend |
Go | HTTP-сервер для обслуговування веб-сайту. Не потребує реєстрації/входу та автоматично генерує ідентифікатори сеансу для всіх користувачів. |
cartservice: ./src/cartservice |
C# | Зберігає товари в кошику користувача в Redis і отримує їх. |
productcatalogservice: ./src/productcatalogservice |
Go | Надає список продуктів із файлу JSON і можливість пошуку продуктів і отримання окремих продуктів. |
currencyservice: ./src/currencyservice |
Node.js | Конвертує одну грошову суму в іншу валюту. Використовує реальні значення, отримані від Європейського центрального банку. |
paymentservice: ./src/paymentservice |
Node.js | Стягує з указаної інформації кредитної картки (фіктивну) вказану суму та повертає ідентифікатор транзакції. |
shippingservice: ./src/shippingservice |
Go | Дає оцінку вартості доставки на основі кошика для покупок. Відправляє товари за вказаною адресою (імітація) |
emailservice – ./src/emailservice | Python | Надсилає користувачам електронний лист із підтвердженням замовлення (макет). |
checkoutservice: ./src/checkoutservice |
Go | Отримує кошик користувача, готує замовлення та організовує оплату, доставку та сповіщення електронною поштою. |
recommendationservice: ./src/recommendationservice |
Python | Рекомендує інші продукти на основі того, що надано в кошику. |
adservice: ./src/adservice |
Java | Надає текстові оголошення на основі заданих контекстних слів. |
loadgenerator: ./src/loadgenerator |
Python/Locust | Постійно надсилає запити, що імітують реалістичні потоки покупок користувачів, до інтерфейсу. |
Назва | Для чого використовувався |
---|---|
Docker, Docker Compose | Складання образів Docker, запуск декількох образів в одному робочому процесі |
Сервіс GitLab.com | Хостинг програмного коду, хостинг приватного реєстру docker, deploy контейнерів в Kubernetes, хостинг Terraform state, Ci/CD з допомогою gitlab-runner |
Ansible | Автоматичне розгортання Rancher на віртуальних машинах |
Terraform | Створення кластера EKS на AWS, та супутніх сервісів – Route53, ELB, KMS, VPC |
Proxmox VE | Створення на bare-metal інфраструктури для Stage (UAT) |
Rancher (RKE1) | Розгортання та адміністрування Kubernetes на віртуальних машинах. Документація |
merge-markdown | Автоматична компіляція html сайту, та Readme.md з документацією по всім розділам. Документація. |
ansible-autodoc | Автоматичне створення документації з коментарів коду Ansible role. Документація |
terraform-docs | Автоматичне створення документації з коментарів коду Terraform файлів. Документація |
jinja2-render | Створює файли за допомогою jinja2-templates. Документація |
detect-secrets | Шукає файли з sensitive content у файлах Проєкту. Документація |
pluralith | Візуалізує інфраструктуру aws із файлу terraform.state ( не зовсім корисний але допомагає бачити масштаб ). Документація |
Структуру розробки можна поділити на три складові:
- Програмний код, який супроводжується розробниками
- Створення Docker-образів з програмного коду, хостинг образів в приватному реєстрі Docker
- Запуск та оркестрування контейнерів на базі цих образів
Для ускладнення проєкт програмний код всіх мікросервісів був об'єднаний в monorepo, у
вигляді ./src/{microservice-name}
Використовується схема gitlab-flow:
- Dev – основна гілка розробки.
- Stage – гілка для тестування (UAT – User Acceptible Testing)
- Production – гілка, на основі якої працює продуктивне середовище
Розгортується на комп'ютері розробника. Складання образу відбувається в Docker за допомогою gitlab-runner
. Він може
працювати на комп'ютері розробника, або на окремому сервері. Після складання образ публікується у приватному реєстрі на
GitLab. Йому призначаються теги -- latest
, та git short sha1
.
Це дозволяє використовувати образи, які складені іншими розробниками. Їх можна знаходити по схемі:
№ завдання в Jira ===> № у назві git-branch ===> git short-sha ===> docker-image: short-sha1
Запуск та розгортання відбувається в docker-compose.yml
. Спочатку там використовуються образи з тегом latest.
Треба додати docker-compose.yml
в .gitignore
, та потім використовувати в ньому робочі
теги ( latest
, short sha1
).
В моєму випадку використовувались 5 віртуальних машин: 2 ядра CPU, 4-8 Gb оперативної пам'яті, Ubuntu 20 LTS. Зовнішній доступ — фіксований зовнішній IP, віртуальна машина з PFSense + Haproxy.
На базі Rancher був розгорнутий кластер Kubernetes з окремим project
таnamespace
для цього Проєкту. Також було
підключено приватний реєстр GitLab для образів Docker.
Зовнішній трафік був спрямований на PFSense + Haproxy з використанням Letsencrypt для SSL-termination, далі він
пересилався на NodeIP
кластера Kubernetes
1. Обробка змін в каталогах с вихідним кодом мікросервісів.
Основний процес розробки. При змінах в певних каталогах src/{project_name}/*
запускаються автоматичні дії:
-
Збирання образу Docker
-
После збирання,образу призначається "службовий" тег, який визначається у змінній
$DEBUG
:
DEBUG_TAG: $CI_REGISTRY/$CI_PROJECT_ROOT_NAMESPACE/$CI_PROJECT_NAME/$IMAGE_NAME:$CI_COMMIT_SHORT_SHA
-
Образ с тегом з пункту 2 завантажується в реєстр. Необхідно щоб було зафіксовано артифакт успішного збирання образу, якщо далі буде збій pipeline.
-
Завантаження із реєстру образа с тегом
$DEBUG
-
Надання тегів
DEV_TAG:
- тег для розробників, щоб вони завжди могли працювати з останньою версією –$CI_REGISTRY/$CI_PROJECT_ROOT_NAMESPACE/$CI_PROJECT_NAME/$IMAGE_NAME:latest
, тегуBRANCH_TAG
-$CI_REGISTRY/$CI_PROJECT_ROOT_NAMESPACE/$CI_PROJECT_NAME/$IMAGE_NAME:$CI_COMMIT_BRANCH
- тег гілки з якої відбувається build. Це зроблено з метою ілюстрації. Можна додавати теги поgit-tag
,release
та ін. -
Завантаження у реєстр образів с тегами з пункту 5
-
Зміна образа, який працює з відповідним
debpoiment
у кластерахkubernetes
. Наприклад, для тестування, образ змінюється на тег$DEBUG
.
2. Мати гнучкі функції по збиранню образів Docker
Повинна бути профілактика реєстру образів – видалення старих, безпекові тестування та інше. Але інколи для debug або для усунення технічного боргу, потрібно мати старий образ, або образ якогось git-commit. Може виникнути ситуація, коли потрібно мігрувати в інший приватний реєстр Docker образів. Для цього є функція автоматичного збирання образу якщо в gitc ommint є спеціальний текст.
Наприклад, щоб зібрати образ для мікросервісу cartservice
потрібно в тексті опису git commit
вказати cartservice-manual
. Тоді буде автоматично збиратись відповідний образ по build workflow з першого розділу.
**3. Мати можливість автоматично робити Kubernetes Deployment у відповідний кластер **
Для всіх сервісів створені deployment в кластер kubernetes, які розташовані в
в ./kubernetes/deploy-{envirinment_name}-{service_name}.yaml
.
Тобто файл Kubernetes Deployment для мікросервісу
cartservice
для середовищаstage
буде мати назвуdeploy-stage-cartservice.yaml
Якщо ці фйли змінено, то за допомогою образу bitnamy:kubectl
виконується оновлення deployment, що змінився. Або
створюється новий, якщо з`являється новий файл.
Змінні, які не є частиною IaC-продуктів (Ansible, Terraform) використовуються у наступних файлах:
- Gitlab CI — на рівні Проєкту в панелі керування сервером, на рівні змінних у
.gitlab-cy.yml
. Ці змінні опрацьовуються gitlab-runner - ./templates/ci-template.yaml.j2 — змінні, які використовуються
jinja2-render
при створенні шаблонів, з яких генеруються файли створення docker образів мікросервісів в./ci/ci-{{service}}.yml
- ./templates/deploy-template.yaml.j2 — змінні, які використовуються
jinja2-render
при створенні шаблонів, з яких генеруються файли kubernetes deployment для мікросервісів./kubernetes/{service}-{git_branch_or_stage}-deploiment.yml
Змінна | Опис |
---|---|
CI_REGISTRY_USER |
Користувач приватного реєстру Docker Gitlab. Встановлюється як глобальна змінна групи або Проєкту в Gitlab |
CI_REGISTRY_PASSWORD |
Пароль користувача приватного реєстру Docker Gitlab. Встановлюється як глобальна змінна групи або Проєкту в Gitlab |
DEBUG_TAG |
$CI_REGISTRY /$CI_PROJECT_ROOT_NAMESPACE /$CI_PROJECT_NAME /$IMAGE_NAME :$CI_COMMIT_SHORT_SHA - складений тег для образів з git-short-sha |
BRANCH_TAG |
$CI_REGISTRY /$CI_PROJECT_ROOT_NAMESPACE /$CI_PROJECT_NAME /$IMAGE_NAME :$CI_COMMIT_BRANCH - складений тег для образів з git-branch |
DEV_TAG |
$CI_REGISTRY /$CI_PROJECT_ROOT_NAMESPACE /$CI_PROJECT_NAME /$IMAGE_NAME:latest - складений тег для образів з latest |
DEBUG_IMAGE |
$IMAGE_NAME :$CI_COMMIT_SHORT_SHA |
DOCKER_REGISTRY_URL |
url Docker Hub або аналогів, використовувати, якщо не плануєте користуватись приватним реєстром Docker Gitlab |
DOCKER_REGISTRY_USER |
login користувача у Docker Hub або аналог |
DOCKER_REGISTRY_PASSWD |
пароль користувача Docker Hub або аналогів |
BUILD_CONTEXT |
Docker WORKDIR - відносно цієї директорії буде відбуватися складання образу Docker |
IMAGE_NAME |
Назва образу мікросервісу |
KUBE_CONTEXT |
uri зв'язку gitlab.com з кластером Kubernetes |
PROJECT_NAMESPACE |
Kubernetes namespace в якому будуть розгорнуті deployments мікросервисів |
DEFAULT_IMAGE |
Образ по замовчуванню, на базі якого gitlab-runner буде складати образи мікросервісів |
Змінна | Опис |
---|---|
project_path |
GitLab uri поточного Проєкту на сервері. Приклад: gitlab.example.com/group-name/project-name |
branch |
Гілка git, в якої розробляються скрипти для деплою |
service |
Назва мікросервісу |
build_context |
Docker WORKDIR - відносно цієї директорії буде відбуватися складання образу Docker, дублюється за відповідною змінною в BUILD_CONTEXT GitLab Ci |
environment_name |
Назва оточення, на яке буде розгортатись deploy. Дублює змінну GitLab CI –CI_ENVIRONMENT_NAME |
environment_url |
URL оточення, на яке буде розгортатись deploy. Дублює змінну GitLab CI –CI_ENVIRONMENT_URL |
autorules |
Перелік директорій в Проєкті, в яких будуть відстежуватись зміни файлів для автоматичного збирання образів та deploy |
Приклади використання змінних:
Проєкт gitlab та git branch, з якого будуть включені файли ci_docker_build.yml
та ci_variables.yml
:
include:
- project: "{{ project_path }}"
ref: "{{ branch }}"
file:
- ci/ci_docker_build.yml
- ci/ci_variables.yml
Визначення GitLab environment
:
environment:
name: { { environment_name } }
url: { { environment_url } }
Визначення правил для автоматичного збирання образів докер для сервісу:
.{{ service }}_auto_rules:
rules:
- changes:
{ { autorules } }
when: always
Змінна | Опис |
---|---|
deployment_name |
Назва kubernetes deploymet, яка буде вказана в Cluster Control. Бажано вказувати назву мікросервісу, або вказувати зв`язок з оточенням. Приклад: "deployment_name ": "stage-emailservice". Використовується в labels |
image_pull_secrets |
Kubernetes Secret, який містить ідентіфікатори доступу до приватного реєстру docker образів |
app_name |
Назва мікросервісу. Приклад: "app_name": "emailservice". Використовується в labels |
app_version |
Версія або реліз мікросервісу. Бажано використовувати правила для Semantic Versioning. Використовується в labels |
project_name |
Якщо використовується Rancher, назва Проєкту у Rancher. В Rancher до POD у межах Проєкту заборонений трафік POD за межами Проєкту. Це дає додаткову можливість захисту. !!! Проєкт не namespace. В межах Проєкту може буди декілько namespace. Використовується в labels |
namespace_name |
namespace в який буде проводитись Kubernetes Deployment. Використовується в labels |
app_creator |
email або інший контакт розробника. Використовується в labels |
app_supporter |
Відповідальний за support. mail або інший контакт. Використовується в labels |
environment_stage |
Назва оточення, на яке буде розгортатись deploy. Дублює змінну GitLab CI –CI_ENVIRONMENT_NAME , та environment_name в ./templates/ci-template.yaml.j2. Використовується в labels |
environment_uri |
URL оточення, на яке буде розгортатись deploy. Дублює змінну GitLab CI –CI_ENVIRONMENT_URL , та environment_url в ./templates/ci-template.yaml.j2. Використовується в labels |
app_component |
Яку функцію виконує мікросервіс в загальному продукті. Використовується в labels. Приклад: "app_component": "email-campain" |
business_domain |
Показує зв`язок між мікросервісом, та бізнес-функцією на яку він має вплив. Використовується в labels. Приклад: "business_domain": "marketing" |
business_owner |
Контакт відділу або відповідального за бізнес-функцію. Використовується в labels. Приклад: "business_owner": "marketing@company.com" |
app_instance |
На якій базі розгорнуто оточення для deploy. baremetal, rancher, eks. Використовується в labels. Приклад: "app_instance": "aws-eks"; "app_instance": "baremetal-rancher"; "app_instance": "ec2-kubespray" |
app_replicas |
Кількість екземплярів мікросервісу, які мають працювати |
image |
Повний URL образу Docker. Приклад для Gitlab: registry.gitlab.com/{gitlab_group}/project/{service_name}:{tag} |
requests_memory |
Скільки відводити оперативної пам`яті (мінімум) для одного екземпляру контейнера |
requests_cpu |
Скільки відводити cpu_millicores (мінімум) для одного екземпляру контейнера |
limits_memory |
Скільки відводити оперативної пам`яті (максимум) для одного екземпляру контейнера |
limits_cpu |
Скільки відводити cpu_millicores (максимум) для одного екземпляру контейнера |
ports_container_port |
Порт, на якому POD приймає вхідний трафік |
port_name |
Назва порту |
ports_protocol |
Протокол, по якому працює POD в Kubernetes Deployment. TCP, UDP, SCTP |
service_port |
Порт, на який контейнер приймає вхідний трафік. Відноситься до Kubernetes Service |
service_protocol |
Протокол, по якому працює Kubernetes Service. TCP, UDP, SCTP. Приклад: spec.ports.protocol: TCP. Відноситься до Kubernetes Service |
service_target_port |
Порт на який буде спрямовано трафік з service_port . Відноситься до Kubernetes Service |
service_ip_type |
Яким чином Kubernetes Service публікує порти — ExternalName , ClusterIP , NodePort , LoadBalancer . Відноситься до Kubernetes Service |
app_env |
Змінні для специфікації контейнеру |
{service} — змінна, в яку підставляється назва мікросервісу (
./scr/{service}
)
Згідно схеми gitlab-flow — це основна робоча гілка. Після коміту автоматично буде створено образ Docker з відповідними тегами:
- {service}-image:latest — основний робочий артефакт гілки dev. Використовується в
docker-compose.yml
на комп'ютері розробника. Для тестування власної розробки та останніх комітів мікросервісів колег - {service}-image:sha1 — використовується для зв'язку: № Jira Task ===> № Jira Task у назві git-branch ===> git
short-sha ===> {service}-image:sha1. Потрібен для debug, або для перевірки робочих гіпротез на
локальному
docker-compose.yml
. - {service}-image:dev — сінонім {service}-image:latest
Всі ці теги будуть завантажені у Docker Registry (Docker Hub або приватний)
Гілка для тестів.
Створює образ з тегами:
- {service}-image:sha1 — debug або Jira Task
- {service}-image:stage — артифакт, який буде автоматично викладено на середовище stage (Proxmox + Rancher) за допомогою ./kubernetes/{service}-stage-deploiment.yml
Всі ці теги будуть завантажені у Docker Registry (Docker Hub або приватний)
Гілка для робочого середовища.
Створює образ з тегами:
- {service}-image:sha1 — debug або Jira Task
- {service}-image:master — артифакт, який буде автоматично викладено на середовище production (AWS EKS) за допомогою ./kubernetes/{service}-master-deploiment.yml
Всі ці теги будуть завантажені у Docker Registry (Docker Hub або приватний)
Головний файл стандарний — ./gitlab-ci.yml
у "корені" Проєкту. Він включає решту файлів, яка підключаються за
допомогою директиви include
.
У сервісі gitlab.com у мене не вийшло підключати файли за допомогою (local), повідомлення linter — "file not found"
include: - local: '/templates/.gitlab-ci-template.yml'
Тому довелося використовувати "hack":
include: - project: "project_name" ref: "ci-template" file: - ci/ci_docker_build.yml - ci/ci_variables.yml - ci/kubectl_handler.ymlДе у якості
project_name
був цей Проєкт.
Звісно, що є принята практика створювати окремий Проєкт для всій логіки CI/CD, та писати узагальнені скрипти. Але це занадто ускладнює учбовий Проєкт.
Але для коректного використання оператору
include
потрібно вказувати параметрref:
.Тому всі зміні в скриптах GitLabCI, Kubernetes Deployment та інше потрібно виконувати в гілці
ci-template
.Звісно це не зовсім зручно, ало поки буде так.
Кожен сервіс має свій "іменний" CI/CD-файл ./ci/ci-{service}.yml
та свій Kubernetes
Deploiment ./kubernetes/{service}-stage-deploiment.yml
.
- build-debug-image — створюється образ docker, надається тег short-sha, образ завантажується в docker registry
- pull-debug — вивантаження з реєстру, надання інших тегів
- push-image — завантаження нових тегів в реєстр
- deploy — деплой в kubernetes cluster
До Проєкту додані файли, які можна використовуваті в інших Проєктах.
./ci/ci_docker_build.yml
— містить hidden jobs по
створенню, тегуванню та завантаженню образів. Ці дії можна підключати за
допомогою anchors
./ci/ci_variables.yml
— містить основні змінні, які підключаються до всіх ci-файлів.
Ці два файли підключаються в кожен ./ci/ci-{service}.yml
Система назви jobs:
- {{service}}_auto_rules — правила, по яким спрацьювує автоматичний build docker image
- {{service}}_manual_rules — правила, по яким спрацьовує ручний build docker image
- auto_{{service}}_debug — автоматичне створення и push docker image:sha1
- manual_{{service}}_debug — ручне створення и push docker image:sha1
- pull_auto_{{service}}_sha — pull образів docker з артефакту auto_{{service}}_debug
- pull_manual_{{service}}_sha — pull образів docker з manual_{{service}}_debug
- push_currencyservice_image — push образів docker з тегами latest, branch_name
Відстежування змін файлів у директоріях:
.emailservice_auto_rules:
rules:
- changes:
- src/{{service}}/**/**/*
when: always
Відстежування тексу git commit:
.emailservice_manual_rules:
rules:
- if: "$CI_COMMIT_MESSAGE =~ /emailservice-manual/"
when: always
Завантаження образів docker в реєстр:
push_{{ service }}_image:
stage: push-image
# Змінні
variables:
<<: *global_var
IMAGE_NAME: { { service } }
# Перевикористання jobs з умовами для завантаження образів
extends:
- .deploy_latest_tag_to_registry
- .deploy_branch_tag_to_registry
# Умиви, за яких відбувається завантаження образів
rules:
- !reference [ ".{{ service }}_auto_rules", "rules" ]
- !reference [ ".{{ service }}_manual_rules", "rules" ]
# Залежність від виконання попередніх jobs
needs:
- job: pull_manual_{{ service }}_sha
optional: true
- job: pull_auto_{{ service }}_sha
optional: true
Завантаження образу за тегом latest
, якщо назва git brabch дорівнюєdev
, для використання на комп`ютері розробника
за допомогою docker compose:
# Extnetd from ./ci/ci_docker_build.yml
.deploy_latest_tag_to_registry:
stage: push-image
script:
- docker tag $DEBUG_TAG $DEV_TAG
- docker push $DEV_TAG
rules:
- if: "$CI_COMMIT_BRANCH =~ /dev/"
when: auto
when: on_success
Завантаження образу з тегом branch_name, якщо назва git brach дорівнює stage
або main
:
# Extnetd from ./ci/ci_docker_build.yml
.deploy_branch_tag_to_registry:
stage: push-image
script:
- docker tag $DEBUG_TAG $BRANCH_TAG
- docker push $BRANCH_TAG
rules:
- if: "$CI_COMMIT_BRANCH =~ /stage/ || $CI_COMMIT_BRANCH =~ /main/"
when: on_success
Кожен мікросервіс має свої конфігураційні файли:
- ./ci/ci-{service}.yml — GitLab CI файл для створення образів мікросервісів та розгортання їх у кластер Kubernetes
- ./kubernetes/{service}-{environment}-deployment.yaml — Kubernetes Deployment для мікросервісу для конкретного
оточення (
stage
,prod
) - ./kubernetes/{service}-{environment}-service.yaml — Kubernetes Service для мікросервісу для конкретного
оточення (
stage
,prod
)
Всі ці файли створюються з jinja2 templates за допомогою python скрипта jinja2-render.
pip install jinja2-render
Застосування:
jinja2-render -c {файл контексту} -f {шаблон з якого створюються файли} -o {віхідний файл} {пункт контексту}
файл контексту – json-файл в якому визначаються змінні та їх значення, які потім будуть застосовані в вихідних файлах.
шаблон з якого створюються файли – jinja2 шаблон, в якому застосовуються змінні
пункт контексту – першій рівень у списку в json
Приклад:
файлу контексту – debpoy-prod-env.py:
CONTEXTS = {
"adservice": {
"deployment_name": "prod-adservice",
"deployment_app": "adservice",
"app_replicas": "1",
"image": "registry.gitlab.com/devops1121/microservices-demo-google/adservice:stage",
"image_pull_secrets": "registry.gitlab",
"app_name": "adservice",
"app_version": "0.1.0",
"project_name": "microservices-demo",
"namespace_name": "prod-microservices-demo",
"app_creator": "developer_company.com",
"app_supporter": "devops_company.com",
"environment_stage": "prod",
"environment_uri": "prod-env.company.com",
"app_component": "advertise-service",
"business_domain": "sales",
"business_owner": "marketing_company.com",
"app_instance": "baremetal-rancher",
"requests_memory": "512Mi",
"requests_cpu": "500m",
"limits_memory": "1Gi",
"limits_cpu": "1000m",
"ports_container_port": "9555",
"ports_protocol": "TCP",
"port_name": "grpc",
"service_port_name": "grpc",
"service_port": "9555",
"service_protocol": "TCP",
"service_target_port": "9556",
"service_ip_type": "ClusterIP",
"app_env": "- name: PORT\n value: \"9555\"\n - name: \"DISABLE_STATS\"\n value: \"1\"\n - name: \"DISABLE_TRACING\"\n value: \"1\"\n"
},
"cartservice": {
"deployment_name": "prod-cartservice",
"deployment_app": "cartservice",
"app_replicas": "1",
"image": "registry.gitlab.com/devops1121/microservices-demo-google/cartservice:stage",
"image_pull_secrets": "registry.gitlab",
"app_name": "cartservice",
"app_version": "0.1.0",
"project_name": "microservices-demo",
"namespace_name": "prod-microservices-demo",
"app_creator": "developer_company.com",
"app_supporter": "devops_company.com",
"environment_stage": "prod",
"environment_uri": "prod-env.company.com",
"app_component": "advertise-service",
"business_domain": "sales",
"business_owner": "marketing_company.com",
"app_instance": "baremetal-rancher",
"requests_memory": "512Mi",
"requests_cpu": "500m",
"limits_memory": "1Gi",
"limits_cpu": "1000m",
"ports_container_port": "7070",
"ports_protocol": "TCP",
"port_name": "grpc",
"service_port_name": "web",
"service_port": "7070",
"service_protocol": "TCP",
"service_target_port": "7070",
"service_ip_type": "ClusterIP",
"app_env": "- name: REDIS_ADDR\n value: \"redis-cart:6379\"\n"
}
}
Файл шаблону – deploy-template.yaml.j2
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ deployment_name }}
namespace: {{ namespace_name }}
labels:
deployment_name: {{ deployment_name }}
app.kubernetes.io/name: {{ app_name }}
app.kubernetes.io/version: "{{ app_version }}"
project_name: {{ project_name }}
namespace_name: {{ namespace_name }}
app.kubernetes.io/created-by: {{ app_creator }}
app.kubernetes.io/managed-by: {{ app_supporter }}
environment_stage: {{ environment_stage }}
environment_uri: {{ environment_uri }}
app.kubernetes.io/component: {{ app_component }}
business_domain: {{ business_domain }}
business_owner: {{ business_owner}}
app.kubernetes.io/instance: {{ app_instance }}
spec:
replicas: {{ app_replicas }}
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
template:
metadata:
name: {{ deployment_name }}
namespace: {{ namespace_name }}
labels:
deployment_name: {{ deployment_name }}
deployment_app: {{ deployment_app }}
app.kubernetes.io/name: {{ app_name }}
app.kubernetes.io/version: "{{ app_version }}"
project_name: {{ project_name }}
namespace_name: {{ namespace_name }}
spec:
containers:
- name: {{ deployment_name }}
image: {{ image }}
imagePullPolicy: Always
resources:
requests:
memory: {{ requests_memory }}
cpu: {{ requests_cpu }}
limits:
memory: {{ limits_memory }}
cpu: {{ limits_cpu }}
ports:
- containerPort: {{ ports_container_port }}
protocol: {{ ports_protocol }}
name: {{port_name}}
env:
{{app_env}}
restartPolicy: Always
imagePullSecrets:
- name: {{ image_pull_secrets }}
selector:
matchLabels:
deployment_app: {{ deployment_app }}
namespace_name: {{ namespace_name }}
environment_stage: {{ environment_stage }}
Створити Kubernetes deployment для adservice:
jinja2-render -c ./debpoy-prod-env.py -f ./deploy-template.yaml.j2 -o ./adservice-prod-deploy.yaml adservice
Створити Kubernetes deployment для cartservice:
jinja2-render -c ./debpoy-prod-env.py -f ./deploy-template.yaml.j2 -o ./cartservice-prod-deploy.yaml cartservice
- файл контексту для Gitlab CI — ./templates/ci-env.py
- шаблон з якого створюються файли Gitlab CI — ./templates/ci-template.yaml.j2
- вихідні файли — ./ci/ci_{service}.yml
- bash скрипт, який автоматично створює файли для всіх сервісів в ./src/ — ./tools/ci-template.sh
-
файл контексту для Kubernetes Deployment — ./templates/debpoy-stage-env.py (оточення
stage
), ./templates/debpoy-prod-env.py (оточенняprod
) -
шаблон з якого створюються файли Kubernetes Deployment — ./templates/deploy-template.yaml.j2
-
вихідні файли — ./kubernetes/{service}-{environment}-deployment.yaml
-
bash скрипт, який автоматично створює файли для всіх сервісів в ./src/ — ./tools/kube-deploy-template.sh Застосування:
./kube-deploy-template.sh prod або ./kube-deploy-template.sh stage
Скрипт використовує результат команди
ls ./src
як перелік значень для створення відповідних файлів. Тобто, якщо є каталог сервісаadservice
, то повинен будт відповідний контекс у файлі контексту.
У адмініструванні Kubernetes можуть бути різні ситуації, коли потрібно створювати та зміновати різні об`єкті Kubernetes. Ці віпалки важко продумати заздалегідь та створити відповідні jinja2 templates. Тому решту файлів для об` єктів Kubernetes потрібно створювати самойстійно у форматі:
./kubernetes/{service}-{environment}-{kubernetes-resource}.yaml.
Зміни у диркторії ./kubernetes/ обпрацьовуються автоматично GitLabCI pipeline
gitla-agent
інтегрує Gitlab с кластером Kubernetes. Працює навіть за NAT.
Потрібно створити порожній файл в .gitlab/agents/{agent-name}/config.yaml
Потім зареєструвати агента в GitLab:
- Infrastructure ==> Kubernetes clusters ==> Create agent
- Ввести
{agent-name}
та отримати токен -{cluster-token}
- В кластері:
helm repo add gitlab https://charts.gitlab.io helm repo update helm upgrade --install `{agent-name}` gitlab/gitlab-agent \ --namespace gitlab-agent-`{agent-name}` \ --create-namespace \ --set image.tag=v15.9.0-rc1 \ --set config.token=`{cluster-token}` \ --set config.kasAddress=wss://kas.gitlab.com
Це нам каже офіційна документація.
Але в мене в Rancher запрацювало якщо додати HELM репозіторій та додади агента за допомогою yaml:
affinity: { }
config:
kasAddress: wss://kas.gitlab.com
token: `{ cluster-token }`
extraEnv: [ ]
fullnameOverride: ''
image:
pullPolicy: IfNotPresent
repository: registry.gitlab.com/gitlab-org/cluster-integration/gitlab-agent/agentk
tag: 'v15.2.0'
imagePullSecrets: [ ]
nameOverride: ''
nodeSelector: { }
podAnnotations:
prometheus.io/path: /metrics
prometheus.io/port: '8080'
prometheus.io/scrape: 'true'
rbac:
create: true
resources: { }
serviceAccount:
annotations: { }
create: true
tolerations: [ ]
Агент може працювати в двох режимах:
- GitOps workflow - GitLab agent в кластері періодично відстежує зміни у віхідному коді Kubernetes Deployment, та робить pull з репозіторію Проєкту
- GitLab CI/CD workflow - за допомогою
kubectl
pipeline push зміни у віхідному коді Kubernetes Deployment у кластер
Якщо використовувати GitLab CI/CD workflow, то файл .gitlab/agents/{agent-name}/config.yaml
повинен мати такий
вигляд:
ci_access:
projects:
- id: path/to/project
Для встановлення Kubernetes використовується RKE1 (Rancher Kubernetes Engine v.1) від Rancher Labs.
RKE — це дистрибутив Kubernetes, сертифікований CNCF, якій повністю працює в контейнерах Docker
В ролі використовуються 5 віртуальних машин (нод):
- 1 Rancher Server — на неї встановлюється первинний набір ПЗ та нода для кластера
etcd
- 2 Control Plane
Node —
kube-apiserver
,etcd
,cloud controller manager
,kube-controller manager
,kube-sheduler
- 2 Worker Node —
kubelet
,kubeproxy
В інфраструктурі використовується ProxmoxVE KVM з Ubuntu 20.04 (LTS)
Оскільки Rancher Kubernetes Engine ще "молодий" продукт — ще не все добре працює, тому рекомендовано використовувати саме ті версії системного ПЗ, які 100% працюють в цій ролі:
- rancher_image: rancher:v2.6.6
- rancher_agent_image: rancher-agent:v2.6.6
- ansible_distribution: ubuntu
- ansible_distribution_release: focal
- rancher_k8s_version: 1.20.15
- docker: 5:20.10.12~3-0~ubuntu-focal
Роль створена для автоматичного встановлення кластера kubernetes для навчання та експериментів.
Actions performed by this role
- Імпортує
rancher_api_key
з "віртуального" хоста. Створює Kubernetes кластер зі значеннями зміннихrancher_k8s_version
таrancher_cluster_name
. Генерує bearer token для реєстрації нод -rancher_registration_token
, посилання для реєстрації нод -rancher_registration_link
, команду для реєстрації нод -rancher_node_register
, до якої буде додаватись зміннаrancher_role_flags
, в залежності від ролі ноди в кластері. Всі ці змінні експортуються через "віртуальні" хости для інших задач. (rancher-cluster)
- Очікує три хвилини щоб кластер повністю розгорнувся, імпортує значення змінної
export_node_start
з віртуального хоста, додаєControl Plane
ноду до кластера. Задача виконується тільки на нодах де в inventoryrancher_node_role: "control"
(rancher-cluster)
- Очікує 5 хвилин щоб кластер та
Control Plane
ноди повністю розгорнулися, імпортує значення змінноїexport_node_start
з віртуального хоста, додаєWorker Node
до кластера. Задача виконується тільки на нодах де в inventoryrancher_node_role: "node"
(rancher-cluster)
- Запускає на ноді з
rancher_node_role: "server"
контейнерranсher-server
, який запускає процес інсталювання, та приймає з'єднання на портах 80,443. На ці операції відведено 3 хвилини (в залежності від "заліза" можна вказати свої параметри). Потім за допомогоюcurl
здійснюються 3 спроби логину на сервер - це "хак", бо Rancher незрозуміло поводить себе с токенами, дуже часто їх змінює. Експерименти показали, що з третьої спроби логін на сервер працює без помилок. Також все запрацювало тільки після заміниansible.builtin.command
наansible.builtin.shell
в завданнях. Результат сесії логіну - це json, з якого за допомогоюjq
знаходиться значення параметраtoken
. Він зберігається для інших завдань за допомогою "віртуального" host в inventory:ansible.builtin.add_host
. Таким чином, експортується зміннаexport_login_token
. За допомогоюrancher_login_token.stdout
завантажується api key з сервера, який також експортується через "віртуальний" хост, як зміннаrancher_api_key
. За допомогоюrancher_api_key
призначуєтеся значення змінноїrancher_server_url_name
. (rancher-cluster)
- Визначає які пакети вже є в системі та встановлює лише ті, яких не вистачає. Необхідні пакети визначаються у
змінній
base_packages
. Виразname: {{ base_packages | difference(ansible_facts.packages) }}
порівнює список пакетів зі змінною та з наявними в системі, тому встановлюється тільки різниця. (rancher-cluster)
- Підключає apt repository від Docker, перемикає на нього інсталятор, встановлює Docker, вмикає маршрутизацію
ipv4
, вимикає firewall (тимчасове рішення для тестів), завантажує образи Docker від Rancher, які визначаються через змінніrancher_agent_image
таrancher_image
. (rancher-cluster)
- Призначає ноді необхідний
hostname
та генерує коректний файл/etc/hosts
, в якому вказані всі ноди с їх IP-адресами таhostname
. Для цього використовується шаблон з/templates/hosts.j2
(rancher-cluster)
- Встановлення необхідних python pip-пакетів. Пакети можна додавати в перелік
loop
. Потребує пакетpython3-pip
, який потрібно встановити на ноду. (rancher-cluster)
- Створює каталоги для даних Rancher, які потім будуть примонтовані в Docker контейнери через
volumes
. ( rancher-cluster)
- Створює конфігураційний файл для демона точного часу
tymesyncd
, завантажує на всі ноди, та перезапускає сервіс, щоб не було розбіжностей в часі. Використовується шаблон/templates/timesyncd.conf.j2
, але без змінних для серверів часу та інших параметрів. (rancher-cluster)
- Вимикає автоматичне оновлення пакетів, оновлює наявні до останньої версії, та перезавантажує ноди на випадок якщо було встановлено нове ядро linux. (rancher-cluster)
ansible_port
:22
- SSH host portansible_host
: `` - Host IPansible_distribution
:ubuntu
- Підтримуються: Alpine, Altlinux, Amazon, Archlinux, ClearLinux, Coreos, Centos, Debian, Gentoo, Mandriva, NA, OpenWrt, OracleLinux, RedHat, Slackware, SMGL, SUSE, VMwareESX. Нижній регістр.ansible_distribution_release
:focal
- Версія дистрибутиву. В цієї ролі використовується focal, тобто Ubuntu 20.04 (LTS)rancher_image
:rancher:v2.6.6
- Тег Docker образу сервера Rancher. Буде встановлено на ноду зrancher_node_role: "server"
rancher_agent_image
:rancher-agent:v2.6.6
- Тег Docker образу агента rancher, котрий встановлюється на кожну ноду для зв'язку з сервером Rancher. Буде встановлено на ноду зrancher_node_role: "node"
rancher_node_role
:node
- "Маркер" який вказує на роль ноди в кластері.server
- головний сервер, на базі якого буде будуватися кластер, та встановлюватися необхідне програмне забезпечення на ноди. Потрібна хоча б одна нода с цією роллю.control
- буде встановлено KubernetesControl Plane
.node
- робоча нода (Worker Node
), на яку буде встановлено ПЗ Rancher. Ця змінна по змісту схожа наrancher_role_flags
, але в неї інша функція.rancher_k8s_version
:1.20.15
- Версія Kubernetes, яку буде встановлено на всі ноди.rancher_setup_password
: `` - Пароль для з'єднання с web-інтерфейсом та API для адміністрування. Адреса для з'єднання визначається в змінноїrancher_server_url_name
. Потрібно вказати тільки на нодах з `rancher_node_role: "server"`rancher_role_flags
:--worker
- Роль, яку буде виконувати нода.--etcd
- одна з нод в кластері etcd, які зберігають поточний стан кластера Kubernetes. Потрібно як найменш 3 ноди с цією роллю.--controlplane
- нода на якої виконуються Kubernetes shedulers та ін., може бути в одному екземплярі, але бажано щоб їх було декілька.--worker
- будуть встановленіkubelet
,kube-proxy
. Для--etcd
та--controlplane
потрібно достатньо ресурсів, тому бажано відводити для них не менш ніж 8Gb оперативної пам'яті. Всі ці ролі також можуть буди встановлені на одну ноду.rancher_server_url_name
:"https://192.168.101.141"
- Адреса для з'єднання с web-інтерфейсом та API сервера. Бажано використовувати IP-адресу. Або коректно налаштувати локальну DNS-зону на DNS-сервері.rancher_init_ip
: `` - IP адреса до якої будуть з'єднуватися ноди за допомогоюcurl
. Потрібно вказувати IP-адресу ноди з `rancher_node_role: "server"`rancher_cluster_name
: `` - Ім'я кластера Kubernetes, який буде створено після встановлення всього необхідного ПЗ. Всі сценарії в ролі будуть використовувати з'єднання нод з цим кластером.base_packages
:["bash-completion", "bind9utils", "curl", "git", "git-extras", "htop", "iptraf", "lsof"]
- Базові програмні пакети, яки необхідні для системного адміністрування ноди.
- Замінити вимкнення firewall логікою, яка відкриває потрібні сервіси в залежності від значення
змінної
rancher_node_role
(rancher-cluster)
- Додати логіку пошуку події оновлення ядра, та поставити як умову для перезавантаження ноди, якщо встановлено нове ядро. (rancher-cluster)
This playbook was created by: Andriy pustovit
Documentation generated using: Ansible-autodoc
kubernetes:
gather_facts: false
hosts:
node1.example.net:
ansible_port: 22
ansible_host: 192.168.101.141
ansible_distribution: ubuntu
ansible_distribution_release: focal
ansible_fqdn: "node1.example.net"
ansible_domain: "example.net"
ansible_hostname: "node1.example.net"
inventory_hostname: "node1.example.net"
ansible_nodename: "node1"
rancher_image: "rancher:v2.6.6"
rancher_agent_image: "rancher-agent:v2.6.6"
rancher_node_role: server
rancher_k8s_version: "1.20.15"
rancher_setup_password: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
rancher_role_flags: "--etcd --controlplane --worker"
rancher_server_url_name: "https://192.168.101.141"
rancher_init_ip: 192.168.101.141
rancher_cluster_name: dev-cluster
ansible_ens18:
device: ens18
ipv4:
address: 192.168.101.141
node1.example.net
— дуже важливий параметр для встановлення FQDN ноди, на його основі будуть створені коректні
файли /etc/hosts
.
Це дасть можливість нодам вільно комунікувати, що є обов'язковими умовами для коректної роботи ролі.
rancher_setup_password
- задає пароль для доступу до web-інтерфейсу сервера
ansible_ens18:
device: ens18
ipv4:
address: 192.168.101.141
Дуже важливий параметр — опис мережевих інтерфейсів, залежить від ОС, яка використовується на нодах. В Ubuntu
це ens18
. На цій основі створюється коректний файл /etc/hosts
.
Використовується в шаблоні ansible/roles/rancher-cluster/templates/hosts.j2
{ % for host in groups [ 'kubernetes' ] % }
{ % if 'ansible_ens18' in hostvars[ host ] % }
{ { hostvars[ host ][ "ansible_ens18" ][ "ipv4" ][ "address" ] } } { { hostvars[ host ][ "inventory_hostname" ] } }
{ % endif % }
{ % endfor % }
Name | Version |
---|---|
tls | 4.0.4 |
aws | 4.29.0 |
kubernetes | 2.16.1 |
Name | Version |
---|---|
aws | 4.29.0 |
kubernetes | 2.16.1 |
pgp | 0.2.4 |
tls | 4.0.4 |
Name | Description | Type | Default | Required |
---|---|---|---|---|
aws_access_key | AWS access key. Використовується як ідентифікатор для з'єднання с AWS API | string |
n/a | yes |
aws_secret_key | AWS secret key. Використовується для шифрування сесії з'єднання с AWS API | string |
n/a | yes |
region | AWS регіон де буде розташовано Проєкт | string |
n/a | yes |
project | Назва Проєкту, яку можна використовувати у складі ресурсів | string |
n/a | yes |
az_count | Кількість AZ. На основі кількості AZ створюються ресурси VPC | number |
n/a | yes |
eks_version | Версія Kubernetes для AWS EKS | string |
"1.24" |
no |
resource_tags | Tag, які будуть використовуватись для всіх ресурсів. Треба вказувати у 'provider { default_tags {} }' | map(string) |
{ |
no |
public_dns_zone_name | Публічний домен, якій буде використовуватись на Проєкті. До якого спрямовуватись трафік користувачів продукту | string |
n/a | yes |
public_route53_records | Піддомени та інші ресурси AWS Route53 DNS - CNAME, A, MX, TXT та інші. Але поки без використання Route53 aliases |
list(object({ |
n/a | yes |
private_dns_zone_name | Приватні DNS домени, які можуть використовуватись для внутрішніх сервісів | list(object({ |
n/a | yes |
private_route53_records | Піддомени в приватних DNS зонах для внутрішніх сервісів | list(object({ |
n/a | yes |
vpc_cidr | Блок CIDR Amazon VPC | string |
n/a | yes |
subnet_cidr_bits | Кількість бітів subnet для VPC Subnets. 8 дає маску /24 | number |
n/a | yes |
vpc_public_subnets | CIDRs public-підмереж, які будуть доступні із зовні через AWS Internet Gateway | list(string) |
n/a | yes |
vpc_private_subnets | CIDRs private-підмереж, які можуть бути доступні із зовні через AWS NAT Gateway + AWS Elastic IP | list(string) |
n/a | yes |
node_group_desired_size | Бажана кількість EC2 нод, на яких буде розгорнуто EKS кластер | number |
n/a | yes |
node_group_max_size | Максимальна кількість EC2 нод, на яких буде розгорнуто EKS кластер | number |
n/a | yes |
node_group_min_size | Мінімальна кількість EC2 нод, на яких буде розгорнуто EKS кластер | number |
n/a | yes |
node_instance_type | EC2 тип нод | string |
n/a | yes |
node_instance_disk_size | Розмір жорсткого диску EC2 ноди | number |
n/a | yes |
Name | Description |
---|---|
region | AWS region, в якому встановлюється інфраструктура |
cluster_name | Назва EKS кластера. Визначається за допомогою змінної var.project |
cluster_endpoint | Endpoint для з`еднання з EKS control plane, який буде використовуватись для інструментів розробників та адміністраторів - kubectl, lens, тощо` |
cluster_ca_certificate | TLS сертифікат, який використовується як credentials для EKS control plane |
oidc_provider_arn | Унікальне им`я ресурсу Amazon ARN(Amazon Resource Name) провайдера OIDC (OpenID). Це допомагає Kubernetes Services кластера EKS взаємодіяти з API AWS` |
aws_nat_gateway_public_ip | Зовнішня IP-адреса, яку використовує Public NAT Gateway. Він же Elastic IP |
Документация створюється шляхом об`єднання :
- файлів *.md з каталогу
./docs/
, які створюються самостійно - файлів *.md з каталогу
./ansible/roles/rancher-cluster/docs/doc
, які створюються і самостійно, і автоматично за допомогоюansible-autodoc
- файлу Readme.md з каталогу
./terraform/doc/
, якій створюється автоматично за допомогоюterraform-docs
за допомогою утіліти merge-markdown
Для автоматичного створення використовується bash скріпт ./tools/makedocs.sh
Конфигураційні файли утіліт та документация:
ansible-autodoc — ./ansible/autodoc.conf.yaml
. Документація по роботі
terraform-docs — ./terraform/.terraform-docs.yml
. Документація по роботі
- merge-markdown —
./docs/makedocs.yml
Документація по роботі
Всі зображення та схеми потрібно зберігати в каталозі ./img