<?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: Samoilenko Yuri</title>
    <description>The latest articles on DEV Community by Samoilenko Yuri (@kinnalru).</description>
    <link>https://dev.to/kinnalru</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%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png</url>
      <title>DEV Community: Samoilenko Yuri</title>
      <link>https://dev.to/kinnalru</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kinnalru"/>
    <language>en</language>
    <item>
      <title>Наконец-то GitHub Pages (и GitLab Pages — для симметрии)</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Sun, 02 Aug 2026 12:22:16 +0000</pubDate>
      <link>https://dev.to/kinnalru/nakoniets-to-github-pages-i-gitlab-pages-dlia-simmietrii-18cc</link>
      <guid>https://dev.to/kinnalru/nakoniets-to-github-pages-i-gitlab-pages-dlia-simmietrii-18cc</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%2Fsz5h4oto803lzdq5pwsi.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsz5h4oto803lzdq5pwsi.jpeg" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Давно присматривался к &lt;a href="https://pages.github.com/" rel="noopener noreferrer"&gt;GitHub Pages&lt;/a&gt;, но руки никак не доходили.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Всякие разные Pages
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GitHub Pages&lt;/code&gt; нативно поддерживает &lt;a href="https://jekyllrb.com/" rel="noopener noreferrer"&gt;Jekyll&lt;/a&gt; — пушнул в репозиторий, и сайт сам собрался. Никаких докеров, никаких CI/CD (ну почти), никаких серверов.&lt;/p&gt;

&lt;p&gt;А чтобы было совсем хорошо, решил поддерживать и &lt;code&gt;GitLab Pages&lt;/code&gt; — для симметрии. Чтоб не привязываться к одной платформе и чтоб было куда переключиться, если что. Но тут без CI/CD никак.&lt;/p&gt;

&lt;h3&gt;
  
  
  Структура репозитория и настройки
&lt;/h3&gt;

&lt;p&gt;Репозиторий называется как положено — &lt;code&gt;kinnalru.github.io&lt;/code&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="s2"&gt;"github-pages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;group: :jekyll_plugins&lt;/span&gt;

&lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="ss"&gt;:jekyll_plugins&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"jekyll-feed"&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"jekyll-sitemap"&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;code&gt;github-pages&lt;/code&gt; — это мета-гем, который тянет &lt;code&gt;Jekyll&lt;/code&gt; и все нужные плагины в совместимых версиях. Никаких "а у меня работает", потому что &lt;code&gt;GitHub Pages&lt;/code&gt; собирает ровно тем же набором.&lt;/p&gt;

&lt;p&gt;Заполняем конфигурацию &lt;a href="https://github.com/kinnalru/kinnalru.github.io/blob/master/_config.yml" rel="noopener noreferrer"&gt;_config.yml&lt;/a&gt; где выбираем тему, плагины, и всякие настройки. Тема &lt;a href="https://github.com/jekyll/minima" rel="noopener noreferrer"&gt;minima&lt;/a&gt; — адаптивная под светлую/темную тему. И сразу же в конфиге ссылочки на все свои ресурсы: &lt;strong&gt;GitLab&lt;/strong&gt; , &lt;strong&gt;GitHub&lt;/strong&gt; , &lt;strong&gt;Teletype&lt;/strong&gt; , &lt;strong&gt;Telegram&lt;/strong&gt; , &lt;strong&gt;Dev.to&lt;/strong&gt;. Чтоб гость зашел и сразу понял, где меня искать, а то вдруг нигде раньше не нашел ссылок :)&lt;/p&gt;

&lt;h3&gt;
  
  
  GitLab Pages — симметрия ради симметрии
&lt;/h3&gt;

&lt;p&gt;Для &lt;code&gt;GitLab Pages&lt;/code&gt; пришлось чуть заморочиться. &lt;code&gt;GitHub Pages&lt;/code&gt; собирает &lt;code&gt;Jekyll&lt;/code&gt; сам, а &lt;code&gt;GitLab&lt;/code&gt; — нет, нужен &lt;a href="https://github.com/kinnalru/kinnalru.github.io/blob/master/.gitlab-ci.yml" rel="noopener noreferrer"&gt;.gitlab-ci.yml&lt;/a&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="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby:3.2-slim&lt;/span&gt;

