<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: RNDSOFT</title>
    <description>The latest articles on DEV Community by RNDSOFT (rnds).</description>
    <link>https://dev.to/rnds</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png</url>
      <title>DEV Community: RNDSOFT</title>
      <link>https://dev.to/rnds</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rnds"/>
    <language>en</language>
    <item>
      <title>SQLite в Docker: жить хорошо, а хорошо жить — ещё лучше (Litestream)</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Wed, 29 Jul 2026 13:44:57 +0000</pubDate>
      <link>https://dev.to/rnds/sqlite-v-docker-zhit-khorosho-a-khorosho-zhit-ieshchio-luchshie-litestream-23ei</link>
      <guid>https://dev.to/rnds/sqlite-v-docker-zhit-khorosho-a-khorosho-zhit-ieshchio-luchshie-litestream-23ei</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkdgcxeidj75mrvj5vzk2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkdgcxeidj75mrvj5vzk2.png" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Иногда хочется, чтобы всё было просто. Взял Rails-приложение, завернул в Docker, запустил — и оно работает. Без отдельной СУБД, без монтирования хранилища для картинок и пр.&lt;/p&gt;

&lt;p&gt;Но вот незадача: если взять &lt;code&gt;SQLite&lt;/code&gt;, положить её в контейнер и запустить — при &lt;code&gt;docker rm&lt;/code&gt; ваша база улетает в никуда. Навсегда. Вместе с пользователями, настройками и всем, что вы так старательно назаполняли.&lt;/p&gt;

&lt;p&gt;Сегодня расскажу, как я в pet-проекте &lt;a href="https://runit.rnds.pro/" rel="noopener noreferrer"&gt;RuNiT&lt;/a&gt; решил эту проблему с помощью &lt;strong&gt;Litestream&lt;/strong&gt; — и просто кайфую.&lt;/p&gt;

&lt;h3&gt;
  
  
  Проблема
&lt;/h3&gt;

&lt;p&gt;Классический путь деплоя pet-проекта выглядит так:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Поднять &lt;code&gt;PostgreSQL&lt;/code&gt; в отдельном контейнере (найти под него постоянное хранилище и сервак)&lt;/li&gt;
&lt;li&gt;Прокинуть volume&lt;/li&gt;
&lt;li&gt;Понять, что вы тратите больше времени на администрирование СУБД, и хранилище картинок, чем на разработку фич&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Для pet-проекта это ощутимый оверхед. &lt;code&gt;SQLite&lt;/code&gt; решает первый пункт, но создаёт два других:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;файл базы живёт внутри контейнера и при пересоздании исчезает&lt;/li&gt;
&lt;li&gt;бэкапов нет, и если сервер упал — всё, приплыли.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Тут бы и сказать "ну, SQLite не для продакшена", но мне как-то попадался проект &lt;a href="https://litestream.io/" rel="noopener noreferrer"&gt;Litestream - Streaming SQLite Replication&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Litestream
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Litestream&lt;/code&gt; — это standalone-утилита (написана на Go, ~14k stars на &lt;a href="https://github.com/benbjohnson/litestream" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;) для потоковой репликации &lt;code&gt;SQLite&lt;/code&gt;. Она следит за WAL (Write-Ahead Log) вашей базы и в реальном времени отправляет изменения в S3-совместимое хранилище: &lt;code&gt;Yandex Object Storage&lt;/code&gt;, &lt;code&gt;MinIO&lt;/code&gt;, &lt;code&gt;RustFS&lt;/code&gt; и многие другие.&lt;/p&gt;

&lt;p&gt;Умеет:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Continuous backup&lt;/strong&gt; — каждая транзакция стримится в облако почти мгновенно&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restore при старте&lt;/strong&gt; — при пересоздании контейнера база автоматически восстанавливается из реплики&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Point-in-time recovery&lt;/strong&gt; — можно откатиться к состоянию на конкретную секунду.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ноль изменений в коде приложения&lt;/strong&gt; — &lt;code&gt;Litestream&lt;/code&gt; работает как отдельный процесс.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Дёшево&lt;/strong&gt; — object storage стоит копейки.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Звучит как магия? По сути — да. Но магия очень простая и надёжная - разработчик говорит что работает в проде стабильно.&lt;/p&gt;

&lt;h3&gt;
  
  
  Как запустить
&lt;/h3&gt;

&lt;p&gt;Тыкать надо тут следующее:&lt;/p&gt;

&lt;p&gt;Устанавливаем &lt;code&gt;Litestream&lt;/code&gt; прямо в образ:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;#Dockerfile&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-ex&lt;/span&gt;
  &amp;amp;&amp;amp; curl -sL https://github.com/benbjohnson/litestream/releases/download/v0.5.9/litestream-0.5.9-linux-x86_64.deb -o litestream.deb \
  &amp;amp;&amp;amp; dpkg -i litestream.deb \
  &amp;amp;&amp;amp; rm litestream.deb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Передаём credentials для S3 и URL реплики:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;LITESTREAM_ACCESS_KEY_ID=${LITESTREAM_ACCESS_KEY_ID-key}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;LITESTREAM_SECRET_ACCESS_KEY=${LITESTREAM_SECRET_ACCESS_KEY-secret}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;REMOTE_S3_FILE=${REMOTE_S3_FILE-s3://11.11.11.11:9000/}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Главный трюк — в &lt;code&gt;docker/start_service.sh&lt;/code&gt;, который является CMD в Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-ex&lt;/span&gt;

&lt;span class="c"&gt;# 1. Восстановить базу из S3, если она не существует локально&lt;/span&gt;
litestream restore
  &lt;span class="nt"&gt;-o&lt;/span&gt; storage/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.sqlite3
  &lt;span class="nt"&gt;-if-replica-exists&lt;/span&gt;
  &lt;span class="nt"&gt;-if-db-not-exists&lt;/span&gt;
  &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REMOTE_S3_FILE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.sqlite3

&lt;span class="c"&gt;# 2. Стандартные Rails шаги (idempotent — безопасны для существующей базы)&lt;/span&gt;
bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rails db:create
bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rails db:migrate
bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rails db:seed

&lt;span class="c"&gt;# 3. Запускаем Rails ПОД управлением Litestream&lt;/span&gt;
&lt;span class="nb"&gt;exec &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;litestream replicate
  &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="s2"&gt;"rails s"&lt;/span&gt;
  storage/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.sqlite3
  &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REMOTE_S3_FILE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.sqlite3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Что здесь происходит:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;litestream restore&lt;/strong&gt; — если локального файла &lt;code&gt;storage/production.sqlite3&lt;/code&gt; нет, а в S3 есть бэкап, база скачивается. Флаги &lt;code&gt;-if-replica-exists&lt;/code&gt; и &lt;code&gt;-if-db-not-exists&lt;/code&gt; делают команду безопасной: она не упадёт, если бэкапа ещё нет (первый деплой), и не перезапишет существующую базу если она есть&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;db:create db:migrate db:seed&lt;/strong&gt; — классика Rails&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;litestream replicate -exec "rails s"&lt;/strong&gt; — &lt;code&gt;Litestream&lt;/code&gt; запускается как supervisor. Он начинает реплицировать WAL в S3 и одновременно запускает дочерний процесс &lt;code&gt;rails s&lt;/code&gt;. Если Rails упадёт, &lt;code&gt;Litestream&lt;/code&gt; тоже завершится.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Результат: контейнер можно остановить, удалить, пересоздать на другом хосте — база восстановится из S3 и продолжит реплицироваться. Красота!&lt;/p&gt;

&lt;h3&gt;
  
  
  Профит и подводные камни
&lt;/h3&gt;

&lt;p&gt;Плюсы:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Минимальная инфраструктура — один контейнер, без отдельной СУБД&lt;/li&gt;
&lt;li&gt;Персистентность — данные не пропадают при удалении контейнера&lt;/li&gt;
&lt;li&gt;Бэкап "из коробки" — каждая транзакция летит в S3, RPO стремится к нулю&lt;/li&gt;
&lt;li&gt;Простое восстановление — &lt;code&gt;litestream restore&lt;/code&gt;или автовосстановление при старте&lt;/li&gt;
&lt;li&gt;Дёшево — &lt;code&gt;SQLite&lt;/code&gt; бесплатна, object storage стоит копейки&lt;/li&gt;
&lt;li&gt;Не требует изменений в коде приложения&lt;/li&gt;
&lt;li&gt;Point-in-time recovery — можно восстановить базу на конкретный момент времени&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Минусы:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Только один writer — &lt;code&gt;SQLite&lt;/code&gt; поддерживает только одного писателя. Это значит, что приложение должно работать в единственном экземпляре (single-node). Ни о каком горизонтальном масштабировании речи не идёт, но в списке ссылок будет сюрприз :)&lt;/li&gt;
&lt;li&gt;Зависимость от object storage — если S3 недоступен, &lt;code&gt;Litestream&lt;/code&gt; накапливает WAL локально, но при длительном отключении может потребоваться внимание&lt;/li&gt;
&lt;li&gt;Не замена PostgreSQL для высоконагруженных систем — для pet-проекта и небольших сервисов подходит идеально, для HA — нет, но можно посмотреть в ссылочки в конце статьи...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;SQLite&lt;/code&gt; + &lt;code&gt;Litestream&lt;/code&gt; — это мощная комбинация для «лёгкого» деплоя. Вы получаете полноценную реляционную базу данных в одном файле, непрерывный backup в S3 и автоматическое восстановление при перезапуске контейнера — без необходимости поднимать и обслуживать отдельную СУБД.&lt;/p&gt;

&lt;p&gt;Для pet-проекта это часто идеальный баланс между простотой и надёжностью.&lt;/p&gt;

&lt;p&gt;Пользуйтесь на здоровье! 🤝&lt;/p&gt;

&lt;h3&gt;
  
  
  Мои любимые ссылки, которые стоит почитать:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://litestream.io/" rel="noopener noreferrer"&gt;Litestream&lt;/a&gt; и &lt;a href="https://github.com/benbjohnson/litestream" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — официальный сайт и исходники&lt;/li&gt;
&lt;li&gt;Вот он сюрприз: &lt;a href="https://fly.io/docs/litefs/" rel="noopener noreferrer"&gt;LiteFS - Distributed SQLite&lt;/a&gt; - как сделать, горизонтальное масштабирование с &lt;code&gt;SQLite&lt;/code&gt;. Говорят, работает в проде :)&lt;/li&gt;
&lt;li&gt;Ну и ссылочка на сам проект &lt;a href="https://runit.rnds.pro/" rel="noopener noreferrer"&gt;Беговой Клуб -=БеГиМ=-&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;br&gt;
    #sqlite&lt;br&gt;
    #rails&lt;br&gt;
    #database&lt;br&gt;
    #replicantion&lt;br&gt;
  &lt;/p&gt;

</description>
      <category>other</category>
    </item>
    <item>
      <title>DevSecOps подкрался незаметно, хотя заметен был издалека…</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Tue, 11 Feb 2025 12:34:47 +0000</pubDate>
      <link>https://dev.to/rnds/devsecops-podkralsia-niezamietno-khotia-zamietien-byl-izdalieka-3gjj</link>
      <guid>https://dev.to/rnds/devsecops-podkralsia-niezamietno-khotia-zamietien-byl-izdalieka-3gjj</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7yxdcoty3l1vw31dg73d.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7yxdcoty3l1vw31dg73d.jpeg" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;DevSecOps уверенно шагает по нашей индустрии, и горе тому, кто попадёт под его поступь… Эта статья про &lt;del&gt;ультимативную&lt;/del&gt; сборку базовых образов для Ruby для удовлетворения самых параноидальных потребностей ИБ. Да, именно об этом мы и расскажем - что такое "инсталляция ruby", где, что, почему лежит и как с этим жить нашему пайплайну сборки и самому приложению.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;p id="CaKt"&amp;gt;Статья подойдёт тем, кто хочет более глубоко понимать, какие процессы происходят в системе, когда вызывается &amp;lt;code&amp;gt;gem install&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;bundle install&amp;lt;/code&amp;gt; или (не дай Бог) &amp;lt;code&amp;gt;gem update –system&amp;lt;/code&amp;gt;&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Как было до?
&lt;/h2&gt;

&lt;p&gt;С самого начала появления docker мы в RNDSOFT использовали парадигму "всё включено". Для нас это означало, что в образ мы включаем всё, что нам надо не только для продуктовой эксплуатации, но и для проведения всех тестов. При прохождении CI пайплайна на следующие стадии продвигался образ целиком и, в конце концов, выкатывался на прод.&lt;/p&gt;

&lt;p&gt;Это было очень удобно и позволяло быть максимально (насколько это вообще возможно) уверенным в работоспособности, однако имело ряд серьёзных минусов, с которыми мы прекрасно жили достаточно долгое время:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;размер образа был большим - от 1 ГБ;&lt;/li&gt;
&lt;li&gt;образ включал большое количество неиспользуемого (в проде) ПО.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Сканеры сканировали, сканировали, да не высканировали…
&lt;/h2&gt;

&lt;p&gt;И вот однажды один (на данный момент уже далеко не один) наш клиент захотел устранения всех замечаний, которые смог выявить сканнер &lt;a href="https://github.com/aquasecurity/trivy" rel="noopener noreferrer"&gt;Trivy&lt;/a&gt;. А их, как не трудно догадаться, было достаточно много. И главная проблема заключается в том, что большая часть замечаний никак не связана с непосредственными зависимостями вашего приложения (теми, которые фиксируются в Gemfile.lock).&lt;br&gt;&lt;br&gt;
Так откуда же они берутся?&lt;/p&gt;

&lt;p&gt;Если коротко отвечать - отовсюду :) При сборке проекта необходимо поставить его зависимости - это план минимум. Кроме того, часто появляется необходимость установить bundle определенной версии или даже обновить ruby целиком, выполнив команду &lt;a href="https://guides.rubygems.org/command-reference/#gem-update" rel="noopener noreferrer"&gt;gem update --system&lt;/a&gt;. В результате этих действий в ваш образ в разнообразные папки ставятся разнообразные гемы, но&lt;/p&gt;


    &lt;p id="gt1O"&gt;старые версии гемов и сама базовая "инсталляция ruby" остаётся в системе со всеми своими "устаревшими" и "уязвимыми" версиями. И эти версии очень нравятся сканерам для того, чтобы поднять тревогу. &lt;/p&gt;
  

&lt;p&gt;И еще два слова о том, что же именно сканирует Trivy (касательно ruby конечно):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;сканирует &lt;a href="https://guides.rubygems.org/specification-reference/" rel="noopener noreferrer"&gt;.gemspec файлы&lt;/a&gt; на всей файловой системе и не важно, установлен гем или нет - если в .gemspec указана версия "с уязвимостью" - алярм;&lt;/li&gt;
&lt;li&gt;сканирует .gem файлы на всей файловой системе и не важно, установлен гем, лежит просто в папке cache, используется или нет в вашем приложении.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Значит для удовлетворения хотелок сканера, надо сделать так, чтоб нигде не лежало ничего лишнего или не используемого, &lt;strong&gt;включая&lt;/strong&gt; &lt;a href="https://docs.ruby-lang.org/en/3.2/standard_library_rdoc.html" rel="noopener noreferrer"&gt;stdlib&lt;/a&gt; - стандартную библиотеку ruby.&lt;/p&gt;

&lt;p&gt;Проблема ясна, задача очевидна - можно приступать к решению и начинать надо с базовых образов. Их надо собрать так, чтобы сами базовые образы уже не содержали никаких уязвимостей.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuay6p772f7lsq9gp9wyz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuay6p772f7lsq9gp9wyz.png" width="800" height="267"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Немного удручающее зрелище, особенно для ИБ клиента&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Что такое инсталляция ruby?
&lt;/h2&gt;

