Сучасна диджитал-освіта для дітей — безоплатне заняття в GoITeens ×
Mazda CX 30
×

Як організувати локальну розробку. На прикладі команди, яка розвиває медіа в Африці й Азії

Усі статті, обговорення, новини про DevOps — в одному місці. Підписуйтеся на DOU | DevOps!

Усім привіт! Мене звати Андрій Товстоног, я Team Lead команди технічної розробки в компанії GMEM з екосистеми бізнесів Genesis. Ми в GMEM займаємося новинними порталами в Африці та Азії й маємо власну CMS, яку використовуємо на усіх наших проєктах. Власне новинний портал складається з декількох частин — frontend, backend, CMS та усілякі сервіси навколо.

Так історично склалося, що локальна розробка цього всього супроводжувалась деякими «танцями з бубном» на кшталт:

  • згенеруй та додай сертифікати;
  • пропиши хости в hosts файлі;
  • встанови залежності;
  • збери проєкт;
  • запусти docker-compose;
  • все інше. Завжди, навіть при слідуванні README, виходило щось не так. На це впливає і деяка кількість сервісів, що залежать один від одного.

Це наче нескладно, але все ж трошки напружує. Особливо, коли проводиш онбординг нового колеги й намагаєшся допомогти йому підняти локальне середовище, а воно з якоїсь причини не заводиться (Місяць не в тій фазі 🙂).

Нещодавно ми вирішили почати розробку новинного порталу з використанням сучаснішого стеку технологій. Разом із тим постало питання побудови production оточення, stage і, звісно, локального оточення розробки.

У цій статті поділюся нашим баченням такого рішення, і як це тепер працює у нас. Зокрема, розкажу про наш досвід локальної розробки з Kubernetes (далі  —  K8s), який, можливо, допоможе тим, хто зіштовхнувся або постійно зіштовхується з проблемами організації локального середовища розробників.

Пристібайте паски й поїхали. Буде цікаво!

З чого все починалося

Усе, як завжди, починається з якоїсь дрібнички. Отже, ми вирішили будувати проєкт з нуля. Одразу постало питання, які технології обрати, щоб і працювати було цікаво, і «козацького драйву» для розробників додати, і трішки небанального «головного болю» DevOps-інженеру. Новий проєкт, побудований на нових технологіях — це завжди виклик та заохочення до експериментів 🙂

Думаю, що вам буде цікаво, який такий стек ми притягли за вуха обрали:

  • backend —  вирішили залишити на PHP версії 8 із фреймворком Laravel;
  • RoadRunner  —  замість класичного PHP-FPM, вирішили скористатися сервером застосунків для PHP, написаним на Go;
  • frontend  —  використали Next.Js;
  • CMS  —  все той же React;
  • бази  —  класичний MySQL (куди ж без нього 🙂), а також Redis, Memcached, Elasticsearch і MongoDB;
  • Production  —  вирішили використовувати K8s;
  • Stage  —  знову ж таки, кластер на K8s, щоб було максимально подібно;
  • локальне оточення  —  спочатку була думка залишити Docker Compose та й не морочитися. Але врешті вирішили, що коли production і stage на K8s, то і локальне середовище розробки варто будувати на суміжній технології.

Отже, визначившись із технологіями, потроху почали рухатись уперед. Все за SDLC  —  формування вимог, ТЗ й таке інше. А поки все йшло, розпочалась фаза пошуку вирішення «невеличкого головного болю».

Вибір рішення для локального оточення розробки

Позначивши кінцеву або просто контрольну точку, ми пішли дивитися, що вже є на теренах інтернету, врешті-решт, ми не перші, хто взагалі задумався над таким питанням.

Погугливши, ми знайшли декілька варіантів: Skaffold, Devspace, Tilt. Обирати в муках не довелося: проглянули продукти та зрозуміли, що рішення схожі за функціональністю та розв’язують одні проблеми.