&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apt-get update -y; apt-get install -y build-essential&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bundle&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;JEKYLL_ENV=production bundle exec jekyll build --config _config.yml,_config.gitlab.yml -d ./public/&lt;/span&gt;
  &lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;($CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH) &amp;amp;&amp;amp; ($CI_PIPELINE_SOURCE != "schedule")&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;И отдельный &lt;code&gt;_config.gitlab.yml&lt;/code&gt; — переопределяем только &lt;strong&gt;url&lt;/strong&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="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kinnalru.gitlab.io&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Jekyll&lt;/code&gt; умеет мержить конфиги при сборке: &lt;code&gt;--config _config.yml,_config.gitlab.yml&lt;/code&gt;. Всё, что идет справа, переопределяет то, что слева.&lt;/p&gt;

&lt;h3&gt;
  
  
  Чтоб было "хорошо" — намазал сверху RSS и Sitemap
&lt;/h3&gt;

&lt;p&gt;Просто собирать HTML — это скучно. Мне нужно, чтоб поисковики индексировали, а читатели подписывались. Поэтому сверху еще и:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ставим плагин &lt;code&gt;jekyll-sitemap&lt;/code&gt;, и он автоматически генерирует &lt;code&gt;/sitemap.xml&lt;/code&gt;. Но я не доверяю автоматике до конца, поэтому в &lt;code&gt;_includes/custom-head.html&lt;/code&gt; добавил явную ссылку:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"sitemap"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/xml"&lt;/span&gt; &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"Sitemap"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/sitemap.xml"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;ставим плагин &lt;code&gt;jekyll-feed&lt;/code&gt; — он генерирует базовый &lt;code&gt;/feed.xml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;но я решил, что хочу RSS с полным контентом статей и расширенными настройками, поэтому написал свой шаблон &lt;a href="https://github.com/kinnalru/kinnalru.github.io/blob/master/feed.xml" rel="noopener noreferrer"&gt;feed.xml&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Кастомные шаблоны
&lt;/h3&gt;

&lt;p&gt;Стандартная minima хороша, но мне хотелось, чтоб на каждом посте была ссылка на оригинальную публикацию. Переопределил &lt;code&gt;_layouts/post.html&lt;/code&gt; и &lt;code&gt;_layouts/home.html&lt;/code&gt; — добавил &lt;strong&gt;canonical_url&lt;/strong&gt; и пометку &lt;em&gt;"Моя публикация на ..."&lt;/em&gt;. Теперь зеркало не выглядит как украденный контент, а как полноценный агрегатор.&lt;/p&gt;

&lt;h3&gt;
  
  
  Что в итоге?
&lt;/h3&gt;

&lt;p&gt;В результате мы имеем:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Статический сайт на &lt;code&gt;Jekyll&lt;/code&gt;, который собирается автоматически
&lt;/li&gt;
&lt;li&gt;Зеркало на &lt;code&gt;GitHub Pages&lt;/code&gt; — &lt;a href="https://kinnalru.github.io" rel="noopener noreferrer"&gt;kinnalru.github.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Зеркало на &lt;code&gt;GitLab Pages&lt;/code&gt; — &lt;a href="https://kinnalru.gitlab.io" rel="noopener noreferrer"&gt;kinnalru.gitlab.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RSS&lt;/code&gt; с полным контентом для подписчиков и дальнейшей обработки
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Sitemap&lt;/code&gt; для поисковиков
&lt;/li&gt;
&lt;li&gt;Все ссылки на мои ресурсы в одном месте&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Keep it simple, stupid — но при этом всё должно работать.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;a href="https://teletype.in/@jerry_ru" rel="noopener noreferrer"&gt;Teletype&lt;/a&gt; | &lt;a href="https://t.me/architdev" rel="noopener noreferrer"&gt;Архи DEV&lt;/a&gt; | &lt;a href="https://dev.to/kinnalru"&gt;Dev.to&lt;/a&gt; | &lt;a href="https://github.com/kinnalru" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://kinnalru.blogspot.com" rel="noopener noreferrer"&gt;Blogger&lt;/a&gt;&lt;/p&gt;