&lt;p&gt;Если мы возьмём любой дистрибутив с установленным там ruby, то увидим, что само ruby будет находиться где-то в районе:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;/usr/lib/ruby - например в alpine после &lt;code&gt;apk add ruby&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;/usr/local/lib/ruby - например в &lt;code&gt;ruby:alpine&lt;/code&gt;, где руби собирается отдельно от apk&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;А дальше начинается интересное. Если посмотреть в финальный образ, в котором сделано много различных операций (обновление ruby, установка гемов через &lt;code&gt;gem install&lt;/code&gt;, установка через &lt;code&gt;bundle install&lt;/code&gt;), то в корневой папке ruby обнаружится несколько папок, по которым тем или иным способом будут распределены установленные вами гемы:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/local/lib/ruby
├── 3.2.0
├── gems/3.2.0
├── site_ruby/3.2.0
└── vendor_ruby/3.2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Сразу добавим к этому списку другие папки (которые можно посмотреть в выводе команды &lt;code&gt;gem env&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt; &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;INSTALLATION DIRECTORY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/local/bundle&lt;/span&gt;
 &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;USER INSTALLATION DIRECTORY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/root/.local/share/gem/ruby/3.3.0&lt;/span&gt;
 &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;SPEC CACHE DIRECTORY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/root/.cache/gem/specs&lt;/span&gt;
 &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;GEM PATHS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/usr/lib/ruby/gems/3.3.0&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/root/.local/share/gem/ruby/3.3.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;И вишенкой на торте будет настройка вашего bundle, если вы используете кеширование сборки (&lt;a href="https://bundler.io/man/bundle-cache.1.html" rel="noopener noreferrer"&gt;bundle cache&lt;/a&gt; или &lt;code&gt;bundle config set cache_path vendor/cache&lt;/code&gt;), и используемые в этом зоопарке переменные окружения &lt;code&gt;GEM_HOME&lt;/code&gt;, &lt;code&gt;BUNDLE_CACHE_PATH&lt;/code&gt;, &lt;code&gt;BUNDLE_PATH&lt;/code&gt; и скорее всего еще какие-то скрытые в глубинах экосистемы ruby.&lt;/p&gt;

&lt;p&gt;Немного путано и в результате беспорядочных &lt;del&gt;связей&lt;/del&gt; установок и обновлений во всех этих папках могут (и будут) появляться гемы. Надо исправить!&lt;/p&gt;

&lt;p&gt;Постараюсь дать верхнеуровневое описание, что же именно это за папочки, не углубляясь в подробности и исключения:&lt;/p&gt;

&lt;h3&gt;
  
  
  /usr/local/lib/ruby/3.3.0
&lt;/h3&gt;

&lt;p&gt;Именно в этой папке установлены основные файлы ruby и stdlib, а также компилируемые расширения в папке x86_64-linux-musl (в нашем случае собранные alpine для x86_64). Эти файлы принадлежат условной "системе" и никак не будут изменяться при дальнейших модификациях, например при установке или обновлении гемов через &lt;code&gt;gem install&lt;/code&gt;. Вместо этого библиотеки будут ставиться в папку &lt;code&gt;/usr/local/lib/ruby/gems/3.3.0&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  /usr/local/lib/ruby/gems/3.3.0
&lt;/h3&gt;

&lt;p&gt;Тут собрано всё, что вы ставите (без модификации &lt;code&gt;GEM_PATH&lt;/code&gt;) командами &lt;code&gt;gem install,&lt;/code&gt; включая компилируемые расширения.&lt;/p&gt;

&lt;h3&gt;
  
  
  /usr/local/lib/ruby/vendor_ruby/3.3.0
&lt;/h3&gt;

&lt;p&gt;Сюда &lt;strong&gt;должны&lt;/strong&gt; ставиться дополнительные гемы и/или патчи от команды мейнтейнеров дистрибутива. Об этом сложно найти информацию, но несколько слов есть в книге &lt;code&gt;The Ruby Programming Language&lt;/code&gt; или, что интереснее, в changelog для &lt;a href="https://docs.ruby-lang.org/en/2.3.0/NEWS-1_8_7.html" rel="noopener noreferrer"&gt;NEWS for Ruby 1.8.7&lt;/a&gt; (да, окаменелое…):&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;p id="VAZR"&amp;gt;&amp;lt;strong&amp;gt;vendor_ruby&amp;lt;/strong&amp;gt; directory&amp;lt;br&amp;gt;A new library directory named &amp;lt;code&amp;gt;vendor_ruby&amp;lt;/code&amp;gt; is introduced in addition to &amp;lt;code&amp;gt;site_ruby&amp;lt;/code&amp;gt;. The idea is to separate libraries installed by the package system (&amp;lt;code&amp;gt;vendor&amp;lt;/code&amp;gt;) from manually (&amp;lt;code&amp;gt;site&amp;lt;/code&amp;gt;) installed libraries preventing the former from getting overwritten by the latter, while preserving the user option to override vendor libraries with site libraries. (&amp;lt;code&amp;gt;site_ruby&amp;lt;/code&amp;gt; takes precedence over &amp;lt;code&amp;gt;vendor_ruby&amp;lt;/code&amp;gt;)&amp;lt;br&amp;gt;If you are a package maintainer, make each library package configure the library passing the &amp;lt;code&amp;gt;--vendor&amp;lt;/code&amp;gt; option to &amp;lt;code&amp;gt;extconf.rb&amp;lt;/code&amp;gt; so that the library files will get installed under &amp;lt;code&amp;gt;vendor_ruby&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;You can change the directory locations using configure options such as&amp;lt;code&amp;gt; --with-sitedir=DIR&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;--with-vendordir=DIR&amp;lt;/code&amp;gt;.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  /usr/local/lib/ruby/site_ruby/3.3.0
&lt;/h3&gt;

&lt;p&gt;Сюда будут ставиться "системные" файлы, но не от OS, а от самого ruby, например после выполнения &lt;code&gt;gem update –system&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;site_ruby/
└── 3.4.0
   ├── bundler
   ├── rubygems
   └── x86_64-linux-musl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  /usr/local/lib/ruby/gems/3.3.0/specifications/
&lt;/h3&gt;

&lt;p&gt;…а также INSTALLATION DIRECTORY &lt;code&gt;/usr/local/bundle/specifications&lt;/code&gt;…&lt;br&gt;&lt;br&gt;
…а также USER INSTALLATION DIRECTORY: &lt;code&gt;/root/.local/share/gem/ruby/3.3.0&lt;/code&gt;…&lt;br&gt;&lt;br&gt;
…а также SPEC CACHE DIRECTORY &lt;code&gt;/root/.cache/gem/specs&lt;/code&gt;…&lt;br&gt;&lt;br&gt;
…а также BUNDLE_CACHE_PATH …&lt;/p&gt;

&lt;p&gt;Сюда ставятся .gemspec файлы, которые собственно и говорят пакетному менеджеру ruby (gem и bundle), какие именно версии каких гемов установлены.&lt;/p&gt;
&lt;h3&gt;
  
  
  /usr/local/lib/ruby/gems/3.3.0/specifications/default
&lt;/h3&gt;

&lt;p&gt;Default, Карл… Default - это особое состояние гема, и эти гемы нельзя удалить. Если вы обновили гем до новой версии, то старая всё равно останется, и Trivy вам этого не простит. Весьма вредная папка с точки зрения сканирования.&lt;/p&gt;
&lt;h3&gt;
  
  
  /usr/local/lib/ruby/gems/3.3.0/cache/
&lt;/h3&gt;

&lt;p&gt;…а также INSTALLATION DIRECTORY &lt;code&gt;/usr/local/bundle/cache&lt;/code&gt;…&lt;br&gt;&lt;br&gt;
…а также BUNDLE_PATH …&lt;/p&gt;

&lt;p&gt;Сюда пакетный менеджер ruby (gem или bundle) скачивает гемы (допустим вы ставите faraday.gem) перед установкой. Этот кеш очень часто используется для ускорения сборки, например &lt;a href="https://docs.gitlab.com/ee/ci/caching/#cache-ruby-dependencies" rel="noopener noreferrer"&gt;в вашем Gitlab&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Что здесь у вас происходит?!
&lt;/h2&gt;

&lt;p&gt;После того как мы в процессе исследования детально разобрались и увидели всё это многообразие мест, где могут находиться файлы, вызывающие панические атаки у Trivy, мы решили радикально решить эту проблему: свести все файлы, все гемы и все спеки (.gemspec) в одно место. Это позволит легко следить за всеми (всеми!) фактическими зависимостями и эффективно пользоваться командами &lt;code&gt;gem cleanup&lt;/code&gt; и &lt;code&gt;bundle clean&lt;/code&gt;. Тут надо отметить, что для вашей рабочей OS (системы общего назначения) такое решение приведёт к поломке системного пакетного менеджера (&lt;a href="https://ru.wikipedia.org/wiki/Portage" rel="noopener noreferrer"&gt;Gentoo Portage&lt;/a&gt;, &lt;a href="https://ru.wikipedia.org/wiki/Dpkg" rel="noopener noreferrer"&gt;dpkg/apt&lt;/a&gt;, &lt;a href="https://ru.wikipedia.org/wiki/RPM" rel="noopener noreferrer"&gt;rpm/zypper&lt;/a&gt; и пр.), но мы ведь говорим о конкретной сборке ruby под ваш конкретный проект - и тут никаких проблем не будет.&lt;/p&gt;


    &lt;p id="ma0i"&gt;Для того чтобы узнать что куда, когда и зачем ставится, мы использовали git прямо на корне файловой системы внутри контейнера (ruby:alpine, просто alpine, ruby:debian и другие образы для сравнения) и фиксировали изменения после различных команд ⏳&lt;/p&gt;
  

&lt;p&gt;Но это еще не всё. Остаётся еще проблема с default gemspec, но её относительно легко решить:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; /usr/local/lib/ruby/gems/3.3.0/specifications/default/&lt;span class="k"&gt;*&lt;/span&gt; /usr/local/lib/ruby/gems/3.3.0/specifications/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Теперь мы можем сформулировать План:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Все дороги ведут в Рим - делаем ссылочки для &lt;code&gt;vendor_ruby&lt;/code&gt;, &lt;code&gt;site_ruby&lt;/code&gt; и пр. Также явно прописываем системные переменные &lt;code&gt;GEM_HOME&lt;/code&gt; и &lt;code&gt;BUNDLE_APP_CONFIG.&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Разбираемся с default gems.&lt;/li&gt;
&lt;li&gt;Обновляем ruby (имеется в виду stdlib) до последней требуемой версии.&lt;/li&gt;
&lt;li&gt;Обновляем bundle до нужной версии.&lt;/li&gt;
&lt;li&gt;Удаляем лишние гемы (например rdoc).&lt;/li&gt;
&lt;li&gt;Переставляем (!) системные (на текущий момент сборки базового образа - &lt;strong&gt;все&lt;/strong&gt; ) гемы, потому что, как оказалось, &lt;code&gt;mv&lt;/code&gt; для default gemspec имеет не очень хорошие последствия.&lt;/li&gt;
&lt;li&gt;profit!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Сказано - сделано, и добро пожаловать под кат!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; BASE_RUBY=3.2&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; BASE_ALPINE=alpine3.16&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; BASE_IMAGE=ruby:${BASE_RUBY}-${BASE_ALPINE}&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ${BASE_IMAGE}&lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; BASE_RUBY=3.2&lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; RUBYGEMS_VERSION=3.5.20&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; BUNDLER_VERSION=2.5.20&lt;/span&gt;

&lt;span class="c"&gt;# эта переменная есть в старом alpine но нет в debian и новом&lt;/span&gt;
&lt;span class="c"&gt;# добавляем потому что она очень нужна для работы с папочками&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; RUBY_MAJOR=${BASE_RUBY}&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; RUBYGEMS_VERSION=${RUBYGEMS_VERSION} \&lt;/span&gt;
    BUNDLER_VERSION=${BUNDLER_VERSION}

&lt;span class="k"&gt;RUN &lt;/span&gt;apk update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apk upgrade

&lt;span class="c"&gt;# Это наш костыльный скрипт который удаляет всякие лишние кеши,&lt;/span&gt;
&lt;span class="c"&gt;# man-файлы и прочий мусор. &lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; common/scripts/cleanallbuilds.sh /usr/bin/&lt;/span&gt;

&lt;span class="c"&gt;# dumb-init всегда используем как PID-1 но это немного другая история&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-ex&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apk add dumb-init &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cleanallbuilds.sh

&lt;span class="c"&gt;###### &lt;/span&gt;
&lt;span class="c"&gt;# Надругиваемся над диструбутивом, чтоб иметь строго&lt;/span&gt;
&lt;span class="c"&gt;# одну версию руби в систему и управлять ею целиком через gem/bundle&lt;/span&gt;

&lt;span class="c"&gt;# все пути ведут в Рим&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; GEM_HOME=/usr/local/lib/ruby/gems/${RUBY_MAJOR}.0/&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; BUNDLE_APP_CONFIG=/usr/local/lib/ruby/gems/${RUBY_MAJOR}.0/&lt;/span&gt;

&lt;span class="c"&gt;# Сводим vendor_ruby, site_ruby и GEM_HOME в одно место&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-ex&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /usr/local/lib/ruby/site_ruby /usr/local/lib/ruby/vendor_ruby &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-sf&lt;/span&gt; /usr/local/lib/ruby /usr/local/lib/ruby/site_ruby &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-sf&lt;/span&gt; /usr/local/lib/ruby /usr/local/lib/ruby/vendor_ruby &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /root/.local/share/gem/ruby/ &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GEM_HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; /root/.local/share/gem/ruby/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RUBY_MAJOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.0

&lt;span class="c"&gt;# Чутка тюним bundle config чтоб в дальнейшем не забыть&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-ex&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bundle config &lt;span class="nt"&gt;--local&lt;/span&gt; disable_version_check &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bundle config &lt;span class="nt"&gt;--local&lt;/span&gt; clean &lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bundle config &lt;span class="nt"&gt;--local&lt;/span&gt; no_prune &lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bundle config &lt;span class="nt"&gt;--local&lt;/span&gt; disable_local_branch_check &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bundle config &lt;span class="nt"&gt;--local&lt;/span&gt; &lt;span class="nb"&gt;jobs &lt;/span&gt;2 &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bundle config &lt;span class="nt"&gt;--local&lt;/span&gt; allow_offline_install &lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="c"&gt;# настраиваем .gemrc чтоб не было ничего лишнего, вклюячая rdoc&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-ex&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'gem: --no-document'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /usr/local/etc/gemrc &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'update_sources: false'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /usr/local/etc/gemrc &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'verbose: false'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /usr/local/etc/gemrc &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'update: --no-suggestions'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /usr/local/etc/gemrc &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'install: --no-suggestions --conservative'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /usr/local/etc/gemrc

&lt;span class="c"&gt;# пытаемся удалить ненужные гемы с самого начала - попытка не пытка&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-ex&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; gem uninstall &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;gem list | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; 1 &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;" "&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; gem cleanup

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-ex&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apk add &lt;span class="nt"&gt;--virtual&lt;/span&gt; .build-deps &lt;span class="se"&gt;\
&lt;/span&gt;   autoconf &lt;span class="se"&gt;\
&lt;/span&gt;   bison &lt;span class="se"&gt;\
&lt;/span&gt;   bzip2 &lt;span class="se"&gt;\
&lt;/span&gt;   bzip2-dev &lt;span class="se"&gt;\
&lt;/span&gt;   coreutils &lt;span class="se"&gt;\
&lt;/span&gt;   curl-dev &lt;span class="se"&gt;\
&lt;/span&gt;   dpkg-dev dpkg &lt;span class="se"&gt;\
&lt;/span&gt;   g++ &lt;span class="se"&gt;\
&lt;/span&gt;   gcc &lt;span class="se"&gt;\
&lt;/span&gt;   gdbm-dev &lt;span class="se"&gt;\
&lt;/span&gt;   git &lt;span class="se"&gt;\
&lt;/span&gt;   glib-dev &lt;span class="se"&gt;\
&lt;/span&gt;   libc-dev &lt;span class="se"&gt;\
&lt;/span&gt;   libffi-dev &lt;span class="se"&gt;\
&lt;/span&gt;   libxml2-dev &lt;span class="se"&gt;\
&lt;/span&gt;   libxslt-dev &lt;span class="se"&gt;\
&lt;/span&gt;   linux-headers &lt;span class="se"&gt;\
&lt;/span&gt;   make &lt;span class="se"&gt;\
&lt;/span&gt;   ncurses-dev &lt;span class="se"&gt;\
&lt;/span&gt;   procps &lt;span class="se"&gt;\
&lt;/span&gt;   readline-dev &lt;span class="se"&gt;\
&lt;/span&gt;   &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;   xz &lt;span class="se"&gt;\
&lt;/span&gt;   yaml-dev &lt;span class="se"&gt;\
&lt;/span&gt;   zlib-dev &lt;span class="se"&gt;\
&lt;/span&gt;   shared-mime-info &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mv&lt;/span&gt; /usr/local/lib/ruby/gems/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RUBY_MAJOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.0/specifications/default/&lt;span class="k"&gt;*&lt;/span&gt; /usr/local/lib/ruby/gems/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RUBY_MAJOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.0/specifications/ &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; gem cleanup &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; gem update &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RUBYGEMS_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; gem uninstall bundler &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; gem &lt;span class="nb"&gt;install &lt;/span&gt;bundler &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUNDLER_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; gem uninstall rdoc &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;GMS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;gem list | &lt;span class="nb"&gt;sed &lt;/span&gt;s/default:&lt;span class="se"&gt;\ &lt;/span&gt;// | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'s/\ \((.*)\)/:\1/'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;DEBUG_FLAGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-Wno-calloc-transposed-args"&lt;/span&gt; gem pristine &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--extensions&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;commands&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="k"&gt;for &lt;/span&gt;x &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$GMS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;   &lt;span class="nv"&gt;g&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$x&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; 1 &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;":"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;   &lt;span class="nv"&gt;v&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$x&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; 2 &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;":"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;   &lt;span class="nb"&gt;echo &lt;/span&gt;gem pristine &lt;span class="nv"&gt;$g&lt;/span&gt; &lt;span class="nt"&gt;--version&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$commands&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | xargs &lt;span class="nt"&gt;-I&lt;/span&gt; CMD &lt;span class="nt"&gt;-P&lt;/span&gt; 3 bash &lt;span class="nt"&gt;-c&lt;/span&gt; CMD&lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; gem cleanup &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; root/.local/share/gem/specs &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apk del .build-deps &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cleanallbuilds.sh

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /home/app&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-ex&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; adduser &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /sbin/nologin app &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; app:app /home/app

&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/usr/bin/entrypoint.sh"]&lt;/span&gt;
&lt;span class="k"&gt;SHELL&lt;/span&gt;&lt;span class="s"&gt; ["/bin/sh", "-c"] &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Самое интересное, конечно, находится в одном слое самого пухленького RUN, и по некоторым командам надо дать пояснения:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GMS=&lt;/code&gt;gem list | sed s/default:\ // | sed -E 's/\ ((.*))/:\1/' | sort`&lt;code&gt;- получаем список установленных гемов с версиями в формате "yaml:0.4.0 zlib:3.2.1". Это нам потребуется дальше из-за странного поведения&lt;/code&gt;gem pristine`&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gem pristine --all --extensions&lt;/code&gt; - должен для всех установленных гемов сделать "чистовую" установку, включая скачивание и перекомпиляцию расширений. Но нет. На самом деле он делает не для всех и не всё :(&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEBUG_FLAGS="-Wno-calloc-transposed-args"&lt;/code&gt; - мы используем один Dockerfile для сборки базовых образов ruby 2.7, 3.0, 3.1, 3.2, и по умолчанию все расширения должны компилироваться без единого &lt;del&gt;разрыва&lt;/del&gt; ворнинга компиляции, но для некоторых версий руби (кажется 3.0) это не так, и именно этот ворнинг всё портит, поэтому его подавляем без затей.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;commands=$(for x in $GMS; do …&lt;/code&gt; - а вы знали, что в bash тоже есть пул потоков? Строго говоря, он &lt;a href="https://stackoverflow.com/questions/6441509/how-to-write-a-process-pool-bash-shell" rel="noopener noreferrer"&gt;пул процессов&lt;/a&gt;, и не в баше, а в xargs, но это не важно. В общем тут список &lt;code&gt;$GMS&lt;/code&gt; в виде "yaml:0.4.0 zlib:3.2.1" трансформируется в список &lt;code&gt;$commands&lt;/code&gt;, потому что в такой форме &lt;code&gt;gem pristine&lt;/code&gt; работает именно так как надо:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;`shell&lt;br&gt;
gem pristine yaml --version "0.4.0"&lt;br&gt;
gem pristine zlib --version "3.2.1"&lt;br&gt;
...&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;echo -e "$commands" | xargs -I CMD -P 3 bash -c CMD&lt;/code&gt; - отправляем &lt;code&gt;$commands&lt;/code&gt; в пул из трех процессов для одновременной установки гемов. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cleanallbuilds.sh&lt;/code&gt; - зовём в самом конце кустарный скрипт, который удаляет всякий мусор из системы:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;shell&lt;/p&gt;

&lt;h1&gt;
  
  
  !/bin/sh
&lt;/h1&gt;

&lt;h1&gt;
  
  
  ruby (alpine + debian)
&lt;/h1&gt;

&lt;h1&gt;
  
  
  Скрипт единый идля debian и для alpine, поэтому ошибки,
&lt;/h1&gt;

&lt;h1&gt;
  
  
  возникающие из-за разницы дистрибутивов просто игнорируем
&lt;/h1&gt;

&lt;p&gt;rm -rf /usr/src/ruby/ || true&lt;br&gt;
rm -rf /root/.local/share/gem/specs/ || true&lt;br&gt;
rm -rf /root/.local/state/ || true&lt;br&gt;
rm -rf /root/.cache/gem/ || true&lt;br&gt;
rm -rf /usr/local/lib/ruby/gems/3.2.0/cache/ || true&lt;br&gt;
rm -rf /root/.bundle/cache/ || true&lt;br&gt;
gem cleanup || true&lt;/p&gt;

&lt;h1&gt;
  
  
  alpine
&lt;/h1&gt;

&lt;p&gt;apk cache clean || true&lt;/p&gt;

&lt;h1&gt;
  
  
  debian
&lt;/h1&gt;

&lt;p&gt;apt-get clean autoclean || true&lt;br&gt;
apt-get autoclean --yes || true&lt;br&gt;
apt-get autoremove --yes || true&lt;/p&gt;

&lt;p&gt;find /var/log/ -type f -delete || true&lt;br&gt;
find /var/lib/log/ -type f -delete || true&lt;br&gt;
find /usr/share/doc/ -type f -delete || true&lt;br&gt;
&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;h2&gt;
  
  
  Результат
&lt;/h2&gt;

&lt;p&gt;Старый образ занимал 900MB, новый - 152MB (не спрашивайте...)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F91efaia4bzng23u80z0w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F91efaia4bzng23u80z0w.png" width="800" height="137"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Значительно лучше. Особенно для базового образа&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Что дальше?
&lt;/h2&gt;

&lt;p&gt;Теперь эти базовые образы надо начать использовать непосредственно в CI пайплайнах боевых сервисов и, конечно, процедуру сборки и тестирования придётся поменять. Точнее, мы уже перешли на новые образы и сборку уже пару недель назад - дело теперь только за статьёй. А в качестве приманки приведу результаты перехода для одного из самых "толстых" наших сервисов:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`plaintext&lt;br&gt;
image:old size:1.49GB vulns:255&lt;br&gt;
image:new-prod size:259MB vulns:1 (Вчерашняя!! СVE-2025-25186)&lt;br&gt;
image:new-test size:269MB vulns:1 (СVE-2025-25186)&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Если взглянуть ретроспективно, то совсем не весело - так что:&lt;/p&gt;


&lt;br&gt;
    &lt;p id="tf2v"&gt;профилируйте чаще не только ваш код и инфраструктуру сборки.&lt;/p&gt;
&lt;br&gt;
  &lt;br&gt;
    #docker&lt;br&gt;
    #devsecops&lt;br&gt;
    #ruby&lt;br&gt;
    #cicd&lt;br&gt;
  

</description>
    </item>
    <item>
      <title>Selenium. Как заставить браузер работать на вас</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Wed, 25 Dec 2024 05:52:13 +0000</pubDate>
      <link>https://dev.to/rnds/selenium-kak-zastavit-brauzier-rabotat-na-vas-1mim</link>
      <guid>https://dev.to/rnds/selenium-kak-zastavit-brauzier-rabotat-na-vas-1mim</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9azkyq7fh0qahmlt3s3y.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9azkyq7fh0qahmlt3s3y.jpeg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Всем привет! Сегодня я расскажу, что такое Selenium, как его запустить, зачем он нужен, и какие у него есть плюсы и минусы.&lt;/p&gt;

&lt;h2&gt;
  
  
  Небольшая вводная
&lt;/h2&gt;

&lt;p&gt;Selenium — это инструмент для автоматизации веб-браузеров. Он позволяет разработчикам и тестировщикам писать скрипты, которые могут управлять браузером, имитируя действия пользователя, такие как клики, ввод текста и навигация по страницам.&lt;/p&gt;

&lt;p&gt;Selenium поддерживает множество языков программирования, включая Python, Java, Ruby и другие, и может работать с различными браузерам. Это делает его популярным выбором для автоматизации тестирования веб-приложений и выполнения рутинных задач в браузере.&lt;/p&gt;

&lt;h2&gt;
  
  
  Задачи, решаемые Selenium
&lt;/h2&gt;

&lt;p&gt;Можно разделить на 2 большие группы. Это тестирование и парсинг (скрейпинг). Парсинг - это процесс извлечения и обработки данных из целевых ресурсов.&lt;/p&gt;

&lt;p&gt;К тестированию можно отнести такие подгруппы как:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Тестирование пользовательских интерфейсов.&lt;/strong&gt; Позволяет проверять элементы интерфейса, такие как кнопки, поля ввода и ссылки, чтобы убедиться, что они работают, как задумано.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Кросс-браузерное тестирование.&lt;/strong&gt; Позволяет запускать тесты в различных браузерах, что помогает убедиться, что приложение работает корректно в разных средах.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Регрессионное тестирование.&lt;/strong&gt; Позволяет повторно запускать тесты после внесения изменений в код, чтобы убедиться, что новые изменения не нарушили существующий функционал.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;В простых случаях можно обойтись и без Selenium. Например получать html страницы с помощью простых инструментов, таких как curl.&lt;/p&gt;

&lt;p&gt;Но бывает более сложные случаи, когда Selenium становится нашим лучшим другом:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Динамически загружаемые страницы.&lt;/strong&gt; SPA приложению требуется js.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Обход ограничений.&lt;/strong&gt; Современные сайты часто содержат механизмы защиты от парсинга. Например, проверка cookie, user-agent и, конечно же, captcha.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Для обхода капчи часто используется комбинация Selenium, который позволяет выполнять js на странице и прогрузить капчу, и специального механизма для решения капчи. К таким механизмам относятся:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;_ &lt;strong&gt;Сторонние платные сервисы.&lt;/strong&gt; Принимают изображение или аудиофайл через API и возвращают готовое решение._&lt;/li&gt;
&lt;li&gt;_ &lt;strong&gt;Обученные нейронные сети.&lt;/strong&gt; Запускаются локально и самостоятельно распознают капчи._&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;После получения ответа от выбранного механизма Selenium используется для ввода решения в поле или для выполнения требуемых действий, например, выбора объектов вроде "светофоров" или “мостов”.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Конфигурирование приложения для работы с Selenium
&lt;/h2&gt;

&lt;p&gt;Рассмотрим конфигурирование на примере Ruby on Rails приложения.&lt;/p&gt;

&lt;p&gt;В первую очередь нам необходимо поставить гемы. Для этого добавляем в Gemfile следующие строки:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'selenium-webdriver'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'webdrivers'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;И устанавливаем гемы:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Работа с Selenium
&lt;/h2&gt;

&lt;p&gt;Пример парсинга данных:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'selenium-webdriver'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'webdrivers'&lt;/span&gt;

&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Selenium&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;WebDriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;for&lt;/span&gt; &lt;span class="ss"&gt;:chrome&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="s1"&gt;'http://example.com/products'&lt;/span&gt;
&lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s1"&gt;'product-item'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="nb"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s1"&gt;'product-name'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;
  &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s1"&gt;'product-price'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;
  &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Название: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, Цена: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Функционально пример выше найдет названия и цену товаров на воображаемом сайте и напечатает их в логах. Но этот код можно улучшить.&lt;/p&gt;

&lt;p&gt;Во-первых, нужно учитывать то, что Selenium - это браузер, а значит нам стоит ждать загрузки страницы. Поэтому модифицируем код, и при попытке получить список товаров, ждем 5 секунд и только потом падаем с ошибкой.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Selenium&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;WebDriver&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;timeout: &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;until&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s1"&gt;'product-item'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Во-вторых, нужно хоть немного замаскироваться от систем сайта, которые ограничивают работу парсеров. Для этого добавим немного “человечности” нашему браузеру.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Selenium&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;WebDriver&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Chrome&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'--disable-blink-features=AutomationControlled'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'--headless=new'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'--disable-gpu'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'window-size=1200x800'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Selenium&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;WebDriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:chrome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;options: &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Небольшое пояснение к опциям:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;--disable-blink-features=AutomationControlled.&lt;/strong&gt; Отключает некоторые функции Blink, которые указывают на то, что браузер управляется автоматизированным инструментом. Может помочь избежать обнаружения автоматизации на некоторых сайтах.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;--user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36.&lt;/strong&gt; Устанавливает пользовательский агент (User-Agent) для браузера. Пользовательский агент сообщает веб-сайтам, какую версию браузера и операционной системы использует пользователь. Установка пользовательского агента может помочь в обходе блокировок или в получении контента, оптимизированного для определенного браузера.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;--headless=new.&lt;/strong&gt; Запускает браузер в "безголовом" режиме, что означает, что он будет работать без графического интерфейса. Нужно для автоматизации и тестирования.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;--disable-gpu.&lt;/strong&gt; Отключает использование графического процессора (GPU). Полезно в безголовом режиме, так как некоторые функции, зависящие от GPU, могут вызывать проблемы или не поддерживаться.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;По итогу получится такой код:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'selenium-webdriver'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'webdrivers'&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Selenium&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;WebDriver&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;timeout: &lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;
  &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Selenium&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;WebDriver&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Chrome&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'--disable-blink-features=AutomationControlled'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'--headless=new'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'--disable-gpu'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Selenium&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;WebDriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:chrome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;options: &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com/products'&lt;/span&gt;
  &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;until&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s1"&gt;'product-item'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="nb"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s1"&gt;'product-name'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s1"&gt;'product-price'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;
    &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Название: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, Цена: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Selenium&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;WebDriver&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TimeoutError&lt;/span&gt;
  &lt;span class="s1"&gt;'Элемент с классом product-name не найден'&lt;/span&gt;
&lt;span class="k"&gt;ensure&lt;/span&gt;
  &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;И еще пара советов:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;В браузер можно подгружать плагины:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_extension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'plugin.crx'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Обязательно надо закрывать за собой браузер, чтобы избегать утечек памяти:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Иногда требуется работа с cookie. В этом помогут следующие команды:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;manage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all_cookies&lt;/span&gt; &lt;span class="c1"&gt;# получить все куки&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;manage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cookie_named&lt;/span&gt; &lt;span class="c1"&gt;# получить куку по названию&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;manage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_cookie&lt;/span&gt; &lt;span class="c1"&gt;# записать куку&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Альтернативы Selenium
&lt;/h2&gt;

&lt;p&gt;Самые популярные инструменты для работы с виртуальными браузерами это:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Selenium.&lt;/strong&gt; Старейший представитель. Поддержка множества браузеров и библиотек на различных языках. Большое и активное сообщество. Более сложное api, чем у остальных.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Puppeteer.&lt;/strong&gt; Работает только на Node.js. Поддержка Chrome и Firefox. Сообщество растет, но меньше по сравнению с Selenium. Простое и интуитивно понятное api.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playwright.&lt;/strong&gt; Самый молодой представитель в этом списке. Поддержка множества браузеров и есть библиотеки на различных языках. Удобное и современное api. Встроенная поддержка мобильных устройств. Сообщество активно развивается.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Вывод
&lt;/h2&gt;

&lt;p&gt;Selenium является одним из самых популярных инструментов для автоматизации браузеров и тестирования веб-приложений. Широкая поддержка различных браузеров и языков программирования упрощает процесс написания кода для реализации задач, будь то тестирование или парсинг.&lt;/p&gt;

&lt;p&gt;Долговечность и стабильность подтверждают его надежность и эффективность.&lt;/p&gt;

&lt;p&gt;С большой вероятностью проблема, которую вы пытаетесь решить, уже кем-то была решена, и в интернете можно найти гайд, который поможет.&lt;/p&gt;

&lt;p&gt;Таким образом, Selenium остается одним из лучших инструментов для автоматизации браузеров. Используем его у себя в работе и вам советуем 🙂&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
    <item>
      <title>Простейший AI ассистент или Tools or not tools</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Thu, 12 Dec 2024 15:43:22 +0000</pubDate>
      <link>https://dev.to/rnds/prostieishii-ai-assistient-ili-tools-or-not-tools-4387</link>
      <guid>https://dev.to/rnds/prostieishii-ai-assistient-ili-tools-or-not-tools-4387</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Нужно бежать со всех ног, чтобы только оставаться на месте, а чтобы куда-то попасть, надо бежать как минимум вдвое быстрее! &lt;em&gt;Льюис Кэролл Алиса в Cтране Чудес&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqef2k6fo2rty6q7za0w4.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqef2k6fo2rty6q7za0w4.jpeg" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Вступление&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;В данной статье мы продемонстрируем, как можно построить простейшего AI ассистента. Давайте сперва определимся с терминами. Обычно под AI ассистентом подразумевают способности Больших языковых моделей (далее по тексту LLM) не просто выдавать готовый текстовый ответ, но и совершать какую-то автономную работу по вызову сторонних функций, отправку запросов в API и на основании полученной информации из сторонних сервисов (но иногда нужно получить именно точный ответ в заданном формате), промпта, а также запроса пользователя выдавать итоговый ответ. Так, с терминами определились. Теперь вкратце о чем будет статья: в статье мы покажем, как 2мя способами LLM заставить взаимодействовать с внешним миром и с информацией, полученной не от пользователя, а из внешнего мира. В данной статье мы будем обогащать вывод LLM информацией из поиска, т.к. основной проблемой LLM является то, что в них информация заморожена на определенный момент времени, и с течением времени она устаревает и требует переобучения модели. Переобучение модели является очень дорогостоящим мероприятием, т.о. чтобы актуализировать информацию можно делать запросы в интернет, чтобы получать свежую информацию, а LLM будет нам, используя информацию из своего пространства знаний, а также дополняя информацией из поиска, выдавать достаточно свежий результат. На основе скриптов из этой статьи можно будет уже делать первые попытки для построения собственных мини-ассистентов.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Инструменты&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;В данной статье будут использованы следующие технологии:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;В качестве поискового движка будем использовать Tavily, т.к. у него простое API, а также они заявляют, что оптимизируют свой поиск как раз для использования с LLM (подробней можно почитать в документации к &lt;a href="https://docs.tavily.com/docs/welcome#tavily-search-api" rel="noopener noreferrer"&gt;tavily&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;LLM YandexGPT 4 Pro 32k RC&lt;/li&gt;
&lt;li&gt;Python, Gradio&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Нужно получить &lt;a href="https://yandex.cloud/ru/docs/foundation-models/api-ref/authentication#service-account_1" rel="noopener noreferrer"&gt;API ключи&lt;/a&gt; для YandexGPT API и &lt;a href="https://docs.tavily.com/docs/python-sdk/tavily-search/getting-started" rel="noopener noreferrer"&gt;ключ&lt;/a&gt; для Tavily&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Промптинг&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;В первом способе будем использовать результаты вызова функции в промпте для получения окончательного ответа от LLM как самый примитивный способ.&lt;/p&gt;

&lt;p&gt;Ниже представлен код скрипта &lt;code&gt;yc-search-prompt.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/usr/bin/env python3
import httpx
import os
import gradio as gr
from tavily import TavilyClient

BASE_YC_GPT_URL = "https://llm.api.cloud.yandex.net/foundationModels/v1/completion"

def format_search_results(search_results):
  formatted_results = "\nRelevant search results:\n"
  for result in search_results['results']:
formatted_results += f"- {result['title']}: - URL: {result['url']}  \n {result['content'][:200]}...\n"

def create_prompt_with_search(user_message, search_results):
  search_context = format_search_results(search_results)
  prompt = f"""Here is some relevant context from a web search:
  {search_context}
  Using the above context, please answer the following question:
  {user_message}
  Please provide a comprehensive answer based on both the search results and your knowledge.
  And add at the end of final answer all titles and URL links at format Title - Url from above context."""
  return prompt

def make_search_request(text):
  tavily_client = TavilyClient(api_key=os.environ["TAVILY_API_KEY"] )
  response = tavily_client.search(text, max_results=8)
  return response

def make_request_yc_gpt(text, history):
  with httpx.Client() as client:
  headers = {'Authorization': "Api-Key " + os.environ['YC_API_KEY'], 
  'content-type':'application/json'}
  r = client.post(BASE_YC_GPT_URL, timeout=None, 
  json={"modelUri": "gpt://"+os.environ['YC_FOLDER_ID'] + "/yandexgpt-32k/rc",
  "completionOptions": {
  "stream": False,
  "temperature": "0.3",
  "maxTokens": "2000"
  },"messages": [{"role": "user","text": text}]}, headers=headers)
  return r.json()["result"]["alternatives"][0]["message"]["text"]

def chatbot_function(message, chat_history, model_choice):
  try:
    if model_choice == "YandexGPT+Tavily":
      search_results = make_search_request(message)
      enhanced_prompt = create_prompt_with_search(message, search_results)
      print(enhanced_prompt)
      bot_message = f"You selected the {model_choice} model.\n" + make_request_yc_gpt(enhanced_prompt, chat_history)
      chat_history.append((message, bot_message))
    else:
      bot_message = f"You selected the {model_choice} model.\n" + make_request_yc_gpt(message, chat_history)
      chat_history.append((message, bot_message))
    return "", chat_history
  except Exception as e:
    error_message = f"An error occurred: {str(e)}"
    chat_history.append((message, error_message))
    return "", chat_history
with gr.Blocks() as demo:
  gr.Markdown("AI prompting with internet search")
  with gr.Row():
    with gr.Column(scale=4):
      chatbot = gr.Chatbot()
      msg = gr.Textbox(label="Сообщение")
      submit = gr.Button("Отправить")
      clear = gr.Button("Очистить")
    with gr.Column(scale=1):
      model = gr.Radio(
          ["YandexGPT+Tavily", "YandexGPT"],
          label="Выберите модель",
          value="YandexGPT+Tavily"
        )
  submit.click(chatbot_function, inputs=[msg, chatbot, model], outputs=[msg, chatbot])
  msg.submit(chatbot_function, inputs=[msg, chatbot, model], outputs=[msg, chatbot])
  clear.click(lambda: None, None, chatbot, queue=False)
demo.launch()

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Примечание:&lt;/em&gt; &lt;strong&gt;Помните: при запросах к YandexGPT и Tavily могут списываться денежные средства. Перед запуском скрипта читайте актуальные правила использования сервисов.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Запускать скрипт следующим образом&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;YC_FOLDER_ID=folder_id YC_API_KEY=YANDEX_API_KEY TAVILY_API_KEY=TAVILY_KEY  python3 yc-search-prompt.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Принцип работы скрипта: В UI Gradio на вход скрипт принимает текст от пользователя, в зависимости от того выбран ли вариант использования вместе с Tavily (YandexGPT+Tavily), тогда отправляется запрос в поиск Tavily, потом результат поиска отдается YandexGPT с промптом и просьбой сформировать окончательный ответ из собственных знаний, а также результатов поиска, а также в ответ добавить ссылки на источники из поиска.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Function calling (Tools)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Второй способ также будет использовать промпт для получения окончательного ответа, но для получения результатов поиска мы будем использовать функционал function calling. Наверное стоит остановиться подробней, для чего это нужно, т.к. кода стало почти в 2 раза больше, а результат такой же. Function calling (Tools) - это способность LLM вызывать сторонние приложения, это могут быть скрипты, обращения к различным API. В большинстве случаев это необходимо, когда для LLM нужно получить конкретный ответ (конечно, пример с использованием поиска не очень подходящий, но хотелось сделать примеры максимально похожими, больше здесь подходит, например, вызов функции, которая использует калькулятор), что-то посчитать, а т.к. LLM не предназначены для конкретных вычислений, то для этого используется функционал function calling. При этом если Tools будет много, то LLM может и сама принимать решение, когда и какой Tool ей вызывать (у anthropic есть прямо &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/tool-use#controlling-claudes-output" rel="noopener noreferrer"&gt;определение поведения&lt;/a&gt; LLM для выбора Tools. Важный момент: нужно делать хорошее описание для tools. Вот примеры хороших и плохих описаний tools от одного из &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/tool-use#best-practices-for-tool-definitions" rel="noopener noreferrer"&gt;лидеров индустрии&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Примечания:&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;em&gt;в YandexGPT API на момент написания статьи функционал Tools находился в режиме бета-тестирования, может быть непредвиденное поведение.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;На момент написания статьи в скриптах использовалась версия релиз кандидат YandexGPT RC 32k, подробней про жизненный цикл моделей &lt;a href="https://yandex.cloud/ru/docs/foundation-models/concepts/yandexgpt/models#model-lifecycle" rel="noopener noreferrer"&gt;читайте в документации&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ниже представлен код скрипта &lt;code&gt;yc-search-tools.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/usr/bin/env python3

import httpx
import os
import json
from tavily import TavilyClient
import gradio as gr

BASE_YC_GPT_URL = "https://llm.api.cloud.yandex.net/foundationModels/v1/completion"
search_tool = {
  "function": {
    "name": "search_tavily",
    "description": "Search the web for current information",
      "parameters": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "The search query"
          }
        },
        "required": ["query"]
      }
  }
}

def format_search_results(search_results):
  formatted_results = "\nRelevant search results:\n"
  for result in search_results['results']:
    formatted_results += f"- {result['title']}: - URL: {result['url']} \n {result['content'][:200]}...\n"
  return formatted_results

def create_prompt_with_search(user_message, search_results):
  search_context = format_search_results(search_results)
  prompt = f"""Here is some relevant context from a web search:
{search_context}

Using the above context, please answer the following question:
{user_message}

Please provide a comprehensive answer based on both the search results and your knowledge.
And add at the end of final answer all titles and URL links at format Title - Url from above context."""
  return prompt

def make_request_yc_gpt(text, is_tool_call=True):
  with httpx.Client() as client:
    headers = {'Authorization': "Api-Key " + os.environ['YC_API_KEY'], 
    'content-type':'application/json'}
    payload = {
      "modelUri": f"gpt://{os.environ['YC_FOLDER_ID']}/yandexgpt-32k/rc",
      "completionOptions": {
        "stream": False,
        "temperature": 0.0,
        "maxTokens": 8000
      },
      "messages": text
    }

    if is_tool_call:
      payload["tools"] = [search_tool]

    r = client.post(
    BASE_YC_GPT_URL,
    timeout=None,
    json=payload,
    headers=headers
    )

    response = r.json()
  return response

def handle_tool_calls(toolCalls):
  results = []
  for tool_call in toolCalls:
    if toolCalls[0]["functionCall"]["name"] == "search_tavily":
      result = make_search_request(tool_call["functionCall"]["arguments"]["query"])
  return result
def make_search_request(text):
  tavily_client = TavilyClient(api_key=os.environ["TAVILY_API_KEY"] )
  response = tavily_client.search(text, max_results=8)
  return response

def process_conversation(user_input, history):
  conversation = [
    {
      "role": "system",
      "text": "You are a helpful bot that helps the user. You can use tools at your discretion to generate answers, but you don't always need to use them."
    },
    {
      "role": "user",
      "text": user_input
    }
  ]
  initial_response = make_request_yc_gpt(conversation)

  if "toolCalls" in initial_response['result']['alternatives'][0]['message']['toolCallList']:
    tool_results = handle_tool_calls(
      initial_response['result']['alternatives'][0]['message']['toolCallList']["toolCalls"]
    )

    enhanced_prompt = create_prompt_with_search(user_input, tool_results)

    final_conversation = [
      {
        "role": "user",
        "text": enhanced_prompt
      }
    ]

    final_response = make_request_yc_gpt(final_conversation, is_tool_call=False)
    return final_response['result']['alternatives'][0]['message']['text']
  else:
    return initial_response['result']['alternatives'][0]['message']['text']

def chatbot_function(message, chat_history, model_choice):
  try:
    response_list = process_conversation(message, chat_history)

    if isinstance(response_list, list):
      formatted_responses = [item['text'] for item in response_list if isinstance(item, dict) and 'text' in item] 
      bot_message = f"You selected the {model_choice} model.\n" + "\n".join(formatted_responses)
    else:
      bot_message = f"You selected the {model_choice} model.\n" + str(response_list)
    chat_history.append((message, bot_message))
    return "", chat_history 
  except Exception as e: 
    error_message = f"An error occurred: {str(e)}" 
    chat_history.append((message, error_message)) 
    return "", chat_history
with gr.Blocks() as demo:
  gr.Markdown("AI function calling tools internet search") 
    with gr.Row(): 
      with gr.Column(scale=4): 
        chatbot = gr.Chatbot() 
        msg = gr.Textbox(label="Сообщение") 
        submit = gr.Button("Отправить") 
        clear = gr.Button("Очистить") 
      with gr.Column(scale=1):
        model = gr.Radio( 
          ["YandexGPT+Tavily"], 
          label="Модель", 
          value="YandexGPT+Tavily" 
        ) 
  submit.click(chatbot_function, inputs=[msg, chatbot, model], outputs=[msg, chatbot]) 
  msg.submit(chatbot_function, inputs=[msg, chatbot, model], outputs=[msg, chatbot]) 
  clear.click(lambda: None, None, chatbot, queue=False) 
demo.launch()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Примечание:&lt;/em&gt; &lt;strong&gt;Помните, при запросах к YandexGPT и Tavily могут списываться денежные средства. Перед запуском скрипта читайте актуальные правила использования сервисов.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Запускать скрипт следующим образом&lt;/p&gt;

&lt;p&gt;&lt;code&gt;YC_FOLDER_ID=folder_id YC_API_KEY=YANDEX_API_KEY TAVILY_API_KEY=TAVILY_KEY  python3 yc-search-tools.py&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Вкратце принцип работы скрипта: В UI Gradio на вход скрипт принимает текст от пользователя, вызывает tool tavily_search, после этого результаты поиска отдается YandexGPt с промптом и просьбой сформировать окончательный ответ из собственных знаний, результатов поиска, а также в ответ добавить ссылки на источники из поиска.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Итоги и дополнительные материалы&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;В итоге мы получили простенький аналог perplexity.ai, сделанный своими руками. Несколько скринов как это выглядит.&lt;/p&gt;

&lt;p&gt;Ответ YandexGPT, дополненный информацией из Tavily&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXcJbJAI8kAVt__Oh7QxUEfbEdLVUd1PM_8QhHozgTxkYysnJy-TwswWc5LHTALoKykLIxo5NcPFQysdTYPf2aTgNv8O2jRi-MoS5bcvVwiprp_u5U8KOrEeYgf_MWVeC58PM1Oo1w%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXcJbJAI8kAVt__Oh7QxUEfbEdLVUd1PM_8QhHozgTxkYysnJy-TwswWc5LHTALoKykLIxo5NcPFQysdTYPf2aTgNv8O2jRi-MoS5bcvVwiprp_u5U8KOrEeYgf_MWVeC58PM1Oo1w%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" width="800" height="440"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;YandexGPT дополненный информацией из Tavily&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;И ниже ответ дополняется ссылками (блок Titles and URLs) в поиске&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXeGwTUa0egcd0d3Ul4ZnRlsdnRm4Oth5r9L2S8LUl7A-L-u0xdQlxQsNi9glUEygtrIvZlg_4LclUISlB7bppvD3f2h7j8c5lr-hbymRtHia2JjMgUIyWytv9pECVQ3SwmK5n71DA%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXeGwTUa0egcd0d3Ul4ZnRlsdnRm4Oth5r9L2S8LUl7A-L-u0xdQlxQsNi9glUEygtrIvZlg_4LclUISlB7bppvD3f2h7j8c5lr-hbymRtHia2JjMgUIyWytv9pECVQ3SwmK5n71DA%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" width="800" height="443"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;YandexGPT дополненный информацией из Tavily&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ответ YandexGPT без дополнения ответа результатами из поиска, как видно внизу без ссылок на результаты поиска&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXf1nEIXuj2_I2fENHgvryV7nbi4dMTKjjwdR7u3RnFDXz-NRaawv8-sSqoKSk0Y1SHgQnsjR5eRQn6fsXS5SUfbrF6pz0p5LqHUt5zR-SJzoQI9Zwq4pIrlq_l8bh-kypZOXUvl7Q%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXf1nEIXuj2_I2fENHgvryV7nbi4dMTKjjwdR7u3RnFDXz-NRaawv8-sSqoKSk0Y1SHgQnsjR5eRQn6fsXS5SUfbrF6pz0p5LqHUt5zR-SJzoQI9Zwq4pIrlq_l8bh-kypZOXUvl7Q%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" width="800" height="438"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;YandexGPT без дополненным ответом из поиска&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Приводим аналогичные скрины с использованием функционала Tools&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXdQxydh47aqy9Z0CUGYvsn57NT9l79qLo_Rdm2K94Xabl-AeI3JXCZSjsE_esTQ02RutkkCQU1dzAT9pdgGeG8haHNybUrLEylXEfHFf0I_HVthKogWFFTWl_d_H_FqYCHRkZwS%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXdQxydh47aqy9Z0CUGYvsn57NT9l79qLo_Rdm2K94Xabl-AeI3JXCZSjsE_esTQ02RutkkCQU1dzAT9pdgGeG8haHNybUrLEylXEfHFf0I_HVthKogWFFTWl_d_H_FqYCHRkZwS%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" width="800" height="435"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;YandexGPT + Tavily + Function calling (Tools)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXfS0QQxLYPTKE3p_QkRlrqBIsq2GHCmDhFnXQ8KwASXqkLOEJbVoiq4wztaGQ8CnfbcT0-Z2s8O1623EhOsa5VwSefNHimaJbikHIBJcFKIXplvQZcLf0nouWzggTRFklbyFmy3gA%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXfS0QQxLYPTKE3p_QkRlrqBIsq2GHCmDhFnXQ8KwASXqkLOEJbVoiq4wztaGQ8CnfbcT0-Z2s8O1623EhOsa5VwSefNHimaJbikHIBJcFKIXplvQZcLf0nouWzggTRFklbyFmy3gA%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" width="800" height="440"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;YandexGPT + Tavily + Function calling (Tools)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Одним из самых частых препятствий в процессе промышленного внедрения LLM являются галлюцинации и проблемы получения конкретных ответов, и хочется сказать еще пару слов про библиотеки и фреймворки, которые могут быть полезны для решения этих проблем.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Не пренебрегайте промптами, если у вас нет других инструментов для контроля LLM.  Хороший материал на тему &lt;a href="https://www.promptingguide.ai/" rel="noopener noreferrer"&gt;prompt engineering&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;Кроме промптинга есть фреймворки для работы с промптами, например, &lt;a href="https://dspy.ai/" rel="noopener noreferrer"&gt;dspy&lt;/a&gt;, к сожалению, из коробки поддержки YandexGPT там нет, но можно пробовать использовать &lt;a href="https://github.com/all-mute/openai2yandex_api_adapter" rel="noopener noreferrer"&gt;адаптер для совместимости с OpenAI API&lt;/a&gt; + dspy (сами, честно говоря, еще не пробовали) и &lt;a href="https://yandex.cloud/ru/features/4631" rel="noopener noreferrer"&gt;проголосовать за фичу&lt;/a&gt; в Yandex Cloud&lt;/li&gt;
&lt;li&gt;Помимо коммерческих реализаций есть также уже много open source моделей, в которых реализован функционал tools, например, у &lt;a href="https://ollama.com/search?c=tools" rel="noopener noreferrer"&gt;ollama&lt;/a&gt;. Кажется, скоро эта фича станет стандартной в LLM.&lt;/li&gt;
&lt;li&gt;Кроме промптинга есть еще возможность заставить LLM четко следовать формату ответа, это т.н. structured output, вот &lt;a href="https://simmering.dev/blog/structured_output/" rel="noopener noreferrer"&gt;хорошая статья&lt;/a&gt; с библиотеками, большинство библиотек в этом списке тоже не поддерживают YandexGPT (а также почти все нестабильных версий 0.x.x), но чтобы этот мир стал еще лучше, можете проголосовать &lt;a href="https://yandex.cloud/ru/features/4627" rel="noopener noreferrer"&gt;за эту фичу&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Фреймворки для построения агентов и мультиагентских систем&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://langchain-ai.github.io/langgraph/tutorials/introduction/" rel="noopener noreferrer"&gt;LangGraph&lt;/a&gt; кандидат, чтобы стать стандартом в индустрии (там готовится целая экосистема библиотек для работы с LLM LangChain, LangSmith, LangGraph), самая большая поддержка различных LLM. Очень нестабильно. Но в документации к LangGraph можно почерпнуть много хороших идей для построения агентов &lt;a href="https://langchain-ai.github.io/langgraph/tutorials/" rel="noopener noreferrer"&gt;Tutorials&lt;/a&gt; и &lt;a href="https://langchain-ai.github.io/langgraph/how-tos/" rel="noopener noreferrer"&gt;How-To&lt;/a&gt;, это прямо must read! Для построения MVP и быстрого прототипирования подходит отлично.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.llamaindex.ai/en/stable/use_cases/agents/" rel="noopener noreferrer"&gt;Llamaindex&lt;/a&gt; Тоже довольно большая библиотека для работы с LLM. Подробней что-то  рассказать сложно, сами не пользовались.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://microsoft.github.io/autogen/0.2/" rel="noopener noreferrer"&gt;AutoGen&lt;/a&gt; библиотека для построения мультиагентских систем. Огромное кол-во примеров под разные use cases.&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Совсем недавно в Yandex cloud появился новый функционал &lt;a href="https://yandex.cloud/ru/docs/foundation-models/concepts/assistant/" rel="noopener noreferrer"&gt;AI assistant&lt;/a&gt;, который с использованием их ML SDK тоже позволяет строить LLM приложения и прячет некоторые вещи “под капот”, которые обычно используются в LLM приложениях: RAG, сохранение контекста.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Также совсем недавно anthropic выпустил &lt;a href="https://modelcontextprotocol.io/introduction" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;, для более глубокой интеграции LLM, tools и источников данных. Посмотрим, сможет ли MCP стать стандартом в будущем.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Также аналогичный функционал, скорей всего, можно реализовать более простым способом, использовав лишь один инструмент &lt;a href="https://yandex.cloud/ru/docs/search-api/concepts/generative-response" rel="noopener noreferrer"&gt;Search API&lt;/a&gt; в Yandex Cloud (на момент написания статьи функционал был в Preview режиме)&lt;/p&gt;&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>other</category>
    </item>
    <item>
      <title>Централизованное логирование</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Thu, 03 Nov 2022 13:12:06 +0000</pubDate>
      <link>https://dev.to/rnds/tsientralizovannoie-loghirovaniie-naa</link>
      <guid>https://dev.to/rnds/tsientralizovannoie-loghirovaniie-naa</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fktmqer6g4p7xl2hbqeyy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fktmqer6g4p7xl2hbqeyy.png" width="695" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;В данной статье мы рассмотрим вопрос централизованного логирования с использованием filebeat (гребаный спойлер) и graylog. В какой-то момент мы заметили, что машин в нашей инфраструктуре стало достаточно много, и чтобы посмотреть логи, приходилось иногда заходить на несколько машин и мучительно искать по множеству контейнеров, в этот момент мы поняли, что дальше жить так нельзя.&lt;/p&gt;

&lt;p&gt;Постановка задачи стояла следующим образом:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Чем собирать логи&lt;/li&gt;
&lt;li&gt;Где хранить&lt;/li&gt;
&lt;li&gt;Нужно не потерять логи при недоступности центрального хранилища логов&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ранее мы уже пробовали централизованно собирать логи vector (&lt;a href="https://vector.dev/" rel="noopener noreferrer"&gt;https://vector.dev/&lt;/a&gt;), но нам хотелось иметь удобный веб-интерфейс для просмотра. А vector веб-интерфейса не имеет. В качестве веб-интерфейса, а заодно и местом централизованного хранения, мы выбрали graylog, фактически сейчас единственное open source решение, которое имеет обширный функционал и удобство использования. На момент написания статьи vector “из коробки” не умеет отправлять в gelf формате в graylog, но в одном из issues на github один энтузиаст смог сконфигурировать vector нужным образом (&lt;a href="https://github.com/vectordotdev/vector/issues/4868#issuecomment-782740639" rel="noopener noreferrer"&gt;https://github.com/vectordotdev/vector/issues/4868#issuecomment-782740639&lt;/a&gt;). В общем vector в качестве решения для сбора логов отбросили.&lt;/p&gt;

&lt;p&gt;Прежде чем отвечать на вопрос “Чем собирать”, нужно немного сказать про инфраструктуру: в инфраструктуре у нас все приложения запускаются в docker контейнерах, машин на момент написания у нас уже 16, а самих контейнеров около 60, контейнеры в свою очередь находятся под управлением nomad. Также важным моментом является то, что несколько машин в нашей инфраструктуре являются прерываемыми, они один раз в сутки перезапускаются. С инфраструктурой разобрались, теперь нужно определяться, чем собирать. Сперва наш взгляд упал на fluentd, fluentbit, т.к. они умели отправлять в gelf формате. В процессе настройки fluentd почему-то не слал больше 30 сообщений, разобраться, почему, нам так и не удалось, при использовании fluentbit в какой-то момент использования docker logging driver gelf и настройки fluentbit c output gelf выяснилось, что при недоступности fluentbit в момент рестарта контейнеров (на прерываемых машинах контейнеры один раз в сутки перезапускаются) они не запустятся и становятся неработоспособными. Мы поняли, что чем проще - тем лучше, и решили, что нужно собирать из файлов. В свою очередь настроили fluentbit для сбора из файлов, но fluentbit почему-то тоже доставлял не все логи, аналогично с fluentd определить, почему так происходит, не удалось. И в итоге мы остановились на filebeat в качестве сборщика логов (&lt;a href="https://www.elastic.co/guide/en/beats/filebeat/current/how-filebeat-works.html" rel="noopener noreferrer"&gt;https://www.elastic.co/guide/en/beats/filebeat/current/how-filebeat-works.html&lt;/a&gt;), а graylog в качестве хранилища логов.&lt;/p&gt;

&lt;p&gt;Еще один важный момент, который нужно учесть, это то, что graylog находится в другой инфраструктуре, т.е. прямой сетевой связности между graylog сервером и машинами, на которых запущен filebeat, нет. Т.о. мы связали это все через гейт, на котором у нас находится traefik (смотри диаграмму ниже). Теперь перейдем к настройке. Graylog мы запустили на отдельной машине в обычном docker-compose.yml файле.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh3.googleusercontent.com%2FWzf3hrjZEtkmygD2Myb8HKG9vYQdSzdaCS8UdGvXwEfO8hBBqSO_OVuI80kjGd3L26CzZSVCDNPCvxLLeVeIK-0HZS3NNW2dOCEOEtTp8VmHwu0MX--123ayMNjPmAKWZ0IC-2LgrNoAFt2VrbzPi3U7G0EMlDhIgANWdpcAHFQJQOnCAlcXivE_WQ" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh3.googleusercontent.com%2FWzf3hrjZEtkmygD2Myb8HKG9vYQdSzdaCS8UdGvXwEfO8hBBqSO_OVuI80kjGd3L26CzZSVCDNPCvxLLeVeIK-0HZS3NNW2dOCEOEtTp8VmHwu0MX--123ayMNjPmAKWZ0IC-2LgrNoAFt2VrbzPi3U7G0EMlDhIgANWdpcAHFQJQOnCAlcXivE_WQ" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Graylog docker-compose.yml&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: "2.3"

services:

 es01:
   image: elasticsearch:7.10.1
   restart: unless-stopped
   network_mode: host
   ulimits:
     memlock:
       soft: -1
       hard: -1
     nofile:
       soft: 65536
       hard: 65536
   environment:
       - "ES_JAVA_OPTS=-Xms5g -Xmx5g"
       - ELASTIC_PASSWORD=test
       - discovery.type=single-node
       - cluster.max_shards_per_node=20000
       - search.max_open_scroll_context=9000
   volumes:
     - /etc/localtime:/etc/localtime:ro
     - /graylog/data/es01:/usr/share/elasticsearch/data
   labels:
     - "SERVICE_CHECK_HTTP=/_cluster/health"
     - "SERVICE_CHECK_INTERVAL=40s"
     - "SERVICE_CHECK_TIMEOUT=3s"
     - "SERVICE_CHECK_DEREGISTER_AFTER=10m"

 mongodb:
   image: mongo:4.2
   restart: unless-stopped
   network_mode: host
   volumes:
     - /graylog/data/mongo:/data/db

 graylog:
   image: graylog/graylog:4.3.3
   restart: unless-stopped
   depends_on:
     - mongodb
     - es01
   network_mode: host
   volumes:
     - /graylog/data/graylog:/usr/share/graylog/data
   environment:
     - GRAYLOG_PASSWORD_SECRET=test
     - GRAYLOG_ROOT_PASSWORD_SHA2=shashasha
     - GRAYLOG_HTTP_EXTERNAL_URI=http://192.168.2.2:9000/
   entrypoint: /usr/bin/tini -- wait-for-it 172.22.202.104:9200 -- /docker-entrypoint.sh
   labels:
     - "SERVICE_TAGS=hostname=graylog,traefik.enable=true,\
        traefik.http.routers.graylog.rule=Host(`graylog.dev`),\
        traefik.http.routers.graylog.entrypoints=websecure,\
        traefik.http.routers.graylog.tls=true"
     - SERVICE_NAME=graylog
     - SERVICE_CHECK_DEREGISTER_AFTER=10m
     - SERVICE_9000_NAME=graylog
     - SERVICE_9000_CHECK_TCP=true
     - SERVICE_9000_CHECK_INTERVAL=40s
     - SERVICE_9000_CHECK_TIMEOUT=5s
     - SERVICE_9000_CHECK_DEREGISTER_AFTER=10m

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Перейдем к конфигурации filebeat&lt;/p&gt;

&lt;p&gt;Конфиг filebeat.yml у нас простейший&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;filebeat.inputs:
- type: container
  paths:
    - /var/lib/docker/containers/*/*.log
  processors:
    - add_docker_metadata:
        match_source_index: 4 
output.logstash:
   hosts: 
     - graylog_host:graylog_port

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Тут все просто, мы просто отправляем все логи всех контейнеров на хост graylog, в свою очередь graylog_host - это у нас хост с traefik, а в конфигурации traefik уже указан сам graylog, т.к. с гейта есть прямая сетевая связность с graylog.&lt;/p&gt;

&lt;p&gt;Т.к. у нас nomad, то filebeat мы деплоим тоже в nomad, далее конфигурация filebeat job для деплоя его в nomad. Единственное, что здесь стоит отметить, так это то, что в env параметры GRAYLOG_HOST, GRAYLOG_PORT мы передаем ip адрес и порт на traefik. Также стоит обратить внимание, что тип job у нас system, что запускает filebeat на всех машинах. Также мы монтируем в контейнер filebeat каталог /filebeat/registry, для сохранения registry файла. Это необходимо для того, чтобы при отказе output filebeat отслеживал последние изменения, чтобы после того, как output вернется в рабочее состояние, доставить туда логи. Более подробно про registry файл можно почитать в документации filebeat &lt;a href="https://www.elastic.co/guide/en/beats/filebeat/current/how-filebeat-works.html#_how_does_filebeat_keep_the_state_of_files" rel="noopener noreferrer"&gt;https://www.elastic.co/guide/en/beats/filebeat/current/how-filebeat-works.html#_how_does_filebeat_keep_the_state_of_files&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;job "filebeat" {
 datacenters = ["a", "b", "c"]
 namespace = "default"
 update {
   max_parallel = 1
   auto_revert = true
   auto_promote = false
   canary = 0
 }
 type = "system"
 group "logging" {
   restart {
     mode = "delay"
     attempts = 2
     interval = "1m"
     delay = "30s"
   }
   task "filebeat" {
     driver = "docker"
     kill_timeout = "30s"
     leader = true
     user = "root"
     env {
       SERVICE_IGNORE = "true"
       GRAYLOG_HOST = "GRAYLOG_HOST"
       GRAYLOG_PORT = "GRAYLOG_PORT"
     }
     config {
       auth {
         username = "test"
         password = "test"
       }
       image = "docker.registry/filebeat:7.10.1"
       force_pull = true
       volumes = [
         "/var/lib/docker/containers:/var/lib/docker/containers:ro",
         "/var/run/docker.sock:/var/run/docker.sock:ro",
         "/filebeat/registry:/filebeat/registry:rw"
       ]
     }
     service {
       name = "filebeat"
       tags = ["filebeat", "logging", "hostname=${attr.unique.hostname}"]
       check_restart {
         grace = "1m"
         ignore_warnings = false
       }
     }
     logs {
       max_files = 3
       max_file_size = 10
     }
     resources {
       memory = 100
       memory_max = 150
     }
   }
 }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Теперь перейдем к конфигурации traefik, здесь лишь фрагмент конфига traefik.yml, мы добавляем еще один entrypoint для graylog.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;…………
entryPoints:
 web:
   address: ":80"
   forwardedHeaders:
     insecure: true

 websecure:
   address: ":443"
   http:
     middlewares:
       - sslheader@file
       - trimwww@file

 graylog:
   address: ":11111"
…………
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Также не забываем сконфигурировать порт для работы с graylog в конфиге job nomad для traefik, ниже указан фрагмент traefik job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;job "traefik" {
 datacenters = ["a", "b", "c"]

…………………

 type = "system"

 group "traefik" {
   network {
     mode = "host"

     port "http" {
       static = 80
       to = 80
       host_network = "private"
     }

     port "public" {
               static = 443
               to = 443
       host_network = "public"
     }

     port "ui" {
               to = 8080
       host_network = "private"
     }

     port "graylog" {
       static = 11111
       to = "11111"
       host_network = "private"
     }

   }

   task "traefik" {
     driver = "docker"
     kill_timeout = "30s"
     leader = true

………………

     config {
       image = "traefik:v2.8.7"
       force_pull = true
       ports = ["http", "public", "ui",
        "graylog"
       ]
…………………

}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Так выглядят entrypoints в веб-интерфейсе traefik.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh5.googleusercontent.com%2Fmz-VGdWXCb8e5kk2aw3Qu8fcp3HrmCBv7BH5c2zlDJ2Q-B-xZfRd26WpeoDzCoQ-Kn9ldo7fMgoK6DUGWAvJjOLNhXmFQB5-MV_MMr9W2XtgGYeEclMuMV6i57VLj7wHUM-AjcoFKy0uvpg-zjRhauC1xRanuFZGUa62nnjt8mh0e9Lv_M7FiI_CSQ" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh5.googleusercontent.com%2Fmz-VGdWXCb8e5kk2aw3Qu8fcp3HrmCBv7BH5c2zlDJ2Q-B-xZfRd26WpeoDzCoQ-Kn9ldo7fMgoK6DUGWAvJjOLNhXmFQB5-MV_MMr9W2XtgGYeEclMuMV6i57VLj7wHUM-AjcoFKy0uvpg-zjRhauC1xRanuFZGUa62nnjt8mh0e9Lv_M7FiI_CSQ" width="800" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Конфигурация роутера для graylog осуществляется в tcp router, вот как это выглядит в веб-интерфейсе traefik:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh4.googleusercontent.com%2FYsxr2MAe0rRmJ0oB8A13O0N92gb8XX2kVsDvvRpEXv2PXxo6pWrK3o0TZthTuj9oXI61ZL7cULvAW4jpEGBaykShHkKOWJiO-lcL3D8xJOUEH7BTjLKtebrbUlw5ay41UNAXMVu9MiWNnYEdgM5ALW3yDkcMmPw0WpyXvcoIH51mfHj1ZywVkyPvgA" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh4.googleusercontent.com%2FYsxr2MAe0rRmJ0oB8A13O0N92gb8XX2kVsDvvRpEXv2PXxo6pWrK3o0TZthTuj9oXI61ZL7cULvAW4jpEGBaykShHkKOWJiO-lcL3D8xJOUEH7BTjLKtebrbUlw5ay41UNAXMVu9MiWNnYEdgM5ALW3yDkcMmPw0WpyXvcoIH51mfHj1ZywVkyPvgA" width="800" height="164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Также нужно сконфигурировать сервис (в терминологии traefik), куда мы будем отправлять логи в graylog, именно в конфигурации сервиса в traefik указывается сам ip и порт graylog сервера.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh6.googleusercontent.com%2FOCl-uznt6dQa2ele5-nFX-ONbgIOzshTwTHIGLklhWQRFmJJ6am12UCE2lvcwZ8VSWYkVWq5DsabEMRFFPAwhByOU8GzDvvCapa_8HiQ4k7CIDczWzHwugvFNcbq-A8nskxOIS_bTmBhAtWvVij2GXIK__IOunMQWz7jONWL-mGkrdHvPvbz1i38sg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh6.googleusercontent.com%2FOCl-uznt6dQa2ele5-nFX-ONbgIOzshTwTHIGLklhWQRFmJJ6am12UCE2lvcwZ8VSWYkVWq5DsabEMRFFPAwhByOU8GzDvvCapa_8HiQ4k7CIDczWzHwugvFNcbq-A8nskxOIS_bTmBhAtWvVij2GXIK__IOunMQWz7jONWL-mGkrdHvPvbz1i38sg" width="800" height="172"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;В веб-интерфейсе graylog вот так конфигурируется input (в терминологии graylog), т.е. входная точка, которая будет принимать логи на стороне graylog. Тут видно, что тип input у нас Beats.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh5.googleusercontent.com%2FRe1exwPm1JsctCoXBboA5KERvxyg-4Z6t1KISMzWUIMIASXtHdF42_Y1sWljwjcRY4gUNaJMPVVQ4NNjNxjEYBoaqz1T4G-0KESu9WK7pErdLXupdtG4bIUX1yotqUAx2nwUEGc5QjAAa2XvN_erECMhvUl0ryFhEymRM-OFJJzba6ikGgPIKCvV3w" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh5.googleusercontent.com%2FRe1exwPm1JsctCoXBboA5KERvxyg-4Z6t1KISMzWUIMIASXtHdF42_Y1sWljwjcRY4gUNaJMPVVQ4NNjNxjEYBoaqz1T4G-0KESu9WK7pErdLXupdtG4bIUX1yotqUAx2nwUEGc5QjAAa2XvN_erECMhvUl0ryFhEymRM-OFJJzba6ikGgPIKCvV3w" width="800" height="251"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Вот так выглядит сам веб-интерфейс graylog, где мы уже ищем конкретные логи:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh3.googleusercontent.com%2FMvroZBekyjT2buttPXlO_nNx39TWqTxUdPz1hJr6CMhgAowPtyEoRo9TnD2eih88Z3ZwU0Ew8dD1KSXOuM2sIGzrX9QwD6Vw42f3G1Dg3NaY6otdBxQg4MF2gP461Il8dNu9PiNixWtIyB-IX7phrq-c8DOSF7dZ5MCJv2DuCCPSZEBC4R2fI100Ag" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh3.googleusercontent.com%2FMvroZBekyjT2buttPXlO_nNx39TWqTxUdPz1hJr6CMhgAowPtyEoRo9TnD2eih88Z3ZwU0Ew8dD1KSXOuM2sIGzrX9QwD6Vw42f3G1Dg3NaY6otdBxQg4MF2gP461Il8dNu9PiNixWtIyB-IX7phrq-c8DOSF7dZ5MCJv2DuCCPSZEBC4R2fI100Ag" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Вот так достаточно просто (конечно после нескольких десятков часов исследований) настраивается централизованный сбор логов. Мы пойдем посмотрим, что у нас там в логах, а вы пока stay tuned.&lt;/p&gt;

</description>
      <category>graylog</category>
      <category>infra</category>
      <category>nomad</category>
      <category>traefik</category>
    </item>
    <item>
      <title>Использование Scientist для рефакторинга критических участков Ruby on Rails приложения</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Fri, 30 Sep 2022 12:19:06 +0000</pubDate>
      <link>https://dev.to/rnds/ispolzovaniie-scientist-dlia-riefaktoringha-kritichieskikh-uchastkov-ruby-on-rails-prilozhieniia-3ckl</link>
      <guid>https://dev.to/rnds/ispolzovaniie-scientist-dlia-riefaktoringha-kritichieskikh-uchastkov-ruby-on-rails-prilozhieniia-3ckl</guid>
      <description>&lt;p&gt;&lt;a href="https://blog.appsignal.com/2022/05/18/using-scientist-to-refactor-critical-ruby-on-rails-code.html" rel="noopener noreferrer"&gt;Перевод статьи &lt;em&gt;“Using Scientist to Refactor Critical Ruby on Rails Code”&lt;/em&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://blog.appsignal.com/authors/darren-broemmer" rel="noopener noreferrer"&gt;Darren Broemmer&lt;/a&gt; от 18 мая 2022 года.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe73kwndoczkr94whrvp3.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe73kwndoczkr94whrvp3.jpeg" width="695" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Попросите любого программиста просмотреть ключевые участки продуктового кода, и он обязательно укажет на несколько моментов, требующих рефакторинга. Почему же так много плохого, ненадежного или непонятного кода продолжает работать в продуктовом окружении?&lt;/p&gt;

&lt;p&gt;Ответ прост: инженеры боятся его трогать. Задачи для рефакторинга обнаруживаются и добавляются в бэклог, но редко попадают в текущий спринт.&lt;/p&gt;

&lt;p&gt;Для этого есть множество причин. Код мог быть написан программистом, давно покинувшим команду и теперь в нём никто не разбирается. Бывает что этот код имеет решающее значение для бизнеса и из-за этого никто не хочет нести ответственность за потенциальный сбой или потерю дохода.&lt;/p&gt;

&lt;p&gt;В данном посте мы рассмотрим, как можно использовать гем &lt;a href="https://github.com/github/scientist" rel="noopener noreferrer"&gt;Scientist&lt;/a&gt; для уверенной миграции, рефакторинга и изменения критического продуктового Ruby-кода.&lt;/p&gt;

&lt;p&gt;Но сначала вы спросите — а нельзя ли использовать тесты для поиска ошибок?&lt;/p&gt;
&lt;h2&gt;
  
  
  Это ведь то самое для чего нужны тесты в Rails, верно?
&lt;/h2&gt;

&lt;p&gt;И да и нет. Часто бывает трудно получить полную уверенность в новых изменениях до развертывания. Допустим модульные (Unit) и системные тесты проходят. Этого достаточно?&lt;/p&gt;

&lt;p&gt;Реальность такова, что ничто не может заменить реальный мир, то есть продуктовую эксплуатацию. А если качество тестовых данных плохое или тесты отсутствуют? А будет ли новое ПО работать достаточно хорошо, чтобы справляться с нагрузкой?&lt;/p&gt;

&lt;p&gt;Команды с публичными сервисами иногда обнаруживают, что им нужно решать проблемы «совместимости с ошибками» (bugwards compatibility). Когда ошибка существует в рабочей среде некоторое время, клиенты могут обходить её таким способом, который зависит от привычного неправильного поведения. Клиенты часто используют ваш софт неожиданным образом.&lt;/p&gt;
&lt;h2&gt;
  
  
  с Scientist можно следить за правками в в Ruby и Rails сразу в боевом окружении
&lt;/h2&gt;

&lt;p&gt;Если продуктовое окружение это лучшее место, где можно обрести уверенность в своих правках, можно подумать о том, чтобы понаблюдать за поведением кода именно там. Поначалу это может показаться пугающим, ведь идея «тестирования в бою» противоречит классическим методам разработки программного обеспечения.&lt;/p&gt;

&lt;p&gt;Однако хорошая новость заключается в том, что это можно легко и безопасно сделать в Ruby и Rails с помощью гема &lt;a href="https://github.com/github/scientist" rel="noopener noreferrer"&gt;Scientist&lt;/a&gt;. Название утилиты основано на научном методе проведения экспериментов для проверки гипотез. В данном случае наша гипотеза состоит в том, что новый код работает, а его использование это “эксперимент”.&lt;/p&gt;

&lt;p&gt;Причина, по которой мы можем безопасно использовать этот подход, заключается в том, что в экспериментах по-прежнему используется результат существующего кода. Новый код оценивается только в целях наблюдения и сравнения, как на точность, так и на производительность. Мы уменьшаем проблемы с тестовым покрытием, которые обсуждались ранее, оценивая производительность с использованием реальных данных и параметров в боевом окружении. Эксперименты обычно проводят с определённой частотой, чтобы свести к минимуму влияние на производительность. Однако при желании можно оценить и каждый вызов.&lt;/p&gt;

&lt;p&gt;Давайте теперь кратко рассмотрим, как Scientist работает с техникой “&lt;a href="https://martinfowler.com/bliki/BranchByAbstraction.html" rel="noopener noreferrer"&gt;Branch by Abstraction&lt;/a&gt;”.&lt;/p&gt;
&lt;h2&gt;
  
  
  Паттерн “Branch by Abstraction” в геме Scientist
&lt;/h2&gt;

&lt;p&gt;Работа Scientist начинается с паттерна &lt;a href="https://martinfowler.com/bliki/BranchByAbstraction.html" rel="noopener noreferrer"&gt;Branch by Abstraction&lt;/a&gt;, описанного Мартином Фаулером как «постепенное крупномасштабное изменение программной системы».&lt;/p&gt;

&lt;p&gt;Мы вводим уровень абстракции, чтобы изолировать обновляемый код. Этот уровень решает, какую реализацию использовать, чтобы эксперимент был прозрачен для остальной части системы. Данный метод связан с использованием флага (feature flag), который определяет какая из ветвей кода будет исполнена.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.blog/2016-02-03-scientist/" rel="noopener noreferrer"&gt;Гем Scientist, созданный в Github&lt;/a&gt;, реализует этот паттерн с помощью “эксперимента”. Существующий код называется “контрольным”, а новая реализация — “кандидатом”. Обе части кода выполняются в случайном порядке, но клиенту возвращается только результат ”контрольной” части.&lt;/p&gt;
&lt;h2&gt;
  
  
  Использование Scientist для рефакторинга сервиса в Ruby
&lt;/h2&gt;

&lt;p&gt;Рассмотрим &lt;a href="https://github.com/dbroemme/scientist-labtech-example/blob/master/app/helpers/prime_factor_helper.rb#L20" rel="noopener noreferrer"&gt;Ruby-сервис&lt;/a&gt;, который возвращает наибольший простой множитель для заданного числа. Предположим, мы определили способы оптимизации для сокращения необходимого набора кандидатов, что ускорит работу сервиса.&lt;/p&gt;

&lt;p&gt;Однако владельцы сервиса хотят быть уверены, что при оптимизации не вкралась ошибка. Они также хотят увидеть все улучшения производительности. Давайте напишем &lt;a href="https://github.com/dbroemme/scientist-labtech-example/blob/master/app/helpers/prime_factor_helper.rb#L3" rel="noopener noreferrer"&gt;следующий код&lt;/a&gt;, для вызова этого метода:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require 'scientist'

def largest_prime_factor(number)
  science "prime-factors" do |experiment|
    experiment.use { find_largest_prime_factor(number) } # old way
    experiment.try { improved_largest_prime_factor(number) } # new way
  end # returns the control value
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;В этот момент вызывается только выражение &lt;code&gt;use&lt;/code&gt; (контрольное). Но чтобы сделать эксперимент полезным, надо определить пользовательский класс &lt;code&gt;Experiment&lt;/code&gt;, чтобы включить его и опубликовать результаты (в данном случае просто логирование). Scientist генерирует очень полезные данные, но по умолчанию ничего с ними не делает. Эта часть остается на ваше усмотрение.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require 'scientist/experiment'
require 'pp'

class MyExperiment
  include Scientist::Experiment

  attr_accessor :name

  def initialize(name)
    @name = name
  end

  def enabled?
    true
  end

  def raised(operation, error)
    p "Operation '#{operation}' failed with error '#{error.inspect}'"
    super # will re-raise
  end

  def publish(result)
    pp result
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Результаты эксперимента будут зарегистрированы, и мы сможем со временем вносить улучшения на основе анализа. Как только новый код будет соответствовать требованиям и доверие к нему станет высоким, осуществим переход на новую реализацию, просто заменив код делегированием в новую реализацию.&lt;/p&gt;

&lt;h2&gt;
  
  
  LabTech для упрощения использования Scientist в Ruby on Rails
&lt;/h2&gt;

&lt;p&gt;Есть &lt;a href="https://github.com/RealGeeks/lab_tech" rel="noopener noreferrer"&gt;гем LabTech&lt;/a&gt;, который может помочь настроить Scientist в Rails приложении и удобнее обрабатывать результаты.&lt;/p&gt;

&lt;p&gt;Приложения, использующие &lt;code&gt;AppSignal&lt;/code&gt;, могут использовать &lt;a href="https://docs.appsignal.com/ruby/instrumentation/instrumentation.html" rel="noopener noreferrer"&gt;вспомогательный инструментарий&lt;/a&gt; &lt;code&gt;Appsignal.instrument&lt;/code&gt;, чтобы отслеживать, сколько времени требуется для выполнения событий Scientist. Обернув в него код эксперимента, можно увидеть, как события появляются в &lt;code&gt;AppSIgnal&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Теперь вернемся к LabTech — пример ниже просто принимает число для разложения.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl3yvnwno4vnwlw8hhwlq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl3yvnwno4vnwlw8hhwlq.png" width="447" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Приступить к работе легко, если у вас есть доступ к консоли. Сначала надо добавить гем LabTech в Gemfile и запустить &lt;code&gt;bundle install&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gem 'lab_tech'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Результаты и конфигурация эксперимента хранятся в БД, поэтому требуется миграция.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails lab_tech:install:migrations db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Уровень абстракции тот же, за исключением того, что используется модуль LabTech. Полный код доступен на GitHub.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def largest_prime_factor(number)
    LabTech.science "prime-factors" do |experiment|
      ...
    end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;На данный момент эксперимент отключен, поэтому воспользуемся консолью, чтобы включить его во всех случаях или настроить процент исполнения.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bin/rails console
LabTech.enable "prime-factors"
LabTech.enable "prime-factors", percent: 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Теперь можно запустить тесты, и эксперимент будет выполнен. Для текстового представления результатов есть следующие команды в консоли Rails.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LabTech.summarize_results "prime-factors"
LabTech.summarize_errors "prime-factors"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;После нескольких успешных прогонов и одной синтетической ошибки получим пример того, как выглядит сводка результатов. Существует обзор успехов и ошибок, а также диаграмма ASCII, показывающая различия в производительности.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;----------------------------------------------------------------------------
Experiment: prime-factors
----------------------------------------------------------------------------
Earliest results: 2022-04-27T02:42:45Z
Latest result: 2022-05-01T17:27:39Z (5 days)

3 of 4 (75.00%) correct
1 of 4 (25.00%) mismatched

Median time delta: +0.000s (90% of observations between +0.000s and +0.000s)

Speedups (by percentiles):
      0% [· █] +2.4x faster
      5% [· █] +2.4x faster
     10% [· █] +2.4x faster
     15% [· █] +2.4x faster
     20% [· █] +2.4x faster
     25% [· █] +2.4x faster
     30% [· █] +2.4x faster
     35% [· █] +2.4x faster
     40% [· █] +2.4x faster
     45% [· █] +2.4x faster
     50% [· · · · · · ·· · · · · · · · · █ ·· · · · · ·] +2.4x faster
     55% [· █] +2.4x faster
     60% [· █] +2.4x faster
     65% [· █] +2.4x faster
     70% [· █] +6.9x faster
     75% [· █] +6.9x faster
     80% [· █] +6.9x faster
     85% [· █] +6.9x faster
     90% [· █] +6.9x faster
     95% [· █] +6.9x faster
    100% [· █] +6.9x faster
----------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Для лёгкого и удобного анализа результатов есть гем &lt;a href="https://github.com/ankane/blazer" rel="noopener noreferrer"&gt;Blazer&lt;/a&gt;. Он прост в установке и позволяет выполнять SQL-запросы. Запрос здесь показывает, что реализация-кандидат значительно быстрее оригинала.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1g1anthh3w6y17w0ryxw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1g1anthh3w6y17w0ryxw.png" width="800" height="523"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;В примере нашего сервиса, возвращающего наибольший простой делитель, ускорение в улучшенной реализации происходит за счет эвристики, исключающей некоторые возможные факторы, которые необходимо учитывать. Поскольку мы рассматриваем большие числа и находим простой множитель, мы можем прекратить поиск после того, как доберемся до целевого числа, делённого на этот множитель. Новый код добавляет только &lt;a href="https://github.com/dbroemme/scientist-labtech-example/blob/master/app/helpers/prime_factor_helper.rb#L38" rel="noopener noreferrer"&gt;одну инструкцию&lt;/a&gt; для достижения этой цели.&lt;/p&gt;

&lt;p&gt;Мы также можем увидеть сокращение времени выполнения используя Blazer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq9l8lmyh8zn57iw26ua9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq9l8lmyh8zn57iw26ua9.png" width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Варианты использования и ограничения Scientist
&lt;/h2&gt;

&lt;p&gt;Оптимальное использование Scientist включает задачи поиска, вычисления и код, который не имеет побочных эффектов. Код, включающий транзакционные обновления или внешние интеграции, такие как электронная почта, не совсем вписывается в модель, поскольку изменяемое действие выполняется дважды (как в старой, так и в новой реализации).&lt;/p&gt;

&lt;p&gt;Это не тривиальное ограничение. Оно блокирует несколько вариантов использования. Однако есть обходные пути, если эксперимент очень важен. Стоит подумать имеют ли значение побочные эффекты или дублирование или они не являются проблемой. Например, в некоторых случаях может не иметь значения, будут ли отправлены два электронных письма во время оценки. Другой вариант — сделать так, чтобы новый код определял результат, но не сохранял его. Это не позволит сравнить производительность, но позволит проверить точность.&lt;/p&gt;

&lt;p&gt;Другие ограничения проистекают из того, что Scientist фокусируется на возвращаемых значениях. В некоторых случаях действительные результаты могут отличаться с течением времени, независимо от того, включают ли они просто временные метки в ответ или изменяются сторонние факторы. Во многих случаях мы можем написать пользовательскую логику сравнения в эксперименте, чтобы проверить точность, выходящую за рамки обычного сравнения строк.&lt;/p&gt;

&lt;p&gt;Наконец, ограничение LabTech заключается в том, что он не был портирован на Rails 7 на момент написания.&lt;/p&gt;

&lt;h2&gt;
  
  
  Лучшие практики для эффективных Scientist-экспериментов в Rails
&lt;/h2&gt;

&lt;p&gt;При проведении экспериментов стоит учитывать следующие моменты:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;В Rails-проектах Scientist может быть настроен либо в инициализаторе, либо в оболочке, такой как гем Rails LabTech. Большинство приложений Rails уже имеют базу данных, поэтому LabTech использует ActiveRecord для хранения результатов.&lt;/li&gt;
&lt;li&gt;Чтобы не замедлять разработку и тестирование, включайте эксперимент только в промежуточной (staging) и продуктовой средах.&lt;/li&gt;
&lt;li&gt;Чтобы свести к минимуму любое потенциальное влияние на продуктовую среду, запускайте эксперимент только для некоторого процента запросов. LabTech поддерживает это из коробки как необязательный параметр при включении эксперимента (изначально он отключен по умолчанию). Используя чистый Scientist, эту логику легко реализовать через &lt;code&gt;enabled?&lt;/code&gt; метод.&lt;/li&gt;
&lt;li&gt;Некоторая логика требует больших ресурсов, поэтому хорошей отправной точкой может быть низкая частота дискретизации. По мере того, как вы обретете уверенность в результатах, увеличивайте процент оцениваемых запросов.&lt;/li&gt;
&lt;li&gt;Вы можете добавить атрибуты контекста, чтобы получить максимальную отдачу от результатов. В качестве контекста эксперимента может быть задан хэш с параметрами, который затем становится доступным в опубликованных результатах, например:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;experiment.context :user =&amp;gt; user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Подводя итоги: наблюдайте и контролируйте свое приложение Ruby с помощью Scientist
&lt;/h2&gt;

&lt;p&gt;В этом посте мы рассмотрели, как использовать гем Scientist для изменения, переноса и рефакторинга кода Ruby в продуктовой среде.&lt;/p&gt;

&lt;p&gt;Мы рассмотрели место Scientist в паттерне Branch by Abstraction, и погрузились в рефакторинг. Затем увидели, как LabTech помогает со сбором результатов и конфигурацией Scientist.&lt;/p&gt;

&lt;p&gt;Мы коснулись некоторых ограничений Scientist, прежде чем, наконец, изложить несколько лучших практик.&lt;/p&gt;

&lt;p&gt;Вам необходимо наблюдать и контролировать то, что происходит в системе. Интегрируйте Scientist в процесс разработки, чтобы с большей уверенностью вносить критические изменения в код Ruby.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.appsignal.com/2022/05/18/using-scientist-to-refactor-critical-ruby-on-rails-code.html" rel="noopener noreferrer"&gt;Перевод статьи &lt;em&gt;“Using Scientist to Refactor Critical Ruby on Rails Code”&lt;/em&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://blog.appsignal.com/authors/darren-broemmer" rel="noopener noreferrer"&gt;Darren Broemmer&lt;/a&gt; от 18 мая 2022 года.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
    </item>
    <item>
      <title>FTP? Нет, не слышал</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Tue, 20 Sep 2022 06:28:10 +0000</pubDate>
      <link>https://dev.to/rnds/ftp-niet-nie-slyshal-503m</link>
      <guid>https://dev.to/rnds/ftp-niet-nie-slyshal-503m</guid>
      <description>&lt;p&gt;Сейчас будет немного боли и радости от победы над &lt;a href="https://ru.wikipedia.org/wiki/FTP" rel="noopener noreferrer"&gt;FTP&lt;/a&gt;. Мы много и упорно работаем со &lt;a href="https://ru.wikipedia.org/wiki/%D0%A1%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0_%D0%BC%D0%B5%D0%B6%D0%B2%D0%B5%D0%B4%D0%BE%D0%BC%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D0%BE%D0%B3%D0%BE_%D1%8D%D0%BB%D0%B5%D0%BA%D1%82%D1%80%D0%BE%D0%BD%D0%BD%D0%BE%D0%B3%D0%BE_%D0%B2%D0%B7%D0%B0%D0%B8%D0%BC%D0%BE%D0%B4%D0%B5%D0%B9%D1%81%D1%82%D0%B2%D0%B8%D1%8F" rel="noopener noreferrer"&gt;СМЭВ3&lt;/a&gt;. Никакой магии: обычный SOAP поверх HTTP, быстро, надёжно - все банки и гос. учреждения знают, как это делается. И вдруг (никогда такого не было, и вот опять) оказалось, что нам надо забирать большие файлы с FTP, предоставляемого СМЭВ.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Дисклеймер: постарайтесь воздерживаться от использования FTP - это технология немного устарела и не отвечает современным требованиям удобства и безопасности.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Тем не менее, надо - значит надо. СМЭВ располагается в защищенной инфраструктуре электронного правительства, куда мы ходим через шлюзы, расположенные в определённой инфраструктуре. В этих шлюзах нет ничего необычного - HTTP, балансировка, отказоустойчивость через избыточность.  &lt;/p&gt;

&lt;h2&gt;
  
  
  В чём проблема?
&lt;/h2&gt;

&lt;p&gt;Шлюзы есть, прокси есть, с чего бы нам бояться &lt;a href="https://ru.wikipedia.org/wiki/FTP" rel="noopener noreferrer"&gt;FTP&lt;/a&gt;? Оказывается, бояться есть с чего - FTP использует не один порт, а множество динамически открываемых, причём есть особенный режим (active), когда сервер инициирует соединение к клиенту - это еще со стародавних времён, когда никакого NAT не было. &lt;/p&gt;

&lt;p&gt;В результате мы не можем работать с этим FTP "по простому" - нам требуется как-то обеспечить сетевую связность и наши любимые и привычные инструменты нам в этом помочь не могут 😠 . Надо думать.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;FTP (англ. File Transfer Protocol) — протокол передачи файлов по сети, появившийся в 1971 году задолго до HTTP и даже до TCP/IP, благодаря чему является одним из старейших прикладных протоколов. Изначально FTP работал поверх протокола NCP, на сегодняшний день широко используется для распространения ПО и доступа к удалённым хостам. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Дополнительной особенностью в нашем случае является не просто наличие шлюза, через который необходимо обращаться в СМЭВ, а целых 2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gateway 1 - внутри облака&lt;/li&gt;
&lt;li&gt;Gateway 2 - в специальной инфраструктуре с сетевой связностью со СМЭВ &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;вот так выглядит наш каскад проксей:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdfy0d3uo0ucyhrns2w7k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdfy0d3uo0ucyhrns2w7k.png" alt="proxy cascade" width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Решение
&lt;/h2&gt;

&lt;p&gt;Если есть проблема, то должно быть и решение. Так и оказалось - есть несколько (наверное даже много) программных продуктов, связанных с проксированием FTP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://3proxy.ru/" rel="noopener noreferrer"&gt;3proxy&lt;/a&gt; - целый набор проксей. Прочитали доку, запустили - не завелось.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://en.wikipedia.org/wiki/Squid_(software)" rel="noopener noreferrer"&gt;Squid&lt;/a&gt; - большой, надёжный, кеширующий прокси, на все случаи жизни. Для нас оказался слишком избыточен в настройке. И в контексте FTP практически нет документации.&lt;/li&gt;
&lt;li&gt;
&lt;a href="http://tdkare.ru/sysadmin/index.php/Ftp-proxy" rel="noopener noreferrer"&gt;ftp-proxy&lt;/a&gt; (ftpproxy) - стандартный пакет в дистрибутиве Debian/Ubuntu, и он взлетел со свитом.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Поскольку всё наше облако оркестрируется с помощью &lt;a href="https://www.nomadproject.io/" rel="noopener noreferrer"&gt;Nomad&lt;/a&gt;, то и этот компонент мы завернули docker.&lt;/p&gt;

&lt;p&gt;Dockerfile&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; debian:bullseye-slim&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update 
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        ftp-proxy gettext

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /tmp/logs/
&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="s"&gt; ./ftp-proxy.conf.in /tmp/ftp-proxy.conf.in&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; DESTINATION_ADDRESS=127.0.0.1&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; DESTINATION_PORT=21&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PROXY_PORT=21&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PROXY_LOG_LEVEL=DBG&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 21&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; envsubst &amp;lt; /tmp/ftp-proxy.conf.in &amp;gt; /etc/proxy-suite/ftp-proxy.conf &amp;amp;&amp;amp; exec /usr/sbin/ftp-proxy -n -d &lt;/span&gt;
ftp-proxy.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;В конфиге можно задать миллион других настроек, но в нашем случае достаточно только этого:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[-Global-]

DestinationAddress      ${DESTINATION_ADDRESS}
DestinationPort         ${DESTINATION_PORT}

Port                    ${PROXY_PORT}

LogDestination          | cat
LogLevel                ${PROXY_LOG_LEVEL}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  nomad
&lt;/h2&gt;

&lt;p&gt;Мы катаем джобы с помощью ансибла и используем его шаблонизацию, так что тут я приведу уже финальный результат препроцессинга:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"instance"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="s2"&gt;"ftp-proxy-prod"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;datacenters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"zone-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"zone-b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"zone-c"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;namespace&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt;

  &lt;span class="nx"&gt;constraint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;attribute&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${meta.gate}"&lt;/span&gt;
    &lt;span class="nx"&gt;value&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;max_parallel&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="nx"&gt;health_check&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"checks"&lt;/span&gt;
    &lt;span class="nx"&gt;min_healthy_time&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10s"&lt;/span&gt;
    &lt;span class="nx"&gt;healthy_deadline&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"3m"&lt;/span&gt;
    &lt;span class="nx"&gt;progress_deadline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"6m"&lt;/span&gt;
    &lt;span class="nx"&gt;auto_revert&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;auto_promote&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="nx"&gt;canary&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"system"&lt;/span&gt;

  &lt;span class="nx"&gt;group&lt;/span&gt; &lt;span class="s2"&gt;"ftp-proxy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nx"&gt;network&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="s2"&gt;"ftp"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;static&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;
        &lt;span class="nx"&gt;host_network&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"private"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; 
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="s2"&gt;"ftp-proxy-prod"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;driver&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"docker"&lt;/span&gt;
      &lt;span class="nx"&gt;kill_timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"30s"&lt;/span&gt;
      &lt;span class="nx"&gt;leader&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;DESTINATION_ADDRESS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.34"&lt;/span&gt;
        &lt;span class="nx"&gt;DESTINATION_PORT&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"8878"&lt;/span&gt;
        &lt;span class="nx"&gt;PROXY_PORT&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"21"&lt;/span&gt;
        &lt;span class="nx"&gt;PROXY_LOG_LEVEL&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DBG"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;restart&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;attempts&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
        &lt;span class="nx"&gt;interval&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"5m"&lt;/span&gt;
        &lt;span class="nx"&gt;delay&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"15s"&lt;/span&gt;
        &lt;span class="nx"&gt;mode&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"fail"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YOURIMAGE:latest"&lt;/span&gt;
        &lt;span class="nx"&gt;force_pull&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="nx"&gt;network_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"host"&lt;/span&gt;

        &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"USER"&lt;/span&gt;
          &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PASS"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-ftp"&lt;/span&gt;
        &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"ftp-proxy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ftp"&lt;/span&gt;

        &lt;span class="nx"&gt;check&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
          &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ftp"&lt;/span&gt;
          &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2s"&lt;/span&gt;
          &lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"5s"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;        

        &lt;span class="nx"&gt;check_restart&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
          &lt;span class="nx"&gt;grace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10s"&lt;/span&gt;
          &lt;span class="nx"&gt;ignore_warnings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;        
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;logs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;max_files&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
        &lt;span class="nx"&gt;max_file_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;memory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
        &lt;span class="nx"&gt;memory_max&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
        &lt;span class="nx"&gt;cpu&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nomad job run &lt;span class="nt"&gt;-detach&lt;/span&gt; ftp-proxy.hcl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Конец
&lt;/h2&gt;

&lt;p&gt;Всем спасибо - всё работает, но если у кого-то есть замечания, связанные с данным решением - мы с огромным интересом их рассмотрим 🤞.&lt;/p&gt;

</description>
      <category>nomad</category>
      <category>ftp</category>
      <category>docker</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Rapidity: распределённый rate limiting</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Wed, 17 Aug 2022 05:14:09 +0000</pubDate>
      <link>https://dev.to/rnds/rapidity-raspriedielionnyi-rate-limiting-48g4</link>
      <guid>https://dev.to/rnds/rapidity-raspriedielionnyi-rate-limiting-48g4</guid>
      <description>&lt;p&gt;Когда ваш продукт начинает активно использоваться, то перед вами обязательно встаёт вопрос масштабирования, а вслед за ним и проблема ограничения доступа к чему-нибудь: &lt;a href="https://en.wikipedia.org/wiki/Rate_limiting" rel="noopener noreferrer"&gt;Rate Limiting&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;RNDSOFT не исключение, поэтому в этой статье мы расскажем небольшую историю и поделимся своим инструментом: &lt;a href="https://blog.rnds.pro/030-rapidity?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=rnds" rel="noopener noreferrer"&gt;https://blog.rnds.pro/030-rapidity&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Furatxhkufh5ygl3n72pj.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Furatxhkufh5ygl3n72pj.jpeg" alt="rapidity" width="695" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>distributedsystems</category>
      <category>microservices</category>
    </item>
    <item>
      <title>Rails 7: три мощных ответа JavaScript’у в 2021+</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Wed, 17 Aug 2022 05:01:24 +0000</pubDate>
      <link>https://dev.to/rnds/rails-7-tri-moshchnykh-otvieta-javascriptu-v-2021-50oi</link>
      <guid>https://dev.to/rnds/rails-7-tri-moshchnykh-otvieta-javascriptu-v-2021-50oi</guid>
      <description>&lt;p&gt;&lt;a href="https://blog.rnds.pro/026-three-great-answers?utm_source=devto&amp;amp;utm_medium=post" rel="noopener noreferrer"&gt;https://blog.rnds.pro/026-three-great-answers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Перевод статьи "Rails 7 will have three great answers to JavaScript in 2021+"&lt;/em&gt;&lt;br&gt;
&lt;em&gt;David Heinemeier Hansson (DHH) от 6 сентября 2021 года.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1fkftwj82fdawp5vouog.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1fkftwj82fdawp5vouog.jpeg" alt="rails 7" width="695" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>webdev</category>
      <category>r</category>
    </item>
    <item>
      <title>Вы хочете песен? Их есть у меня! (Poison Message #2)</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Wed, 17 Aug 2022 04:54:00 +0000</pubDate>
      <link>https://dev.to/rnds/vy-khochietie-piesien-ikh-iest-u-mienia-poison-message-2-d0m</link>
      <guid>https://dev.to/rnds/vy-khochietie-piesien-ikh-iest-u-mienia-poison-message-2-d0m</guid>
      <description>&lt;p&gt;Самое время рассмотреть “достаточно хороший” алгоритм для борьбы с Poison Message. Здесь будет уже специфика RabbitMQ и к Apache Kafka она не применима, точнее применима только частично - но это уже совсем другая история. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;В &lt;a href="https://blog.rnds.pro/018-posion1?utm_source=devto&amp;amp;utm_medium=post" rel="noopener noreferrer"&gt;первой части мы разобрали несколько примеров&lt;/a&gt; и сформулировали проблему Poison Message, здесь же рассмотрим сам алгоритм её решения.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://blog.rnds.pro/019-poison2?utm_source=devto&amp;amp;utm_medium=post" rel="noopener noreferrer"&gt;https://blog.rnds.pro/019-poison2&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftb1h6qlxw7s56y08os3y.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftb1h6qlxw7s56y08os3y.jpeg" alt="Poison Message" width="695" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rabbitmq</category>
      <category>microservices</category>
      <category>eventdriven</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Чистка build-агентов Gitlab</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Thu, 11 Aug 2022 09:52:28 +0000</pubDate>
      <link>https://dev.to/rnds/chistka-build-aghientov-gitlab-2lil</link>
      <guid>https://dev.to/rnds/chistka-build-aghientov-gitlab-2lil</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqe6lcuqnv4lcxaydvacz.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqe6lcuqnv4lcxaydvacz.jpeg" width="695" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Оригинал: &lt;a href="https://blog.rnds.pro/030-gitlab-janitor?utm_source=devto&amp;amp;utm_medium=feed_rss&amp;amp;utm_campaign=rnds" rel="noopener noreferrer"&gt;https://blog.rnds.pro/030-gitlab-janitor&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Спешим поделиться с вами нашим инструментом для поддержания чистоты и порядка на наших (RNDSOFT) сборочных серверах &lt;a href="https://github.com/RND-SOFT/gitlab-janitor" rel="noopener noreferrer"&gt;gitlab-janitor&lt;/a&gt;. О том, как мы к нему пришли, и каков первый опыт - далее по тексту.&lt;/p&gt;

&lt;p&gt;С каждым годом инфраструктура RNDSOFT, обеспечивающая процессы CI/CD, растёт. Появляются новые проекты, усложняются процессы (pipelines) сборки и тестирования, растет количество сборщиков (build agents), и всё это приносит дополнительные накладные расходы. Сегодня мы рассмотрим конкретную проблему - чистку/освобождение ресурсов на самих сборочных серверах. Мы используем Gitlab и несколько &lt;a href="https://docs.gitlab.com/runner/" rel="noopener noreferrer"&gt;Gitlab runners&lt;/a&gt; с докером (&lt;a href="https://docs.gitlab.com/runner/executors/docker.html" rel="noopener noreferrer"&gt;docker executor&lt;/a&gt;) под капотом. Вот с проблем и начнём.&lt;/p&gt;

&lt;h2&gt;
  
  
  Проблемы
&lt;/h2&gt;

&lt;p&gt;Вся терминология будет опираться на &lt;a href="https://docs.gitlab.com/" rel="noopener noreferrer"&gt;Gitlab&lt;/a&gt;, но всё это применимо и к другим решениям, опирающимся на docker.&lt;/p&gt;

&lt;p&gt;Типовой процесс сборки и доставки состоит из 4х больших этапов (stages), по несколько задач (jobs) в каждом:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;сборка docker-образа (обычно одного на проект, но бывает и больше).&lt;/li&gt;
&lt;li&gt;тестирование. Тут мы используем подход, когда тестируем не сам код, а весь контейнер. Конечно, есть юнит-тесты, интеграционные, графические и прочие, но запускаем мы их непосредственно внутри боевого контейнера. А что? У нас ruby - можем себе позволить 💎 :)&lt;/li&gt;
&lt;li&gt;тэгирование (image promoting). Этап в зависимости от результатов тестов и QA перетегируется во что-то вроде &lt;code&gt;stable&lt;/code&gt; или &lt;code&gt;release&lt;/code&gt;, или как-то еще, в зависимости от конкретного проекта, команды и принятых процессов.&lt;/li&gt;
&lt;li&gt;доставка (deploy). Тут всё как обычно - дев/тест стенды, QA-стенды, динамические стенды для &lt;a href="https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html" rel="noopener noreferrer"&gt;MR&lt;/a&gt; и всякое разное вроде документации.

&lt;p id="kswC"&gt;Давно хочу написать отдельно про процесс image promotiong, про то, как мы таскаем образы между стадиями, но руки не доходят. Если кого сильно интересует - пишите в комментариях, и мне придётся найти в себе силы :)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;В результате описанных выше процессов (особенно тестирования) на сборщиках остаётся много мусора, именно это является проблемой. Далее по порядку.&lt;/p&gt;

&lt;h3&gt;
  
  
  Подвисшие контейнеры (dangling containers)
&lt;/h3&gt;

&lt;p&gt;Тут речь, конечно, идёт не о повисании ПО внутри контейнера, а о самих "ненужных" контейнерах. Например, для графических тестов поднимается целый стек: Postgres, Chrome, Selenium, само приложение, контейнер, из которого запускаются тесты, Redis и бог еще знает что. Если весь процесс (pipeline) или непосредственно задача (job) будут прерваны, то никто не погасит за нас эти контейнеры, и они продолжат висеть и потреблять ресурсы. Проблема!&lt;/p&gt;

&lt;h3&gt;
  
  
  Ненужные образы
&lt;/h3&gt;

&lt;p&gt;В процессе сборки имя образа претерпевает примерно следующие изменения: image:$SHA -&amp;gt; image:stable -&amp;gt; image:release. Конечно, это примерно, шагов может больше или именования другие, но сути это не меняет. При последовательных комитах &lt;del&gt;сратые&lt;/del&gt; старые образы остаются лежать мёртвым грузом.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;p id="LCgm"&amp;gt;В день мы генерим примерно 75GB образов. ~25GB на каждом из трех сборщиков. И это летом, когда многие в отпуске :)&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Ситуация усугубляется тем, что один и тот же образ находится (с максимальной вероятностью) сразу на всех сборщиках, поскольку разные типы тестов запускаются параллельно и попадают на все сборщики и занимают x3 места. Проблема! Дважды проблема, поскольку с образами всё не так просто и очень интересно - об этом ниже.&lt;/p&gt;

&lt;h3&gt;
  
  
  Безымянные и кеширующие тома (unnamed and cache volumes)
&lt;/h3&gt;

&lt;p&gt;Многие контейнеры при создании аллоцируют в докере временные анонимные тома/диски (мы же всегда про докер говорим, верно?) и оставляют их после себя. Также сам Gitlab, если вы не используете распределённые кеш (shared cache) &lt;a href="https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnerscaches3-section" rel="noopener noreferrer"&gt;на базе S3&lt;/a&gt;, всё равно создаёт кеш-тома, &lt;a href="https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersdocker-section" rel="noopener noreferrer"&gt;если ему явно не запретить это делать&lt;/a&gt; через &lt;code&gt;disable_cache&lt;/code&gt;. Проблема!&lt;/p&gt;




&lt;p&gt;Конечно, эти проблемы не простой "мусор" - старые образы могут ускорить последующую сборку, кеш-тома также теоретически могут ускорять сборку, так что они не совсем бесполезны. Однако, проблемы ускорения и кеширования мы решаем немного иначе - с помощью определённых схем именования образов, используя buildkit, который умеет подтягивать кешированные слои прямо из реестра образов (в нашем случае harbor) и пр.&lt;/p&gt;

&lt;h2&gt;
  
  
  Что же делать?
&lt;/h2&gt;

&lt;p&gt;Проблемы появляются не сразу. Когда-то у нас был один сборщик, там вообще не было проблем с перетягиванием образов между этапами, а образы чистились через &lt;code&gt;docker rmi&lt;/code&gt; на последнем шаге процесса. Когда сборщиков стало больше, и на них докинули ресурсы, пришлось немного усложнить схему - сначала были bash-скрипты, которые по расписанию удаляли образы по шаблону имени. Затем периодически стали использовать &lt;code&gt;docker system prune&lt;/code&gt;. Но эти решения очень негибкие, плохо поддерживаются и масштабируются и обладают фундаментальной проблемой - частыми кеш-промахами (cache miss), что периодически сильно тормозило процессы (pipelines). И вот однажды терпеть это стало невозможно 😜 :)))&lt;/p&gt;

&lt;p&gt;Нам нужно хорошее решение, желательно с баристой и массажисткой! Встречаем - &lt;a href="https://github.com/RND-SOFT/gitlab-janitor" rel="noopener noreferrer"&gt;gitlab-janitor&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Gitlab-janitor
&lt;/h2&gt;

&lt;p&gt;Основными задачами были:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;консолидация всех чисток сборщика в одном месте;&lt;/li&gt;
&lt;li&gt;уменьшение кеш-промахов, насколько это возможно;&lt;/li&gt;
&lt;li&gt;простота работы.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Для чистки места на сборщиках &lt;a href="https://gitlab.com/gitlab-org/gitlab-runner-docker-cleanup" rel="noopener noreferrer"&gt;уже есть проект gitlab-runner-docker-cleanup&lt;/a&gt;, он и явился идейным вдохновителем нашей утилиты, но, к сожалению, не выполнял всех необходимых нам функций.&lt;/p&gt;

&lt;p&gt;Написав своё решение, мы решили опубликовать его в открытом виде - и &lt;a href="https://github.com/RND-SOFT/gitlab-janitor" rel="noopener noreferrer"&gt;зеркало на github&lt;/a&gt;, и &lt;a href="https://hub.docker.com/r/rnds/gitlab-janitor" rel="noopener noreferrer"&gt;образы на docker-hub&lt;/a&gt;, и даже документация с примерами. Ну и не мог я удержаться от разнообразных бейджиков :)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftiryls98hvqqnv4pigr6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftiryls98hvqqnv4pigr6.png" width="674" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Проще всего запускать его в докере на каждом сборщике (для этого мы используем &lt;a href="https://www.nomadproject.io/" rel="noopener noreferrer"&gt;nomad&lt;/a&gt; 🥂 ):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /persistent/janitor:/store \
  -e REMOVE=true \
  -e INCLUDE="*integr*, *units*" \
  -e EXCLUDE="*gitlab*" \
  -e CONTAINER_DEADLINE="1h10m" \
  -e VOLUME_DEADLINE="4d" \
  -e IMAGE_DEADLINE="4d" \
  -e CACHE_SIZE="10G" \
  -e IMAGE_STORE="/store/images.txt" \
  rnds/gitlab-janitor:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Заметили &lt;code&gt;IMAGE_STORE&lt;/code&gt;? С остальными параметрами всё просто - шаблоны для имён образов, томов, сроки хранения, а вот для образов всё гораздо интереснее!&lt;/p&gt;

&lt;p&gt;Докер не сохраняет в своём локальном хранилище никакой информации о времени загрузки образа, а только обычную дату создания образа. В результате нет возможности определить, какой образ очень нужен и часто используется (хоть и создан давно), а какой свеженький, но больше никогда и никому не будет нужен.&lt;/p&gt;

&lt;p&gt;Для решения этой проблемы в gitlab-janitor сделан простенький механизм - когда образ встречается первый раз, он сохраняется в файл с временной меткой - это считается датой появления. Когда подходит срок - образ удаляется. Однако, если gitlab-janitor увидит созданный контейнер из этого образа - то временная метка в этом файле сбрасывается. Именно для этого и используется параметр &lt;code&gt;IMAGE_STORE&lt;/code&gt;. Данный файл можно монтировать с хост-машины, а можно и нет - в этом случае история образов будет теряться при пересоздании/обновлении контейнера-чистильщика:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;selenium/standalone-chrome:latest sha256:c01aea5eb0bf279df5f745e3c277e30d7d9c81f15b9d1d4e829f1075c31ed5b1 1660205645
postgres:10-alpine sha256:d7023df56cb7cbe961f0c402888f7170397c98c099fb76fbe16b5442a236ad51 1660205040
redis:latest sha256:3edbb69f9a493835e66a0f0138bed01075d8f4c2697baedd29111d667e1992b4 1660205645
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;С остальными параметрами, документацией и примерами можно ознакомиться на гитхабе.&lt;/p&gt;

&lt;h2&gt;
  
  
  Результаты
&lt;/h2&gt;

&lt;p&gt;Вот и подъехали результаты объективного контроля за работой gitlab-janitor на трех наших сборщиках:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frj06s6qygsa5amr3c0i5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frj06s6qygsa5amr3c0i5.png" width="799" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Все чистки с помощью &lt;code&gt;cron+bash&lt;/code&gt;, &lt;code&gt;docker prune&lt;/code&gt; и пр. выключены. Параметры, оптимально подходящие под наши процессы, мы еще подбираем (и будем подбирать), но уже видно, что процесс вошел в стабильную фазу, и наш чистильщик выполняет свою работу!&lt;/p&gt;

</description>
      <category>infra</category>
      <category>gitlab</category>
      <category>cleaner</category>
      <category>devops</category>
    </item>
    <item>
      <title>Dev containers: вскрытие</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Mon, 04 Jul 2022 08:31:03 +0000</pubDate>
      <link>https://dev.to/rnds/dev-containers-vskrytiie-20o3</link>
      <guid>https://dev.to/rnds/dev-containers-vskrytiie-20o3</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frn4enyt0s67zrlt1vg8z.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frn4enyt0s67zrlt1vg8z.jpeg" width="695" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Оригинал: &lt;a href="https://blog.rnds.pro/029-dev-containers-uncovering?utm_source=devto&amp;amp;utm_medium=feed_rss&amp;amp;utm_campaign=rnds" rel="noopener noreferrer"&gt;https://blog.rnds.pro/029-dev-containers-uncovering&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;В этой статье хотелось бы рассказать про разработку внутри docker-контейнера: зачем это нужно, что предоставляет для этого замечательная IDE VSCode, и как это работает.&lt;/p&gt;

&lt;p&gt;Итак, docker - это не только отличная технология для контейнеризации и деплоя приложений, но и возможность развернуть специфическую среду разработки в одно касание. &lt;/p&gt;

&lt;p&gt;Для тех, кто ещё (внезапно!) не знаком с Docker, очень рекомендую сразу пойти и ознакомиться с этой технологией (&lt;a href="https://en.wikipedia.org/wiki/Docker_(software)" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/Docker_(software)&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Для чего же это нужно?
&lt;/h2&gt;

&lt;p&gt;Давайте рассмотрим существующие проблемы, которые, как правило, возникают при работе над проектом.&lt;/p&gt;

&lt;h3&gt;
  
  
  Проблема 1
&lt;/h3&gt;

&lt;p&gt;Вы - &lt;strong&gt;новый разработчик в компании&lt;/strong&gt; , Вам дали свежеустановленный ПК, дали доступ в репозиторий проекта, с которым придется иметь дело. Предположим, что этот проект на Rails. Для запуска проекта необходимо:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;установить системные dev пакеты&lt;/li&gt;
&lt;li&gt;установить rvm&lt;/li&gt;
&lt;li&gt;установить правильную версию ruby, bundle&lt;/li&gt;
&lt;li&gt;установить nvm&lt;/li&gt;
&lt;li&gt;установить node&lt;/li&gt;
&lt;li&gt;установить и настроить БД, например, Postgres&lt;/li&gt;
&lt;li&gt;возможно, что-то ещё, что требует проект (Redis, Consul и т.д.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;В процессе установки могут возникнуть следующие проблемы:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;у вас Windows: это сразу +100 к сложности и боли&lt;/li&gt;
&lt;li&gt;у вас слишком новая версия операционки, а в проекте используются устаревшие библиотеки, и при установке ruby пакетов, либо scss (когда же его наконец перепишут на JS) могут возникнуть ошибки компиляции&lt;/li&gt;
&lt;li&gt;возможно, необходимы какие-то ещё дополнительные шаги, которые забыли описать в README, например, это может быть установка корневых сертификатов, прописывание каких-то переменных среды&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;В целом видно, что процесс первого запуска проекта непростой. Скорее всего, его придется повторить, когда захочется поработать с домашнего компьютера, а там как раз больше вероятность встретить Windows =)&lt;/p&gt;

&lt;h3&gt;
  
  
  Проблема 2
&lt;/h3&gt;

&lt;p&gt;Вы - опытный разработчик и уже давно работаете в компании, &lt;strong&gt;у вас много проектов&lt;/strong&gt; , на разных языках, фреймворках и приходится часто переключаться. Согласитесь, не очень хочется проводить существенное время на развертывание окружения, вспоминая, как это делать.&lt;/p&gt;

&lt;h3&gt;
  
  
  Проблема 3
&lt;/h3&gt;

&lt;p&gt;Бывает так, что &lt;strong&gt;проект уже очень старый&lt;/strong&gt; , порос мхом и обновлять его нерентабельно, но поддерживать надо. Там всё очень старое, и на своей современной операционке это просто не запустить.&lt;/p&gt;

&lt;h3&gt;
  
  
  Проблема 4
&lt;/h3&gt;

&lt;p&gt;Ещё одна проблема - это &lt;strong&gt;замусоренность хостовой системы&lt;/strong&gt; , когда мы долго уже работаем, много всего разного поставили себе в систему, у нас появляются странные глюки, а остальные разработчики говорят нам, что &lt;a href="https://wikireality.ru/wiki/%D0%A3%D0%9C%D0%92%D0%A0" rel="noopener noreferrer"&gt;УМВР&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Решение
&lt;/h3&gt;

&lt;p&gt;Итак, эти все проблемы решает &lt;strong&gt;контейнеризированное&lt;/strong&gt; окружение разработки. Мы получаем:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;повторяемость среды разработки&lt;/li&gt;
&lt;li&gt;быстрое развертывание&lt;/li&gt;
&lt;li&gt;независимость от хостовой системы, её типа и версии&lt;/li&gt;
&lt;li&gt;возможность полностью очистить свою систему от ненужных рудиментов старых проектов&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Конечно же есть и недостатки:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Повышается сложность понимания что происходит&lt;/strong&gt;. Надо не забывать, что мы находимся внутри контейнера, и некоторые внешние ресурсы могут быть недоступны, такие как файлы, либо сторонние сервисы, запущенные на localhost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Требуется больше системных ресурсов&lt;/strong&gt;. В основном, это дополнительное пространство на диске под контейнеры и образы docker, а в случае с MacOS ещё и дополнительный расход оперативной памяти под виртуальную машину с докером.&lt;/li&gt;
&lt;li&gt;Необходимо учитывать, что &lt;strong&gt;ID пользователя внутри контейнера может быть другим&lt;/strong&gt; , и может возникнуть проблема с доступом к файлам проекта. Забегая вперед, скажу, что эта проблема вполне решаемая.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Существует множество вариантов реализовать такое контейнеризированное окружение:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;написать Dockerfile с нужной версией ОС, установкой всего необходимого, зайти в контейнер и разрабатывать в VIM.&lt;/li&gt;
&lt;li&gt;можно смонтировать исходники в запущенный контейнер и разрабатывать в своей IDE, а запускать в контейнере. Правда, у IDE скорее всего будут проблемы с дополнением кода.&lt;/li&gt;
&lt;li&gt;можно установить IDE внутрь контейнера и пробросить X-ы с хостовой машины (работает только с Linux).&lt;/li&gt;
&lt;li&gt;можно поставить sshd в контейнер и заходить внутрь по ssh из VSCode, IDEA или любой другой IDE, которая поддерживает удалённую разработку.&lt;/li&gt;
&lt;li&gt;либо воспользоваться специализированным решением, которое нам предлагает VSCode. Предлагаю в этой статье подробно рассмотреть это решение как самое, на мой взгляд, продвинутое из того, что я видел.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  VS Code Dev container
&lt;/h2&gt;

&lt;p&gt;Полная документация находится здесь: &lt;a href="https://code.visualstudio.com/docs/remote/containers" rel="noopener noreferrer"&gt;https://code.visualstudio.com/docs/remote/containers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Вкратце, это работает следующим образом:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;в проект добавляется конфигурация в папке .devcontainer&lt;/li&gt;
&lt;li&gt;при открытии такого проекта VS Code предлагает переоткрыть его внутри контейнера&lt;/li&gt;
&lt;li&gt;если мы соглашаемся, то после непродолжительных магических действий со стороны VS Code проект открывается&lt;/li&gt;
&lt;li&gt;мы оказываемся внутри контейнера и получаем ровно то окружение, которое нам необходимо для разработки&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Вот шаги, которые нужно выполнить для добавления к себе в проект конфигурации:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker уже должен быть установлен&lt;/li&gt;
&lt;li&gt;устанавливаем плагин в VS Code &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers" rel="noopener noreferrer"&gt;https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;добавляем поддержку dev container в наш проект&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Для этого открываем палитру команд в VS Code, выбираем пункт “Remote-Containers: Add Development Configuration Files”.&lt;/p&gt;

&lt;p&gt;Нам предоставляется выбор:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;воспользоваться готовыми конфигурациями (Node.js, Ruby on Rails, C++, Go, PHP и ещё много других)&lt;/li&gt;
&lt;li&gt;добавить существующий в проекте Dockerfile или docker-compose.yml&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Если сильно спешим, то можно воспользоваться готовыми конфигурациями. Но, на мой взгляд, лучше написать свою конфигурацию с нуля, так мы получаем больше контроля над происходящим, и это же так интересно!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Разбираемся в магии&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Отдельно хотелось бы рассмотреть, что же делает VS Code с нашим контейнером, прежде чем его открыть.&lt;/p&gt;

&lt;p&gt;Существует ряд нюансов, на которые следует обратить внимание:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;при монтировании исходников в контейнер ID пользователя и ID группы внутри и снаружи должны совпадать, иначе мы получим в своих исходниках новые файлы, созданные от имени другого пользователя (возможно от рута)&lt;/li&gt;
&lt;li&gt;у Git есть глобальные конфигурационные файлы, в которых задается ряд параметров, таких как имя пользователя, email, тип переноса строк и т.д., желательно, чтобы git внутри контейнера использовал такие же настройки&lt;/li&gt;
&lt;li&gt;также необходимо скопировать некоторые пользовательские настройки для того, чтобы вы чувствовали себя как дома (.ssh/authorized_hosts, gpg конфигурацию)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Именно это делает VS Code при открытии devcontainer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;он изучает версию операционной системы, которая используется в Dockerfile&lt;/li&gt;
&lt;li&gt;оборачивает ваш Dockerfile своим с дополнительными командами по пробросу UID и GID внутрь контейнера&lt;/li&gt;
&lt;li&gt;патчит /etc/passwd, синхронизируя UID и GID внутреннего пользователя с хостовым пользователем&lt;/li&gt;
&lt;li&gt;копирует в него все необходимые конфигурационные файлы из папки пользователя (git, ssh, gpg)&lt;/li&gt;
&lt;li&gt;устанавливает свой бэкенд на ноде (это порядка 450Мб) внутрь контейнера, который будет связываться с хостовым VSCode по рандомному TCP порту&lt;/li&gt;
&lt;li&gt;устанавливает внутрь контейнера все необходимые плагины для VS Code, прописанные в .devcontainer/devcontainer.json#extensions&lt;/li&gt;
&lt;li&gt;монтирует исходники проекта в папку /workspaces&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Вуаля! Проект открыт, и можно кодить как обычно.&lt;/p&gt;

&lt;p&gt;В итоге, чтобы открыть любой проект с поддержкой devcontainer и начать быстро кодить, Вам нужна лишь машина на Linux, Windows, MacOS с установленным Docker и VS Code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Уже не терпится? Попробуем в деле!
&lt;/h2&gt;

&lt;p&gt;Я заготовил пример проекта, который можно открыть в VSCode и посмотреть, как это работает.&lt;/p&gt;

&lt;p&gt;Для этого я форкнул первый найденный проект “Блог” на Rails (&lt;a href="https://github.com/navrocky/rails-blog-sample-devcontainers" rel="noopener noreferrer"&gt;https://github.com/navrocky/rails-blog-sample-devcontainers&lt;/a&gt; ).&lt;/p&gt;

&lt;p&gt;Как нельзя кстати оказалось, что он уже немного “подзасох”, последний коммит 5 лет назад. Соответственно, в те годы люди сидели на Ruby 2.3, Debian 8 (Jessie). Вот и попробуем всё это завести в нашем 2К22.&lt;/p&gt;

&lt;p&gt;Dev container поддерживает два варианта окружения:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;один &lt;strong&gt;Dockerfile&lt;/strong&gt;. Этот вариант проще и подходит, когда нам не нужны дополнительные сервисы для работы, такие как БД.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;docker-compose.yml&lt;/strong&gt;. Этот вариант позволяет нам запустить как само окружение, так и все необходимые дополнительные сервисы. В примере я использовал именно этот вариант, так как нам нужна ещё сконфигурированная база данных.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Так выглядит типичная конфигурация в проекте:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo1v8fbtygkmgjujdz953.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo1v8fbtygkmgjujdz953.png" width="484" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Всё находится внутри папки &lt;strong&gt;.devcontainer&lt;/strong&gt;. Файл &lt;strong&gt;devcontainer.json&lt;/strong&gt; является основным и описывает конфигурацию для плагина VSCode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;devcontainer.json&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    // Имя конфигурации
    "name": "Ruby",

    // указание файла docker-compose.yml и сервиса в нем, 
    // который будет использоваться для разработки
    "dockerComposeFile": "./docker-compose.yml",
    "service": "dev",

    // папка внутри контейнера, в которую монтируются исходники проекта
    "workspaceFolder": "/workspace",

    // действие при завершении работы с проектом
    "shutdownAction": "stopContainer",

    // дополнительные расширения VSCode, которые необходимо установить 
    // внутрь контейнера перед началом работы
    "extensions": [
        "castwide.solargraph", "eamodio.gitlens"
    ],

    // пользователь внутри контейнера, под которым будет происходить вход 
    // в контейнер
    "remoteUser": "user"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Dockerfile&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# берем нужную версию базового дистрибутива. Debian 8 из 2015 года, нам подходит.
FROM debian:8

# устанавливаем все необходимые утилиты
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y gnupg2 ca-certificates curl procps sudo git mc libpq-dev

# добавляем пользователя, под которым будем работать внутри контейнера, заодно даем ему возможность делать sudo
RUN useradd -m -d /home/user -s /bin/bash user &amp;amp;&amp;amp; adduser user sudo &amp;amp;&amp;amp; \
    echo '%sudo ALL=(ALL) NOPASSWD:ALL' &amp;gt;&amp;gt; /etc/sudoers

# дальше все действия производим уже от пользователя
USER user 

# устанавливаем rvm
RUN gpg2 --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB &amp;amp;&amp;amp; \
    curl -ksSL https://get.rvm.io | bash -s stable

# устанавливаем nvm
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

# подгружаем все инициализационные скрипты и переменные среды в bash, чтобы заработал установленный rvm
SHELL ["/bin/bash", "-c", "-l"]

# устанавливаем нужную версию Ruby и Bundler
RUN rvm install ruby-2.4 &amp;amp;&amp;amp; gem install bundler:1.17.3

# создаем пользовательский каталог .bundle, чтобы были правильные права на эту папку при монтировании volume
RUN mkdir -p /home/user/.bundle

# устанавливаем node v5.3.0 и делаем её по умолчанию
RUN source $HOME/.nvm/nvm.sh &amp;amp;&amp;amp; nvm install v5.3.0 &amp;amp;&amp;amp; nvm alias default v5.3.0

# просто висим в бесконечном ожидании из /dev/null, это нужно чтобы контейнер не закрылся после запуска
ENTRYPOINT ["tail", "-f", "/dev/null"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Открываем проект, VSCode предлагает нам открыть его внутри контейнера - соглашаемся:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fstdhyigw3w0kev1bwj4a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fstdhyigw3w0kev1bwj4a.png" width="555" height="246"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ждем некоторое время, пока все образы будут загружены, и будет произведена сборка контейнера:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg9ehyzn8ipnt29anx430.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg9ehyzn8ipnt29anx430.png" width="481" height="83"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Пока ждем, можно нажать show log и посмотреть, что там происходит.&lt;/p&gt;

&lt;p&gt;Всё - проект открыт, и можно приступать к работе:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9nvh6wq57bm6orn29uxz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9nvh6wq57bm6orn29uxz.png" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Выполняем в терминале VSCode следующие команды:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rake db:setup
rails s -b 0.0.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Открываем в браузере наш блог (&lt;a href="http://localhost:3000/blog" rel="noopener noreferrer"&gt;http://localhost:3000/blog&lt;/a&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkb1ho5k2w3ocznpon1bd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkb1ho5k2w3ocznpon1bd.png" width="800" height="621"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Можно зайти в административную панель (&lt;a href="http://localhost:3000/admin" rel="noopener noreferrer"&gt;http://localhost:3000/admin&lt;/a&gt;) и добавить контент. Пользователь и пароль для входа: &lt;strong&gt;&lt;a href="mailto:admin@example.com"&gt;admin@example.com&lt;/a&gt;&lt;/strong&gt; / &lt;strong&gt;123456&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Вот так просто и быстро можно открыть незнакомый проект и приступить к работе!&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  А что же IDEA и её производные, спросите вы?
&lt;/h2&gt;

&lt;p&gt;Там не всё так радужно, но есть свет в конце туннеля.&lt;/p&gt;

&lt;p&gt;Не так давно JetBrains добавила поддержку remote development в свои платные IDE. Community варианты её, к сожалению, не получили. И называется этот продукт JetBrains Gateway (&lt;a href="https://www.jetbrains.com/remote-development/gateway/" rel="noopener noreferrer"&gt;https://www.jetbrains.com/remote-development/gateway/&lt;/a&gt;). Данная штука позволяет зайти на удалённую машину, в нашем случае в контейнер с поднятым ssh демоном, и загрузить туда выбранную вами IDE и запустить её в режиме сервера, а на вашем хосте будет запущен IDE Client, который будет по ssh туннелю общаться с серверной IDE. В целом всё выглядит примерно так же, как и в VS Code, но менее автоматизированно. При открытии проекта необходимо руками поднимать контейнер или docker-compose конфигурацию, настраивать соединение внутри контейнера. Серверная IDE, которая грузится в контейнер, ничем не отличается от десктопной, и в размерах тоже (это примерно 1,5 ГБ), что весьма печально. Интерфейс IDE клиента ощущается тормознее по сравнению с десктопной IDE и местами подглючивает. Но в целом, всё довольно работоспособно.&lt;/p&gt;

&lt;p&gt;Стоит заметить, что эта система пока находится в бете, поэтому держим кулачки и внимательно наблюдаем за дальнейшим развитием событий.&lt;/p&gt;

</description>
      <category>development</category>
      <category>containers</category>
      <category>docker</category>
      <category>vscode</category>
    </item>
  </channel>
</rss>