Вибір зробили на користь проєкту від хлопаків loft.sh з продуктом Devspace, адже нам дуже сподобався їхній UI. Це непогане доповнення cli-функціональності, яке дозволяє дивитися логи подів, провалюватися в sh та виконувати термінальні команди, а також виконувати команди, які заздалегідь описані в конфігураційному файлі самого Devspace. За необхідності можна подивитися згенерований опис деплойменту.

Запускається він дуже просто й легко однією командою — devspace ui. Сам процес відкриває сторінку локального хоста за адресою localhost:8090/logs/containers. Після запуску команди процес завершується і не займає вікно консолі, тож ви спокійно можете продовжувати роботу.

Виглядає UI просто чудово, чи не так?

Devspace UI

Тепер декілька слів власне про Devspace та його можливості.

Головна й основна функціональність такого застосунку саме в можливості синхронізації файлів між локальною системою та pod’ами. Це допомагає розробникам тримати свої робочі станції у більш «чистому» вигляді, не захаращуючи їх усілякими пакетами та модулями для розробки.

Мені дуже сподобалася фраза, яку розробники Devspace використали у документації про синхронізацію файлів:

THINK kubectl cp ON STEROIDS

You can think of the file sync in DevSpace as a much faster and smarter version of kubectl cp that continuously copies files whenever they change.

Чудово, продукт для побудови локального середовища  обрали. Йдемо далі.

Підготовка локального оточення розробки

Тепер поговоримо трохи про наш поточний стек. Він не великий і не маленький, охоплює такі сервіси:

  • Nginx;
  • Frontend — NextJs;
  • Backend for Frontend — PHP + roadRunner;
  • Backend for CMS — PHP + roadRunner;
  • CMS — React;
  • MySQL;
  • MongoDB;
  • Elasticsearch;
  • Redis;
  • Memcached;
  • Cron jobs;
  • Workers.

Нижче навів схему того, як це все виглядає та функціонує разом:

Загальна схема локального оточення розробника

Тепер пробіжимося тим, як це все зібрати до купи так, щоб голова не боліла потім) Поїхали.

Для підготовки локального оточення, нам необхідні декілька інструментів.

Wrapper.sh —  bash-скрипт, що буде виконувати роль init-скрипту для інсталювання усіх залежностей. Він досить простий та містить перевірку наявності потрібних пакетів у системі. Так розробник не витрачає час на встановлення усіх необхідних компонентів для запуску системи — скрипт все виконає за нього (хіба що каву не зробить, хоча звісно, можна і тут заморочитися 🙂).

Які компоненти встановлює цей скрипт:

  • Devspace;
  • Docker;
  • k3d;
  • dnsmasq;
  • mkcert та nss (nss — потрібен для додавання CA до Firefox);
  • kubectl;
  • Helm;
  • AWS CLI;
  • створює k3d кластер;
  • перевіряє наявність namespace у k3d;
  • перевіряє наявність створеного secret у k3d для доступу до AWS Registry.

Думаю, що особливу увагу тут привертають два цікаві застосунки  —  це Docker та k3d. Тому приділю трішечки уваги цій парі.

На Docker та особливостях його роботи на macOS зупинятися не будемо.

Хоча нещодавно зробив для себе невеличке відкриття, що, починаючи з версії macOS 10.10 Yosemite і вище, Docker під капотом почав використовувати HyperKit замість Virtual Box. Своєю чергою, Hyperkit  —  це «легке» рішення віртуалізації macOS, що побудоване на Hypervisor.framework.

Хочу ще зазначити, що ми тестували також Rancher Desktop. Він цікавий, але трохи важкуватий як для локальної розробки та прожерливий до ресурсів (це якщо брати до уваги існування на робочій станції ще й Docker, хоча є можливість його замінити, наприклад, на nerdctl).

Принцип роботи Rancher Desktop заснований на піднятті віртуальної машини (Lima), що трохи додає навантаження на систему та збільшує час старту самого кластеру десь до двох хвилин.