</description>
      <category>блог</category>
      <category>synchronization</category>
      <category>jekyll</category>
    </item>
    <item>
      <title>Опять синхронизация, но теперь блогов</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Fri, 31 Jul 2026 13:26:25 +0000</pubDate>
      <link>https://dev.to/kinnalru/opiat-sinkhronizatsiia-no-tiepier-bloghov-460n</link>
      <guid>https://dev.to/kinnalru/opiat-sinkhronizatsiia-no-tiepier-bloghov-460n</guid>
      <description>&lt;p&gt;Есть у меня небольшой фетиш по синхронизации… И в очередной раз, подойдя к проблеме блога, я наконец собрал разрозненные куски в единую систему :)&lt;/p&gt;

&lt;h3&gt;
  
  
  Кто к нам с чем и зачем?
&lt;/h3&gt;

&lt;p&gt;За годы у меня накопилась целая коллекция площадок, куда я (и не только я) что-то публикую:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Личный блог&lt;/strong&gt; на &lt;a href="https://teletype.in/@jerry_ru" rel="noopener noreferrer"&gt;Teletype&lt;/a&gt; — сюда пишу руками&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Корпоративный блог&lt;/strong&gt; &lt;a href="https://blog.rnds.pro/" rel="noopener noreferrer"&gt;blog.rnds.pro&lt;/a&gt; — пишем вместе с коллегами&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev.to&lt;/strong&gt; — личный аккаунт &lt;a href="https://dev.to/kinnalru"&gt;kinnalru&lt;/a&gt; и корпоративная организация &lt;a href="https://dev.to/rnds"&gt;RNDSOFT&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Pages&lt;/strong&gt; — &lt;a href="https://kinnalru.github.io" rel="noopener noreferrer"&gt;kinnalru.github.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blogger (Blogspot)&lt;/strong&gt; — &lt;a href="https://kinnalru.blogspot.com" rel="noopener noreferrer"&gt;kinnalru.blogspot.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Раньше тут обитал ещё и &lt;del&gt;Hashnode&lt;/del&gt; , но теперь у этих негодяев платное API для публикации — пусть сами к себе пишут.&lt;/p&gt;

&lt;h3&gt;
  
  
  Как теперь работает схема
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Пишу статью вручную в Teletype (личное) или в корпоративный блог.&lt;/li&gt;
&lt;li&gt;Статьи автоматически улетают в черновики &lt;code&gt;Dev.to&lt;/code&gt; через &lt;code&gt;RSS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Перед публикацией на Dev.to я добавляю &lt;strong&gt;FrontMatter&lt;/strong&gt; &lt;code&gt;original_author=kinnalru&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Публикуем: личные — от себя, корпоративные — от организации &lt;a href="https://dev.to/rnds"&gt;RNDSOFT&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;По крону (через GitLab Pipelines) запускается скрипт-синхронизатор, который забирает только мои статьи (по фильтру &lt;code&gt;original_author&lt;/code&gt;) и публикует их дальше: &lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;на GitHub Pages&lt;/li&gt;
&lt;li&gt;на Blogger&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Итог
&lt;/h3&gt;

&lt;p&gt;Написал небольшой синхронизатор, который делает всё ровно так, как мне нужно. Теперь дело за малым — писать сами статьи :)&lt;/p&gt;




