Путь айтишника
129 subscribers
865 photos
1 file
482 links
Путь становления айтишника глазами практикующего специалиста. Личный опыт, полезные инструменты, разбор технологий, план развития и взгляд на индустрию изнутри.
Download Telegram
Продолжаю заниматься сервером телефонии Asteriks.
Утром вошёл на сервер и обнаружил тупой брутфорс: перебор номеров телефонов и паролей. Явно кто-то за наш счёт хочет позвонить в Никарагуа. Начал копать в сторону fail2ban. Fail2ban — это система для защиты от брутфорс-атак, который сканирует системные логи и автоматически блокирует IP-адреса с подозрительной активностью через правила брандмауэра. Оказалось, она просто не запущена. Посмотрел конфиги, кое-что поправил и запустил. Брутфорс прекратился. От полноценного DDOS fail2ban вряд ли спасёт, но от тупых китайских ботов вполне.

Затем было погружение в дивный мир монтирования NFS. NFS — это протокол сетевой файловой системы, позволяющий клиентам монтировать и работать с удалёнными директориями на сервере так, как если бы они были локальными.
В чём проблема: отвалился важный скрипт обработки записи телефонных разговоров. А в логах пишется дивная ошибка: «Не удалось прочитать файл /home/development/projects/modules/submodules/experimental/features/audio/processing/pipelines/abc/2023/05/10/output.wav из сетевого диска /mnt/media». Я весь сервер облазил, нет ни такого пути, ни такого файла, и папка, откуда /mnt/media, пустая. Пообщался с коллегой. Файл «output.wav» нашли на другом сервере. Путём долгих рассуждений выяснил, что всё, что в пути файла до «abc», отбрасывается, и скрипт ищет файл по следующему пути /mnt/media/2023/05/10/output.wav. Но папка /mnt/media была пустой. Файл fstab тоже не содержал записи об NFS. Поэтому через NFS примонтировали к ней корень пути /2023/05/10/ и прописал эту запись в файл fstab. Скрипт заработал.
Начал с коллегой думать, в чём корень проблемы. Я где-то неделю назад я вместо перезагрузки Asteriks перезагрузил сам сервер. А перезагрузка сервера в нашей компании — это явление экстраординарное, т. к. чаще всего проблему на Linux-серверах это не решает. Поэтому сервера работают годами без перезагрузки. Но я привык перегружать сервера на Windows, там хватает утечек памяти. Думаю, дело было так: кто-то давным-давно примонтировал сетевую шару вручную через NFS, а вот в файле fstab прописать её забыл. После перезагрузки она автоматом не смонтировалась, и работа скрипта поломалась. Пару седых волос я на этом случае заработал.
Продолжаются мои приключения с ZFS. ZFS — это современная файловая система, обеспечивающий пулы хранения, copy-on-write, мгновенные снапшоты и сквозную проверку целостности данных. Передо мной стоит задача сделать резервную копию сервера телефонии.
Напомню, он работает под FreeBSD на ZFS. Меня не покидает ощущение, что эта FreeBSD меня когда-нибудь убьёт. Приходится ломать через себя очень много привычек из мира Linux. И файлы конфигов в других местах, и управление службами по-другому. Всё выглядит также как Linux, но работает чуть-чуть по-иному, и вот это чуть-чуть сильно бесит, ожидаешь один результат, получаешь иной.
ZFS — система умная, в неё уже встроено резервное копирование на уровне снапшотов. И ничего придумывать не нужно, нужно только настроить другой сервер. Вот с этим и возникла проблема. ZFS она как бы есть, но там какая-то дивная чехарда с лицензиями, поэтому в официальную поставку Linux-дистрибутивов её не включают, и надо ставить вручную. FreeBSD убивает не только меня, но и коллег. Поэтому решили мягко мигрировать на Debian. Но просто так на Debian ZFS не установить. Но есть хорошая инструкция «Debian Bookworm Root on ZFS» https://openzfs.github.io/openzfs-docs/Getting%20Started/Debian/Debian%20Bookworm%20Root%20on%20ZFS.html. И как же это сделать? Если вкратце, надо загрузится с LiveCD, разметить диск, установить загрузчик, скопировать операционку и провести её настройки из chroot. Я шёл по этой инструкции, но периодически пропускал важные команды. Но с 3-го раза у меня получилось! Теперь буду настраивать получение ZFS-снапшотов с удалённого сервера телефонии по расписанию. А также буду пробовать восстановить полученной снапшот на виртуалку со FreeBSD. Кто хочет понять, как работает ZFS изнутри, рекомендую видео «Моя лекция о COW-файловых системах (ZFS, BTRFS)» https://www.youtube.com/watch?v=z9G4oD5YPKw. После просмотра у меня отпали вопросы, как ZFS архивирует данные, проверяет их целостность, как борется с дублями данных, как у ней организован RAID и снапшоты. И я понял, разработка серьёзной файловой системы — очень масштабная задача.