Також, якщо дивитися на системний дашборд, що пропонує Rancher Desktop, він часом дає лаг та інколи не показує актуальний стан деплойменту. Натомість показує поди, які вже були видалені (це «лікувалося» переходом на іншу сторінку дашборду). Здається, що дрібниця, але дуже бісило.

Тепер пропоную зупинитися трохи детальніше на k3d, він заслуговує на увагу. Як каже нам перша домашня сторінка проєкту k3d: «k3d is a lightweight wrapper to run k3s (Rancher Lab’s minimal Kubernetes distribution) in docker. k3d makes it very easy to create single- and multi-node k3s clusters in docker, e.g. for local development on Kubernetes».

Що точно потрібно знати про k3d, так це те, що під капотом він запускає k3s, але на відміну від k3s, який потребує підняття віртуальної машини Lima, k3d використовує Docker, для запуску контейнера з k3s. Отже, від k3d ми плавно та планово перейшли до k3s 🙂

K3s  —  це проєкт від Rancher Desktop команди SUSE і, відповідно до опису, дуже «легкий», але при цьому дуже надійний дистрибутив K8s та розрахований на розміщення навіть в IoT-застосунках.

Архітектура, яка представлена нижче, описує, як функціонують k3s-сервер та агент окремо, але в нашій конфігурації  це все розташоване разом. Тобто агент — це та сама нода, на якій запущено процес k3s (на схемі ми просто опускаємо частину з «k3s Agent»).

Архітектура k3s з офіційного джерела

А найцікавіше, що для k3s немає жодних зовнішніх залежностей на кшталт up and running! Всередині файлу вже є все, необхідне для старту. Сам k3s  —  це архів, який розпаковується самостійно й містить усі необхідні бінарні файли для запуску K8s.

K3s сервер процес містить в собі такі компоненти:

  • K8s API, controller, scheduler  —  це базові компоненти майстер ноди K8s.
  • Sqlite замість etcd. Взагалі, хлопаки зробили цікаву річ: вони розробили Kine (Kine is not etcd), який дозволяє підключати різні бази зберігання даних. Це, по суті, бібліотечка, що забезпечує сумісність. По-науковому воно ще зветься shim.
Ось яке визначення shim дає нам «Вікіпедія»: «In computer programming, a shim is a library that transparently intercepts API calls and changes the arguments passed, handles the operation itself or redirects the operation elsewhere. Shims can be used to support an old API in a newer environment, or a new API in an older environment. Shims can also be used for running programs on different software platforms than they were developed for».
  • Containerd у якості CRI.
  • Flannel як мережева фабрика для контейнерів.

Загалом, це все, що потрібно знати про k3d. Звісно, можна копати ще глибше, але стаття не про це, тож поїхали далі.

Налаштування кластера за допомогою k3d займає магічних 20 секунд за допомогою команди:

k3d cluster create dev -v local_path:remote_path \
--k3s-arg "--disable=traefik" \
--no-lb \
--image rancher/k3s:v1.21.14-k3s1

Після чого ви одержуєте повністю готовий до роботи кластер K8s.

Вище ми визначилися, що k3d  —  це wrapper над k3s, тому, створюючи кластер, ми можемо передати аргументи для k3s. У нашому прикладі вимикаємо Traefik ingress controller, тому що будемо використовувати функціональність PortForwarding. До речі, робота з volume’s тут така ж, як і в Docker. Ми виконуємо прокидування volume з сертифікатами в pod Nginx. І, звісно, сертифікати знаходяться у файлі .gitignore.

У списку вище є ще декілька доволі цікавих інструментів, що полегшують роботу з дев-оточенням, як-от dnsmasq, mkcert.

Dnsmasq  —  дуже чудова річ, яка вирішує проблему правок /etc/hosts. Думаю, це знайомо всім. Отже, якщо на вашому проєкті безліч піддоменів і вам набридло постійно виконувати правки hosts-файлу, то вам однозначно варто спробувати цей інструмент. Він чудовий.

Конфігурація нескладна (лінк на прекрасну статтю, звідки й була взята конфігурація ):