&lt;p&gt;&lt;a href="https://teletype.in/@jerry_ru" rel="noopener noreferrer"&gt;Teletype&lt;/a&gt;| &lt;a href="https://t.me/architdev" rel="noopener noreferrer"&gt;Архи DEV&lt;/a&gt; | &lt;a href="https://dev.to/kinnalru"&gt;Dev.to&lt;/a&gt; | &lt;a href="https://kinnalru.github.io" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://kinnalru.blogspot.com" rel="noopener noreferrer"&gt;Blogger&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <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>Syncerman: от bash-скриптов к Go cli для пяти облаков одновременно</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Tue, 21 Jul 2026 15:46:22 +0000</pubDate>
      <link>https://dev.to/kinnalru/syncerman-ot-bash-skriptov-k-go-cli-dlia-piati-oblakov-odnovriemienno-hn7</link>
      <guid>https://dev.to/kinnalru/syncerman-ot-bash-skriptov-k-go-cli-dlia-piati-oblakov-odnovriemienno-hn7</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%2Fa6iahwmaj8emqaayo649.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%2Fa6iahwmaj8emqaayo649.png" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&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;Яндекс Диск&lt;/code&gt; работает, &lt;code&gt;Google Drive&lt;/code&gt; может изменить условия, &lt;code&gt;Dropbox&lt;/code&gt; может внезапно ограничить доступ. Поэтому идея была простая: &lt;strong&gt;хранить копии везде&lt;/strong&gt;. &lt;code&gt;Яндекс Диск&lt;/code&gt;, &lt;code&gt;Google Drive&lt;/code&gt;, &lt;code&gt;Mail.ru Cloud&lt;/code&gt;, &lt;code&gt;Dropbox&lt;/code&gt; — чем больше, тем надёжнее. Только файлы, только хардкор!&lt;/p&gt;

&lt;p&gt;Раньше я обходился консольным демоном &lt;code&gt;yandex-disk&lt;/code&gt; — он работал, но только с Яндексом. Потом перешёл на самописные bash-скрипты, которые вызывали &lt;a href="https://rclone.org/" rel="noopener noreferrer"&gt;rclone&lt;/a&gt; напрямую. Это работало… до поры до времени. Конфигурация разрасталась, порядок синхронизации был важен (нельзя синкать из пустого облака в другое до того, как туда прилетели файлы) и всякое подобное. Проблема!&lt;/p&gt;

&lt;p&gt;Когда появились нормальные ИИ-ассистенты для кодинга, я решил, что пора пробовать, а мне нужен нормальный инструмент. Declarative configuration, predictable order, automatic error handling и пр. Так родился &lt;a href="https://gitlab.com/kinnalru/syncerman" rel="noopener noreferrer"&gt;Syncerman&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;del&gt;Из говна и палок&lt;/del&gt; архитектура и технологии
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Syncerman&lt;/code&gt; написан на &lt;strong&gt;Go&lt;/strong&gt; — выбрал его только за простоту распространения (один бинарник).&lt;/p&gt;

&lt;p&gt;В основе лежит &lt;a href="https://rclone.org/bisync/" rel="noopener noreferrer"&gt;rclone bisync&lt;/a&gt; — двусторонняя синхронизация с сохранением метаданных. &lt;code&gt;Syncerman&lt;/code&gt; не пытается переизобрести колесо синхронизации, а выступает оркестратором над &lt;code&gt;rclone&lt;/code&gt;, предоставляя удобный CLI и YAML-конфигурацию. Никаких серверов и БД.&lt;/p&gt;

&lt;h3&gt;
  
  
  YAML-конфигурация
&lt;/h3&gt;

&lt;p&gt;Вместо папок с ссылками на bash-скрипты — чёткая структура:&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;important&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Важное"&lt;/span&gt;
    &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local:/home/jerry/cloud/mirror/Documents"&lt;/span&gt;
        &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gdrive:folders/Documents"&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ydisk:folders/Documents"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local:Наследие&lt;/span&gt;
        &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gdrive:Наследие/&lt;/span&gt;
            &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--force"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ydisk:Наследие/&lt;/span&gt;
            &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--force"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mailru:Наследие/&lt;/span&gt;
            &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--force"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;games&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;игрули"&lt;/span&gt;
    &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
    &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local:games_sync"&lt;/span&gt;
        &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ydisk:folders/games_sync"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Порядок &lt;strong&gt;tasks&lt;/strong&gt; и &lt;strong&gt;to&lt;/strong&gt; внутри массивов сохраняется строго. Это критично для цепочек: &lt;code&gt;local → Google Drive → Yandex Disk&lt;/code&gt;. Без этого — катастрофа.&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;rclone bisync&lt;/code&gt; требует state-файлы для работы (они лежат где-то в папке пользователя). При первом запуске на новой паре путей он падает с ошибкой &lt;em&gt;"cannot find prior Path1 or Path2 listings"&lt;/em&gt;. Syncerman детектирует это по двум паттернам в выводе и автоматически перезапускает синхронизацию с флагом &lt;code&gt;--resync&lt;/code&gt;. Пользователь даже не замечает проблемы. Раньше делал руками.&lt;/p&gt;