Я продолжаю углубляться в администрирование серверов телефонии на базе Asterisk, сталкиваясь с типичными, но поучительными проблемами: брутфорс, неработающие NFS-смонтированные каталоги, нестандартное поведение FreeBSD и настройка ZFS. Каждый инцидент — это опыт, который делает систему надёжнее, а меня — профессиональнее. Главное — не сдаваться и досконально разбираться в происходящем.
👍3
Всю неделю продолжал делать резервную копию сервера телефонии через ZFS.
Ещё раз прошерстил сервер и решил посмотреть, что занимает больше всего места. Оказалось, логи, они не ротируются от слова «совсем». За пару лет их размер стал несколько гигабайт.
Во FreeBSD ротация логов осуществляется с помощью демона newsyslog, который управляется конфигурацией в /etc/newsyslog.conf. Он проверяется по cron, и ротация выполняется на основе размера файла, возраста и количества копий. Для корректного завершения работы с логом программе обычно отправляется сигнал USR1 — он инициирует переоткрытие файлов журналов без перезапуска процесса.
Однако после получения сигнала USR1 сервер телефонии Asterisk умирает. А должен просто начать запись в новый файл логов. Сколько ни бился, решения этой проблемы я не нашёл, поэтому в cron по расписанию каждую ночь перезапускаю сервер телефонии.
Затем мне поставили задачу, что когда клиент звонит из города или клиенту в город, должно звучать сообщение, что «разговор записывается». Столкнулся с двумя проблемами: кое-где сообщение звучит, а разговор не записывает, кое-где разговор записывается, а сообщение звучит. Поставил проигрывать сообщение везде, где ведётся запись. Оказалось, что внутренние переговоры хоть и записываются, но это сообщение выводить не нужно. Пользователи пугаются.
В Asterisk есть базовое понятие Dialplan — это сценарий, описывающий, как обрабатываются входящие и исходящие звонки. Он реализован на Asterisk Extension Language (AEL). Это сильно упрощенный язык программирования, предназначенный для описания логики обработки вызовов. Так вот, там есть оператор GoTo. Какое фундаментальное правило при работе с GoTo? Нельзя «прыгать» выше по коду и в другие файлы. Это приводит к «лапша»-коду. Те, кто писал Dialplan до меня, про это правило не слышали. Я, пока выяснял, почему не идёт запись входящих городских звонков, через лог нашёл 5 переходов goto в 3 разных файлах. Горите в аду бывшие сисадмины этой АТС. Но я отследил только один входящий маршрут. Угадайте, сколько их всего? 26. Я решил автоматизировать обработку Dialplan, иначе я «закопаюсь».
Но случилось великое событие, я клонировал сервер телефонии через ZFS. Перенёс с «bare metal» на виртуалку. Передачу снапшотов сделал через zrepl https://zrepl.github.io/. А само клонирование реализовал по видео «How to move FreeBSD to another machine (ZFS)» https://www.youtube.com/watch?v=SP7kWg2KHm0. Я напишу 2 больших статьи. Первая — как передать снапшоты на другой сервер при помощи zrepl, вторая — как перенести снапшот на виртуалку со FreeBSD. Будет с чем выступить на конференции.
За время работы я понял, как важно наводить порядок и автоматизировать всё, что можно. Логи не ротировались, Dialplan запутан, сервер падает от простого сигнала — всё это мешает. Теперь всё работает стабильнее, и у меня собрался хороший материал для двух понятных и полезных статей.
👍6
Неделя выдалась какая-то странная и вся в телефонии. Дали задачу выводить сообщение, что «ваш разговор записывается». Но у нас в компании, оказывается, около 70 соединений (транков) с операторами связи, а на транке может быть до 26 номеров.
Я решил отследить, почему один разговор не записался. Оказалось, сообщение производится, а сама запись не ведётся. Анализировал около 3-х часов. Ощущение, код для мини-АТС писала «пьяная шимпанзе» хвостом. 5 операторов GoTo по разным файлам. Нашёл ошибку, но это только один транк и один номер. Я понял, что погибну.
Начал писать проект для синтаксического разбора логов https://github.com/orion55/dial-scan, чтобы он проанализировал конфигурационные файлы Asterisk и проверил, достижима ли функция записи. Т. е. вместо меня прошёл по всем GoTo. Просто длина одного файла около 12 тыс. строк кода. Вручную это сделать нереально.
В качестве хранения решил использовать графовую базу данных [RedisGraph](https://github.com/RedisGraph/RedisGraph). Т. е. важны именно связи, а не данные.
Начать писать начал, но не закончил, т. к. файлы конфигов «битые», синтаксически ошибочные. Asterisk выдаёт уйму предупреждений. Он, конечно, их обрабатывает, но для меня пока это задача со звёздочкой.
В результате, раз «снайперская винтовка» с точечным поиском и анализом файлов не сработала, использовал «пулемёт» — сделал глобальную замену в файлах. Использовал sed, добавил дополнительный параметр к существующей процедуре и стал анализировать длину входящих и исходящих номеров. По факту написал ещё один «костыль». Вот хорошее видео по sed [Linux by Rebrain: Работа в sed](https://www.youtube.com/watch?v=mcT4Y311eWU).
Была задача некорректную работу голосового меню. Кто-то поставил уйму задержек, которые не нужны, и пользователь большую часть времени слушал тишину, точнее, не слушал, а бросал трубку. Я поставил фоновую музыку и ускорил обработку вызова. Опять код писала «пьяная шимпанзе». На самом деле, это коллективное творчество, и, как я понял, я уже 5-й администратор телефонии в этой организации. Каждый программировал мини-АТС в меру своих знаний и умений. Результат получился, мягко говоря, не очень.
Всё это окончательно убедило меня в одном: если мини-АТС на протяжении лет программировали пять разных людей, не договариваясь друг с другом, то результат будет соответствующий — словно код писали с закрытыми глазами, да ещё и по очереди. Каждый внёс что-то своё, и теперь вся логика диалплана напоминает археологический раскоп — слой за слоем, от костылей до магии. Попытка навести порядок вручную — как чинить реактивный двигатель с отвёрткой. Поэтому я и начал писать свой инструмент, чтобы парсить, строить связи и проверять логику вызовов автоматически, а не методом шаманства. sed помог, но это как лечить перелом пластырем. Пора переходить от ремесла к инженерии — с валидацией, тестами и графовым анализом вместо угадайки и надежды, что всё сработает. Иначе — так и будем по кругу чинить то, что должно было работать с самого начала.
👍5
Написал большую статью «Когда просто бэкап — это скучно, ZFS, Asterisk и немного магии» https://infoblog72.ru/blog/kogda-prosto-bekap-eto-skuchno-zfs-asterisk-i-nemnogo-magii.html Она для тех, кто работает с инфраструктурой и отвечает за живучесть сервисов. Рассказываю, как организовал резервное копирование Asterisk-сервера на FreeBSD через ZFS снапшоты, автоматизировал репликацию на удалённый Debian-сервер с помощью zrepl и какие подводные камни возникли в процессе.

В статье описываю реальную рабочую конфигурацию, подход к проектированию, поясняю, почему выбран pull-метод, и делюсь всеми ключевыми командами. Никакой теории ради теории — только практика и конкретика.

Статья для тех, кто хочет не просто бэкапить, а строить надёжную систему восстановления.
👍2
Channel name was changed to «Путь айтишника»
Продолжаю разбираться с Asterisk. Была интересная задача — необходимо было на одном из номеров выводить женским голосом сообщение: «Данная линия не обслуживается». Искать девушку и записывать её голос я не стал, а воспользовался сервисом Yandex SpeechKit. Он озвучил это сообщение женским голосом. Затем я преобразовал полученный аудиофайл в нужный формат, понятный Asterisk (ffmpeg -i input.wav -ar 8000 -ac 1 -c:a pcm_s16le output.wav), и задача была выполнена.
Ещё в нашей фирме есть мобильное приложение, позволяющее совершать звонки. Мне потребовалось выяснить, через какой транк осуществляется звонок. Приложение разработано на Yii. Я решил скачать исходники, зайти на production-сервер и создать архив проекта. Архив создавался примерно полчаса, разросся до размера около 70 Гб, заполнив всё доступное пространство, и завершился ошибкой. Вместе с архиватором отказал и сам production-сервер. Так я случайно «уронил» production, правда всего лишь на десять минут. Выяснилось, что помимо исходников программы я пытался скопировать ещё и каталог загрузок («upload»). Само приложение я потом успешно скачал через GitLab, и оно заняло около одного гигабайта.
Процесс совершения звонков из мобильного приложения выглядит так: PHP-код подключается по SSH к серверу Asterisk и запускает Bash-скрипт с параметрами абонента и направления звонка. Скрипт формирует текстовый файл-заготовку звонка, который затем обрабатывается Asterisk. Однако почему бы не воспользоваться API самого Asterisk, позволившим бы совершить звонок простым HTTP-запросом типа POST? Судя по всему, разработчики на PHP были незнакомы с таким решением. Причины выбора именно этого метода остаются для меня загадкой.
К слову, заметил любопытное сходство между фреймворком Yii и такими технологиями, как NestJS, ASP.NET Core. Здесь прекрасно проявляется принцип объектно-ориентированного программирования, классы, объекты, интерфейсы.
Из этого опыта я вынес несколько выводов. Во-первых, даже простая задача — вроде озвучивания фразы — требует знания инструментов, которые сильно экономят время. Во-вторых, любые действия на production нужно тщательно обдумать, даже если речь идёт об простой архивации. И наконец, архитектура решений, вроде звонков из приложения, должна опираться на здравый смысл и знание возможностей платформы.
В блоге вышла новая статья «Как восстановить Asterisk из снапшота — пошаговая инструкция» https://infoblog72.ru/blog/kak-vosstanovit-asterisk-iz-snapshota-poshagovaya-instrukcziya.html. В ней я рассказываю, как поднять виртуальную копию сервера телефонии на базе FreeBSD 13.1, начиная с загрузки LiveCD и настройки русской раскладки, заканчивая передачей ZFS-снапшота по SSH и активацией загрузочного датасета. Всё описано строго по шагам — без воды и с реальными командами.

Разбираю ключевые действия: создание временного /etc, запуск SSH, получение IP через DHCP, подготовку диска, создание пула zfspool, выбор нужного снапшота и восстановление через zfs send | ssh zfs receive. После этого сервер практически готов к запуску, остаётся лишь отключить ISO и включить машину.

Если вы работаете с Asterisk и используете ZFS, обязательно прочитайте статью. Это реальный опыт восстановления, который можно повторить в условиях продакшена — без лишних рисков и с контролем на каждом этапе.
Продолжаю изучать FreeBSD. Выяснил, что эта операционная система наполнена магией. Система magic распознает тип файлов, Daemon — это служба, ELF — тип файла, а Wizard — это волшебник.

Смотрю курс по FreeBSD, и там тренер рассказал анекдот: «Однажды Ланселоту пришло письмо от короля Артура с заданием: «Ты должен пройти Испытание Рыцаря-администратора. Ответь на этот вопрос: Какие 4 заглавные буквы не являются правильными ключами утилиты ls в FreeBSD?» Ланселот думал долго, но так ничего и не вспомнил. Тогда он написал в ответ: «Не знаю, но точно не -HELP».

Почему это смешно (для тех, кто не в теме)? В системах Unix-like, включая FreeBSD, утилиты, как правило, не имеют опции -HELP. Вместо этого используется man ls, --help, или можно просто попробовать запустить команду без аргументов. Настоящий юниксовый гуру знает, что помощи от самой программы ждать не стоит — только от man или info.

Узнал про vimtutor.ru — это встроенный в систему учебник по vim. А также, что Docker во FreeBSD нет. Есть Jails — родные BSD-контейнеры, но вот Docker нет.

Пока система выглядит, мягко говоря, странно. Есть 2 пути до конфигов: /etc — сама система и /usr/local/etc — конфиги установленных приложений. Все важные настройки хранятся в одном файле /etc/rc.conf — демоны, сеть и файрволл. Поэтому он может принимать огромные размеры. В общем, система, мягко говоря, «странная», если нет большой необходимости переходить, не рекомендую.

Продолжаю изучать Asterisk. Установил на виртуалке FreePBX. Это такой веб-интерфейс над Asterisk. Устанавливал вот по этому гайду: «FreePBX 16 on Debian 12 with Asterisk 18» https://www.youtube.com/watch?v=LRghtpGsYTg. Как было правильно сказано, FreePBX для автоматизации самых простых операций, чтобы реализовать что-то более-менее посложнее, надо писать самому.

Узнал, что существует Kamailio https://www.kamailio.org/w — SIP-прокси для Asterisk. Кто-то может возразить, но есть же haproxy, он прекрасно всё проксирует. Да, прекрасно, но с обрывом разговора, а Kamailio — без обрыва.

Мой вывод такой. Во FreeBSD здесь всё делается руками, осознанно и с пониманием. Это не «поставил и заработало», а «разобрался — и теперь работает так, как нужно именно тебе». Такой подход требует времени, но зато даёт полную прозрачность и контроль.
❤2
Продолжаю изучать FreeBSD. Смотрю учебный курс по FreeBSD 2011 года. Прошло 14 лет, а FreeBSD не поменялась от слова «совсем». Есть изменения, но они не радикальные. Если брать Linux даже 5-летней давности, разница сильно видна, а здесь как в болоте — всё тихо и спокойно. Не знаю, в чём причина, возможно, FreeBSD живёт на грантах, а Linux на сообществе. Но выглядит это печально.

На работе получил задание наклеить стикеры с номерами на сетевые розетки и патч-панель, вспомнил молодость, когда бегал с перфоратором и тянул витую пару. Сильно устал, физподготовка никакая. Заодно выяснил, часть розеток на патч-панели расшита как попало, часть вообще на плане не отражена, они «лишние». Как монтажники делали работу и как у них её принимали — вопрос открытый. Но задачу закрыл.

Продолжил изучать Kamailio — SIP-прокси сервер. По этому списку статей https://blog.ipcalls24.com/sip-register-in-kamailio/. Я всё думал, почему Kamailio такой мощный, а потому что-то низкоуровневый. Там все методы SIP-протокола реализовать самому, а это большой кусок АТС. А во время реализации можно вмешаться и настроить так, как удобно. Kamailio — это мощно, но сложно.

Решил сделать лабораторку из плейлиста «Построение IT-инфраструктуры» https://www.youtube.com/playlist?list=PL148XRxlt8SjX804Fkvn5N5KR9IeaGEz_ от «Сисадмин Михалыч». Автор прекрасно знает матчасть и даёт реальный кейс построения серверной части для небольшой компании. Он даже контроллер домена поднимает на SAMBA под Linux, как и DNS- и DHCP-сервера. Но вот инструменты для управления использует от Windows. То, что реализовано под Linux, смотреть без слёз невозможно, автор даже сам нужный софт. Притом, что всю инфраструктуру он реализует на AltLinux https://www.basealt.ru/. Я выполнял за ним, и AltLinux от российских разработчиков мне понравился. По крайней мере, управление сетью достаточно логичное. Что не понравилось, на рабочей станции в оболочке Gnome по умолчанию кнопки заголовка окна «Свернуть» и «Развернуть» отключены, надо включать вручную. Первичный контроллер домена на SAMBA я пробовал поднять лет 10 назад, но не получилось. OpenSource — это, конечно, здорово, но тогда проект SAMBA делали 2 человека на голом энтузиазме. Потом пришёл MS и стал «дружить» с Linux, а до этого пытался его «убить». Сейчас создание домена прошло с полпинка. «Сисадмин Михалыч» настроил выход локальной сети через роутер, а как мне пройти этот роутер и достучаться до машины за роутером? Помогла команда ssh -J root@192.168.1.144 root@10.10.10.10. Я эту команду ещё ни разу не использовал. Чисто моё мнение, контроллер домена для компьютеров с Windows лучше поднимать на Windows, т. к. разработка на Linux всегда будет отстающей. А как лабораторка этот плейлист очень неплоха.

Продолжая погружаться в администрирование, понимаю: решения с открытым исходным кодом дают гибкость, но требуют опыта и терпения. Однако в боевых условиях критичные сервисы, такие как контроллер домена для Windows-клиентов, я бы доверил самой Windows. А всё остальное — отличное поле для экспериментов и роста.
👍4
Продолжаю разбираться с Asterisk. Попробовал на работе обновить FreeBSD с 13.1 до 13.5. Казалось бы, минорное обновление — что тут может пойти не так? Но хорошо, что начал экспериментировать на виртуальной машине, а не на реальной АТС. Перед началом обновления я посмотрел видео, где один человек рассказывал, как он обновился с версии 13.1 до 14.2 по следующему алгоритму: сначала 13.1 → 13.5, 13.5 → 14.0, потом 14.0 → 14.2. Мне понравился его совет: если хотите обновляться до мажорной версии, начинайте в пятницу вечером, поскольку понедельник утром придут пользователи и за два дня можно успеть обновиться. Но ведь у меня минорное обновление.

Когда я начал обновляться, обнаружил, что предыдущий администратор установил систему с версии 12.2 до 13.1 путём ручной сборки из исходников. Для перехода на новую версию подключил возможность установки пакетов через бинарники. После нескольких попыток наконец обновил ядро. Затем приступил к обновлению встроенных утилит в FreeBSD и столкнулся с неприятностью: выяснилось, что Asterisk 16 несовместим с новейшими версиями библиотек, поэтому система предлагала удалить его целиком. Решив сохранить Asterisk, заблокировал его удаление, однако даже после завершения обновления заметил странности: Asterisk стал вести себя нестабильно, перестали вестись логи, а информация о звонках перестала записываться в базу данных. Оценив ситуацию, я принял решение временно остаться на старой версии FreeBSD 13.1.

Прочитал серию статей про SIP-прокси Kamailio (https://blog.ipcalls24.com/). Стало ясно, как организовать балансировку между двумя серверами Asterisk. Всё основывается на глубоком понимании протокола SIP, который довольно сложен.

Неожиданно перестали проходить звонки через мини-АТС после переезда на новую версию API. Перерыли всю документацию по АТС — всё нормально. Оказалось, проблема была в Docker-контейнере: в исходных кодах были жёстко прописаны пути («hardcoded»). Создал задачу программистам на создание .env-файлов для различных сред.

На FreeBSD перешел с csh на bash — стало гораздо удобнее. Csh — настоящее испытание для администратора: не работает клавиша «Delete» в командной строке, внутренний язык программирования крайне специфичен. Bash значительно удобнее. Во время перехода обнаружил, что кто-то из предыдущих администраторов установил fish (https://fishshell.com/), этот шелл оказался ещё более необычным.

Во FreeBSD отсутствует поддержка Docker, но есть jails (https://docs.freebsd.org/en/books/handbook/jails/) — нечто вроде контейнеризации собственной разработки системы. Прочитав руководство (handbook), понял, насколько низкоуровневыми бывают операции с jails: начиная от выделения тома на ZFS до ручного копирования конфигурационных файлов. Коллеги подтвердили, что большинство операций выполняют через специальную систему управления jails — CBSD (https://www.bsdstore.ru/en/docs.html).

Сейчас складывается впечатление, будто FreeBSD специально создана для мучений системного администратора. Кому-то нравится эта операционная система, но я не отношусь к таким людям. Постоянно возникают проблемы, о существовании которых раньше и не подозревал, например, сложности с csh.

Купил книгу Брюса Э. Тейта «Семь языков за семь недель». Автор рассказывает о семи языках программирования, основанных на разных парадигмах. Каждому языку посвящена одна неделя.

Первым языком является Ruby (https://www.ruby-lang.org/ru/). Сам автор активно программирует на нём и восхваляет его достоинства: композиции вместо интерфейсов, динамическую типизацию вместо статической. Субъективно Ruby привлек внимание благодаря фреймворку Ruby on Rails (https://rubyonrails.org/), который был революционным в своё время, хотя теперь многие его идеи уже повторили. Лично мне динамическая типизация не нравится: в функцию может попасть абсолютно любой объект — будь то число, строка или функция, массив и прочее. Это приходится учитывать и обрабатывать отдельно. Интерпретатор справляется лишь с простыми случаями.

Следующая глава посвящена языку Io (https://iolanguage.org) — языку, основанному исключительно на прототипах.
На досуге составил себе дорожную карту профессионального роста (см. файл). Взглянул на неё и немного расстроился: путь до квалифицированного специалиста предстоит долгий.

Ну вот и опять набрался опыта! Проблемы, конечно, были, но именно такие ситуации делают нас сильнее и умнее.
❤5👍1