brew install Dnsmasq
sudo cp $(brew list dnsmasq | grep /homebrew.mxcl.dnsmasq.plist$) /Library/LaunchDaemons/
sudo launchctl load /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist
echo "address=/dev/127.0.0.1" >> /usr/local/etc/dnsmasq.conf
sudo launchctl stop homebrew.mxcl.dnsmasq
sudo launchctl start homebrew.mxcl.dnsmasq
sudo mkdir -p /etc/resolver
sudo tee /etc/resolver/dev >/dev/null <<EOF
nameserver 127.0.0.1
EOF

Це все, тепер усі запити на домен та піддомени .dev будуть направлятися на 127.0.0.1.

Далі декілька слів про mkcert  —  застосунок, що усуває головний біль при роботі з локальними самопідписаними сертифікатами.

Встановлення виконується трьома рядками:

brew install mkcert
brew install nss
mkcert -install

Після цього можна випускати сертифікати:

mkcert example.dev "*.example.dev"

Тепер згенеровані сертифікати можна використовувати в локальному середовищі й ваш браузер буде їм довіряти без зайвих запитань. Чи не круто?!

Тут треба зауважити, що процес встановлення та випуску сертифікатів необхідно повторювати на кожній робочій станції розробників, а також використовувати сертифікати, що були згенеровані на конкретній робочій станції.

Тож, якщо ви раніше не використовували dnsmasq та mkcert ,  раджу вам додати їх до своєї улюбленої колекції застосунків 😉

Налаштування

Ось ми й добігли до основного застосунку нашого стека  —  Devspace. Ми встановили усі необхідні застосунки й тепер можна приступати до конфігурування нашої основної точки входу до локального оточення. Ось лінк на домашню сторінку Devspace.

До речі, документація в них досить непогана, а якщо щось все-таки не зрозуміло, завжди є варіант запитати в комʼюніті. Наприклад, у Slack-каналі. Усі посилання присутні на домашній сторінці проєкту.

Далі переходимо до кореня нашого проєкту й виконуємо devspace init  —  запуск викличе діалог, яким необхідно буде пройтися (він нескладний та зрозумілий, так що не викличе зайвих запитань). Після цього команда згенерує нам допоміжний файл devspace.yaml, в якому буде описана основна конфігурація.

Звичайно, helm chart вже має бути створений у репозиторії та готовий до деплойменту.

Основні моменти, які нас будуть цікавити  —  це секції deployment, dev і commands.

Отже, в секції deployment — описується сам деплоймент (дякую, кеп 😅), а також який namespace використовувати, де знаходиться helm chart, які файли зі змінними використовувати, або навіть можна перевизначити деякі змінні.

Це має ось такий вигляд:

deployments:
 example:
   namespace: dev
   helm:
     chart:
       name: ./devops/helm
     valuesFiles:
       - ./devops/helm/values.yaml

Далі йде основна секція для роботи із застосунками — секція dev, і має вона ось такий вигляд. Приклад показує, як ми хочемо взаємодіяти із контейнерами нашого деплойменту. Він описує роботу з трьома сервісами: cms, backend, Nginx.

dev:
 cms:
   labelSelector:
     app: spa
     project: example
     component: cms
   restartHelper:
     inject: true
   command: ["npm", "run", "start"]
   sync:
     - path: ./cms/:/app/
       initialSync: mirrorLocal
       startContainer: true
       excludePaths:
         - node_modules/
       onUpload:
         exec:
           - command: |-
               npm install --cache .npm --prefer-offline
               /tmp/devspacehelper restart
             onChange: ["./package.json"]
     - path: ./cms/node_modules/:/app/node_modules/
       initialSync: preferRemote