&lt;p&gt;На ранних этапах обнаружился критический баг: из-за использования map в Go порядок выполнения целей был недетерминированным. Для линейных цепочек это катастрофа: если сначала синкать из пустого &lt;code&gt;Yandex&lt;/code&gt; в &lt;code&gt;Google&lt;/code&gt;, а потом из локальной папки в &lt;code&gt;Google&lt;/code&gt; — быть беде. Проблема! Так появились массивы.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://gitlab.com/kinnalru/syncerman" rel="noopener noreferrer"&gt;Syncerman&lt;/a&gt; версии **0.3.6** сейчас работает в production — то есть на моих машинах :) Само собой нужны пруфы, и их есть у меня!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;бинарники для Linux и Windows (сборка сразу подо всё)&lt;/li&gt;
&lt;li&gt;GitLab CI как в лучших домах&lt;/li&gt;
&lt;li&gt;Покрытие тестами - самому было бы лень писать а тут юнит-тесты для всех ключевых пакетов (config, rclone, sync), интеграционные тесты (не просто интеграционные а целые &lt;strong&gt;bash-сценарии с десятком шагов&lt;/strong&gt; ).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Публичные репозитории (это мой фетиш):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gitlab &lt;a href="https://gitlab.com/kinnalru/syncerman" rel="noopener noreferrer"&gt;https://gitlab.com/kinnalru/syncerman&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Github &lt;a href="https://github.com/kinnalru/syncerman" rel="noopener noreferrer"&gt;https://github.com/kinnalru/syncerman&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Gitflic &lt;a href="https://gitflic.ru/project/kinnalru/syncerman" rel="noopener noreferrer"&gt;https://gitflic.ru/project/kinnalru/syncerman&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Gitverse &lt;a href="https://gitverse.ru/kinnalru/syncerman" rel="noopener noreferrer"&gt;https://gitverse.ru/kinnalru/syncerman&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Использование
&lt;/h3&gt;

&lt;p&gt;Мой &lt;code&gt;crontab&lt;/code&gt; выглядит так:&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="c"&gt;# Синхронизация документов и сохранений 2 рааз в час и лог для ручной проверки&lt;/span&gt;
10,30 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /bin/sh &lt;span class="nt"&gt;-lc&lt;/span&gt; &lt;span class="s2"&gt;"cd /home/jerry/cloud/mirror &amp;amp;&amp;amp; syncerman sync 2&amp;gt;&amp;amp;1 | tee ./lastrun.log"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Конфигурация зеркалирует локальные папки во все облака (настроенные в &lt;code&gt;rclone&lt;/code&gt;) одновременно, а затем синхронизирует облака между собой. Если одно облако отвалится — данные остаются в остальных. Если &lt;code&gt;rclone&lt;/code&gt; выдаст &lt;code&gt;first-run&lt;/code&gt; ошибку — &lt;code&gt;Syncerman&lt;/code&gt; сам всё починит.&lt;/p&gt;

&lt;h3&gt;
  
  
  Вместо вывода
&lt;/h3&gt;

&lt;p&gt;Появление ИИ-ассистентов снизило порог входа для написания таких инструментов. Раньше мне было лень писать то, что и так работает с помощью пары строк на bash. А сейчас это делаю не я :) — разбор выхлопов &lt;code&gt;rclone&lt;/code&gt;, обработка edge cases и тестирование делаются очень легко. Я могу заняться решением проблем (архитектурой и требованиями), а рутина "делается сама".&lt;/p&gt;




&lt;p&gt;Второй фетиш это ссылочки:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://rclone.org/bisync/" rel="noopener noreferrer"&gt;rclone bisync&lt;/a&gt; - это база&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/bcpierce00/unison" rel="noopener noreferrer"&gt;unison&lt;/a&gt; - еще одна классная штука по теме синхронизации, она прочно заняла место в моём сердце, но об это в другой раз&lt;/li&gt;
&lt;/ul&gt;

</description>
    </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>
  </channel>
</rss>