backend:
   labelSelector:
     app: php
     project: example
     component: backendcms
   restartHelper:
     inject: true
   command: ["php", "./apps/ui/cms/bin/artisan", "octane:start"]
   sync:
     - path: ./backend/:/app/
       initialSync: mirrorLocal
       startContainer: true
       excludePaths:
         - vendor/
       onUpload:
         exec:
           - command: |-
               php ./apps/ui/cms/bin/artisan octane:reload
             onChange: ["./apps/**/*", "./vendor/**/*"]
     - path: ./backend/vendor/:/app/vendor/
       initialSync: preferRemote
 nginx:
   labelSelector:
     app: nginx
     project: example
     component: webserver
   ports:
     - port: "8443:443"

Оператор LabelSelector  відповідає за те, як devspace обирає контейнер, до якого треба застосовувати описаний нижче стейт.

Оператор RestartHelper  — цікава річ. Він додається в контейнер та запускається як PID 1. Тобто виступає в ролі wrapper, що запускає наш основний процес, який описаний у command. Це потрібно, щоб hot reloading працював без перезавантаження самого контейнеру. Однак розробники Devspace зазначають, що якщо в застосунку є власний механізм hot reloading, то краще використовувати саме його.

Варто зазначити, що development-контейнери краще запускати з користувачем root, для коректної роботи RestartHelper. Наприклад, у контейнері з CMS підіймається webpack development server, який і займається функціональністю hot reloading сторінки за допомогою сервісу WebSocket. Тобто, devspace, в такому випадку, відповідає лише за синхронізацію файлів між локальною системою та цільовим pod’ом.

Sync  —  це секція, що відповідає за копіювання файлів до контейнера та у зворотному напрямку. Наприклад, у нас це має такий вигляд: джерело правди щодо коду початково знаходиться у розробника на робочій станції, тому при запуску контейнера ми копіюємо у нього код, а, наприклад, vendor або node_modules копіюється з контейнера до локальної файлової системи. За таку стратегію копіювання відповідає директива initialSync.

Варто зазначити, що при кожному пуші у майстер-гілку виконується збірка images на Jenkins, а при запуску деплойменту через devspace, завжди буде стягуватися новий image з репозиторію (AWS ECR). Ця стратегія зазначена у helm chart нашого проєкту.

Взагалі, стратегій копіювання досить багато. Їх можна будувати індивідуально під потреби проєкту.

Також є досить цікава директива onUpload. Вона визначає, що робити при зміні тих чи інших файлів. У нашому випадку (на прикладі backend) ми виконуємо перезавантаження процесу roadRunner при зміні файлів за визначеними шляхами  —  ./apps/**/* та ./vendor/**/*.

І остання директива в блоці dev  —  це ports. Виконує port forwarding вашого сервісу назовні. В нашому випадку, це проксування таких сервісів як Nginx, MySQL, MongoDB, Elasticsearch. Тобто, доступ до них виконується через localhost робочої станції.

Також ще один дуже цікавий блок  —  commands. Він має ось такий вигляд:

commands:
 cluster_stop:
   command: |-
     k3d cluster stop dev
   section: environment
   description: stopping k3d cluster
 cluster_start:
   command: |-
     k3d cluster start dev
   section: environment
   description: starting k3d cluster
 get_pods:
   command: |-
     kubectl get pods
   section: development
   description: getting all pods

У цьому блоці можна описувати будь-які команди, які потрібні вам для роботи. Це має вигляд, схожий на Makefile. А ось такий вигляд має вивід у консолі:

Так виглядає хелп по командах Devspace

Тепер саме час запустити команду devspace dev, яка не тільки виконає деплой у кластер за допомогою Helm, а також виведе інформацію щодо піднятих pod’ів та буде стежити за всіма оновленнями файлів робочого каталогу.

Висновки

Мета цього всього — створити локальне середовище, рішення для якого буде потребувати мінімум зусиль та часу команди розробників, і не тільки розробників. Готове рішення дозволяє вже на ранніх етапах підключати до процесу розробки комплексний процес QA-тестування.

Також варто зазначити, що такий продукт уможливлює розробку з побудовою будь-яких віддалених середовищ, де можливо розгорнути K8s кластер, що переводить локальну розробку від застарілого vagrant (боже мій, його ще використовують, вибачте, але це якийсь волохатий мамонт 😐) та docker-compose на новий рівень, дозволяючи повністю абстрагуватися від можливостей та ресурсів робочої станції розробника.

Чи не круто?!

Зі всіма академічними висновками закінчили, а тепер трохи поговоримо за плюси та мінуси. Було б усе добре, якби було усе добре 🙂 Далі прикріплю відгуки деяких учасників команди про систему.

Олексій Х., Backend-розробник

Плюси:

  • у системі, як і в Docker Compose, за допомогою однієї команди підіймається усе необхідне оточення;
  • є вбудований hot reloading, на який можливо налаштувати тригери;
  • ближче до production;
  • зручний devspace UI, в якому можна переглядати логи та запускати команди;
  • можливо додавати глобальні команди по типу Makefile.

Мінуси:

  • відчутно довший запуск порівняно з docker-compose (бо там ми прокидаємо volume, а у devspace виконується синхронізація файлів);
  • трохи відчуваються затримки у швидкості запитів.

Рекомендації для розробників:

  • якщо раніше не працювали з K8s, devspace, etc., готуйтеся більше звертатися за допомогою. Можливо, буде не зрозуміло, якщо щось піде не так або перестане працювати;
  • доведеться розібратись у примітивах K8s, таких як configmaps, pod, deployment, service, etc;
  • синхронізація через копіювання файлів змусить вас частіше зберігати код. Наприклад, під час написання тестів ви будете спочатку зберігати через CTRL + S, а тільки через секунду запускати тест.

Олексій П., Frontend-розробник

Плюси:

  • не потрібно думати про налаштування локального оточення — просто devspace run install / devspace run dev;
  • достатньо швидка синхронізація файлів з хоста в контейнер;
  • працює hot reloading, повільніше, аніж локально, але теж достатньо швидко;
  • швидке оновлення node_modules при встановленні нового пакету.

Мінуси:

  • підвищений поріг входу точно є; хто не «шарить» у контейнерах, не буде розуміти, «куди бігти», якщо щось не буде працювати;
  • можливо, коли кодова база буде більшою, то hot reloading буде трохи довше «думати».

Отже, якщо вже фінально підсумовувати, то, звісно, ідеальних рішень не існує — у кожної системи є свої плюси й мінуси. Однак пропоноване рішення дозволяє трішечки більше наблизити розробників до production оточення, навчити їх менше боятися та більше експериментувати. Тож, експериментуйте! Enjoy 😉

👍ПодобаєтьсяСподобалось3
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

якщо розробка буде проводитись на локальному ноутбуці розробника, краще використати Docker Desktop що значно спрощує схему вище.
DevSpace було б корисніше використовувати для розгортання оточень не локально, а на remote. А так, який сенс робити sync, якщо файли і так знаходиться локально і можуть бути mounted в k8s pods

Дякую, трішки змінив попередній коментар. Так як спочатку подумав що sync робиться на віддалені оточення.

Ага, зрозумів
ну суть була у тому щоб проект могли використовувати не лише розробники (а, наприклад, QA — по типу up and running), а для цього необхідно мати повністю зібрані імеджі для запуску, тому ось така схема.
Але загалом нічого не заважає тримати і віддалене оточення

Локальные среды разработки — зло. Давно придумали CI/CD.

CI/CD затримує фідбек від коду. До того як робити коміти девелопер повинен впевнитись що його код працює.

Сбилдить образ и запустить в нем тест — это не ракетостроение.

Локальные среды разработки — зло. Давно придумали CI/CD.

Второе не исключает первое, а дополняет.
У меня тут был проектик с turn-around результата — около 1.5 часов. Без дебага какого либо, т.к. локально проект не собирался и фиксить это отказались. А все из-за одного умника, придумавшего компилить только на CI ;)

Что помешало подрубить свой комп как раннер, запустить билд и полазить чтоб выявить багу?

Это так же неудобно как и раннер в облаке
Сборка должна воспроизводиться локально

Ну если должна — вопросов нет, пихай все в докер.

Підписатись на коментарі