<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
  xmlns:atom="http://w3.org"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns:media="http://search.yahoo.com/mrss/"
  xmlns:yandex="http://news.yandex.ru" 
  xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Samoilenko Yuri</title>
    <description>Lead developer and technical writer @ RNDSOFT  // Ruby 💕
Тут пишу про интересное, чем занимаюсь на работе - IT, проектирование, разработка, архитектура и технологии</description>
    <link>https://kinnalru.github.io/</link>
    <atom:link href="https://kinnalru.github.io/feed.xml" rel="self" type="application/rss+xml"></atom:link>
    <pubDate>Mon, 03 Aug 2026 23:36:03 +0300</pubDate>
    <lastBuildDate>Mon, 03 Aug 2026 23:36:03 +0300</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    <language>ru-ru</language>
    
      <item>
        <guid isPermaLink="true">https://kinnalru.github.io/2026/08/02/nakonets_to_github_pages_i_gitlab_pages_dlya_simmetrii.html</guid>
        <link>https://kinnalru.github.io/2026/08/02/nakonets_to_github_pages_i_gitlab_pages_dlya_simmetrii.html</link>
        <pdalink>https://kinnalru.github.io/2026/08/02/nakonets_to_github_pages_i_gitlab_pages_dlya_simmetrii.html</pdalink>
        <dc:creator>kinnalru</dc:creator>
        <author>kinnalru@gmail.com (Самойленко Юрий)</author>
        <title>Наконец-то GitHub Pages (и GitLab Pages — для симметрии)</title>
        <pubDate>Sun, 02 Aug 2026 15:22:16 +0300</pubDate>
        <yandex:genre>article</yandex:genre>
        
        <media:content medium="image" url="https://img3.teletype.in/files/22/f5/22f5b394-0142-43f3-96d5-dc955e05ff2f.jpeg"/>
        
        <description><![CDATA[ <p><img src="https://img3.teletype.in/files/22/f5/22f5b394-0142-43f3-96d5-dc955e05ff2f.jpeg" alt="" /></p><p>Давно присматривался к <a href="https://pages.github.com/">GitHub Pages</a>, но руки никак не доходили.</p><p>Внезапно решил навести порядок на площадках, где публикую статьи и заметки, и решился-таки на статику (как доп. площадку).</p><h2 id="всякие-разные-pages">Всякие разные Pages</h2><p><code class="language-plaintext highlighter-rouge">GitHub Pages</code> нативно поддерживает <a href="https://jekyllrb.com/">Jekyll</a> — пушнул в репозиторий, и сайт сам собрался. Никаких докеров, никаких CI/CD (ну почти), никаких серверов.</p><p>А чтобы было совсем хорошо, решил поддерживать и <code class="language-plaintext highlighter-rouge">GitLab Pages</code> — для симметрии. Чтоб не привязываться к одной платформе и чтоб было куда переключиться, если что. Но тут без CI/CD никак.</p><h3 id="структура-репозитория-и-настройки">Структура репозитория и настройки</h3><p>Репозиторий называется как положено — <code class="language-plaintext highlighter-rouge">kinnalru.github.io</code> - тогда он будет доступен по этому доменному имени. Gemfile:</p><div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s2">"github-pages"</span><span class="p">,</span> <span class="ss">group: :jekyll_plugins</span><span class="n">group</span> <span class="ss">:jekyll_plugins</span> <span class="k">do</span>  <span class="n">gem</span> <span class="s2">"jekyll-feed"</span>  <span class="n">gem</span> <span class="s2">"jekyll-sitemap"</span><span class="k">end</span></code></pre></div></div><p>Обратите внимание на <code class="language-plaintext highlighter-rouge">github-pages</code> — это мета-гем, который тянет <code class="language-plaintext highlighter-rouge">Jekyll</code> и все нужные плагины в совместимых версиях. Никаких “а у меня работает”, потому что <code class="language-plaintext highlighter-rouge">GitHub Pages</code> собирает ровно тем же набором.</p><p>Заполняем конфигурацию <a href="https://github.com/kinnalru/kinnalru.github.io/blob/master/_config.yml">_config.yml</a> где выбираем тему, плагины, и всякие настройки. Тема <a href="https://github.com/jekyll/minima">minima</a> — адаптивная под светлую/темную тему. И сразу же в конфиге ссылочки на все свои ресурсы: <strong>GitLab</strong> , <strong>GitHub</strong> , <strong>Teletype</strong> , <strong>Telegram</strong> , <strong>Dev.to</strong>. Чтоб гость зашел и сразу понял, где меня искать, а то вдруг нигде раньше не нашел ссылок :)</p><h3 id="gitlab-pages--симметрия-ради-симметрии">GitLab Pages — симметрия ради симметрии</h3><p>Для <code class="language-plaintext highlighter-rouge">GitLab Pages</code> пришлось чуть заморочиться. <code class="language-plaintext highlighter-rouge">GitHub Pages</code> собирает <code class="language-plaintext highlighter-rouge">Jekyll</code> сам, а <code class="language-plaintext highlighter-rouge">GitLab</code> — нет, нужен <a href="https://github.com/kinnalru/kinnalru.github.io/blob/master/.gitlab-ci.yml">.gitlab-ci.yml</a>:</p><div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">image</span><span class="pi">:</span> <span class="s">ruby:3.2-slim</span><span class="na">build</span><span class="pi">:</span>  <span class="na">script</span><span class="pi">:</span>    <span class="pi">-</span> <span class="s">apt-get update -y; apt-get install -y build-essential</span>    <span class="pi">-</span> <span class="s">bundle</span>    <span class="pi">-</span> <span class="s">JEKYLL_ENV=production bundle exec jekyll build --config _config.yml,_config.gitlab.yml -d ./public/</span>  <span class="na">pages</span><span class="pi">:</span>    <span class="na">publish</span><span class="pi">:</span> <span class="s">public</span>  <span class="na">rules</span><span class="pi">:</span>    <span class="pi">-</span> <span class="na">if</span><span class="pi">:</span> <span class="s">($CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH) &amp;&amp; ($CI_PIPELINE_SOURCE != "schedule")</span></code></pre></div></div><p>И отдельный <code class="language-plaintext highlighter-rouge">_config.gitlab.yml</code> — переопределяем только <strong>url</strong> :</p><div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">url</span><span class="pi">:</span> <span class="s">https://kinnalru.gitlab.io</span></code></pre></div></div><p><code class="language-plaintext highlighter-rouge">Jekyll</code> умеет мержить конфиги при сборке: <code class="language-plaintext highlighter-rouge">--config _config.yml,_config.gitlab.yml</code>. Всё, что идет справа, переопределяет то, что слева.</p><h3 id="чтоб-было-хорошо--намазал-сверху-rss-и-sitemap">Чтоб было “хорошо” — намазал сверху RSS и Sitemap</h3><p>Просто собирать HTML — это скучно. Мне нужно, чтоб поисковики индексировали, а читатели подписывались. Поэтому сверху еще и:</p><ul>  <li>ставим плагин <code class="language-plaintext highlighter-rouge">jekyll-sitemap</code>, и он автоматически генерирует <code class="language-plaintext highlighter-rouge">/sitemap.xml</code>. Но я не доверяю автоматике до конца, поэтому в <code class="language-plaintext highlighter-rouge">_includes/custom-head.html</code> добавил явную ссылку:</li></ul><div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"sitemap"</span> <span class="na">type=</span><span class="s">"application/xml"</span> <span class="na">title=</span><span class="s">"Sitemap"</span> <span class="na">href=</span><span class="s">"/sitemap.xml"</span> <span class="nt">/&gt;</span></code></pre></div></div><ul>  <li>ставим плагин <code class="language-plaintext highlighter-rouge">jekyll-feed</code> — он генерирует базовый <code class="language-plaintext highlighter-rouge">/feed.xml</code></li>  <li>но я решил, что хочу RSS с полным контентом статей и расширенными настройками, поэтому написал свой шаблон <a href="https://github.com/kinnalru/kinnalru.github.io/blob/master/feed.xml">feed.xml</a></li></ul><h3 id="кастомные-шаблоны">Кастомные шаблоны</h3><p>Стандартная minima хороша, но мне хотелось, чтоб на каждом посте была ссылка на оригинальную публикацию. Переопределил <code class="language-plaintext highlighter-rouge">_layouts/post.html</code> и <code class="language-plaintext highlighter-rouge">_layouts/home.html</code> — добавил <strong>canonical_url</strong> и пометку <em>“Моя публикация на …“</em>. Теперь зеркало не выглядит как украденный контент, а как полноценный агрегатор.</p><h3 id="что-в-итоге">Что в итоге?</h3><p>В результате мы имеем:</p><ul>  <li>Статический сайт на <code class="language-plaintext highlighter-rouge">Jekyll</code>, который собирается автоматически</li>  <li>Зеркало на <code class="language-plaintext highlighter-rouge">GitHub Pages</code> — <a href="https://kinnalru.github.io">kinnalru.github.io</a></li>  <li>Зеркало на <code class="language-plaintext highlighter-rouge">GitLab Pages</code> — <a href="https://kinnalru.gitlab.io">kinnalru.gitlab.io</a></li>  <li><code class="language-plaintext highlighter-rouge">RSS</code> с полным контентом для подписчиков и дальнейшей обработки</li>  <li><code class="language-plaintext highlighter-rouge">Sitemap</code> для поисковиков</li>  <li>Все ссылки на мои ресурсы в одном месте</li></ul><blockquote>  <p>Keep it simple, stupid — но при этом всё должно работать.</p></blockquote><hr /><table>  <tbody>    <tr>      <td><a href="https://teletype.in/@jerry_ru">Teletype</a></td>      <td><a href="https://t.me/architdev">Архи DEV</a></td>      <td><a href="https://dev.to/kinnalru">Dev.to</a></td>      <td><a href="https://github.com/kinnalru">GitHub</a></td>      <td><a href="https://kinnalru.blogspot.com">Blogger</a></td>    </tr>  </tbody></table> ]]></description>
        <content:encoded><![CDATA[ <p><img src="https://img3.teletype.in/files/22/f5/22f5b394-0142-43f3-96d5-dc955e05ff2f.jpeg" alt="" /></p><p>Давно присматривался к <a href="https://pages.github.com/">GitHub Pages</a>, но руки никак не доходили.</p><p>Внезапно решил навести порядок на площадках, где публикую статьи и заметки, и решился-таки на статику (как доп. площадку).</p><h2 id="всякие-разные-pages">Всякие разные Pages</h2><p><code class="language-plaintext highlighter-rouge">GitHub Pages</code> нативно поддерживает <a href="https://jekyllrb.com/">Jekyll</a> — пушнул в репозиторий, и сайт сам собрался. Никаких докеров, никаких CI/CD (ну почти), никаких серверов.</p><p>А чтобы было совсем хорошо, решил поддерживать и <code class="language-plaintext highlighter-rouge">GitLab Pages</code> — для симметрии. Чтоб не привязываться к одной платформе и чтоб было куда переключиться, если что. Но тут без CI/CD никак.</p><h3 id="структура-репозитория-и-настройки">Структура репозитория и настройки</h3><p>Репозиторий называется как положено — <code class="language-plaintext highlighter-rouge">kinnalru.github.io</code> - тогда он будет доступен по этому доменному имени. Gemfile:</p><div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s2">"github-pages"</span><span class="p">,</span> <span class="ss">group: :jekyll_plugins</span><span class="n">group</span> <span class="ss">:jekyll_plugins</span> <span class="k">do</span>  <span class="n">gem</span> <span class="s2">"jekyll-feed"</span>  <span class="n">gem</span> <span class="s2">"jekyll-sitemap"</span><span class="k">end</span></code></pre></div></div><p>Обратите внимание на <code class="language-plaintext highlighter-rouge">github-pages</code> — это мета-гем, который тянет <code class="language-plaintext highlighter-rouge">Jekyll</code> и все нужные плагины в совместимых версиях. Никаких “а у меня работает”, потому что <code class="language-plaintext highlighter-rouge">GitHub Pages</code> собирает ровно тем же набором.</p><p>Заполняем конфигурацию <a href="https://github.com/kinnalru/kinnalru.github.io/blob/master/_config.yml">_config.yml</a> где выбираем тему, плагины, и всякие настройки. Тема <a href="https://github.com/jekyll/minima">minima</a> — адаптивная под светлую/темную тему. И сразу же в конфиге ссылочки на все свои ресурсы: <strong>GitLab</strong> , <strong>GitHub</strong> , <strong>Teletype</strong> , <strong>Telegram</strong> , <strong>Dev.to</strong>. Чтоб гость зашел и сразу понял, где меня искать, а то вдруг нигде раньше не нашел ссылок :)</p><h3 id="gitlab-pages--симметрия-ради-симметрии">GitLab Pages — симметрия ради симметрии</h3><p>Для <code class="language-plaintext highlighter-rouge">GitLab Pages</code> пришлось чуть заморочиться. <code class="language-plaintext highlighter-rouge">GitHub Pages</code> собирает <code class="language-plaintext highlighter-rouge">Jekyll</code> сам, а <code class="language-plaintext highlighter-rouge">GitLab</code> — нет, нужен <a href="https://github.com/kinnalru/kinnalru.github.io/blob/master/.gitlab-ci.yml">.gitlab-ci.yml</a>:</p><div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">image</span><span class="pi">:</span> <span class="s">ruby:3.2-slim</span><span class="na">build</span><span class="pi">:</span>  <span class="na">script</span><span class="pi">:</span>    <span class="pi">-</span> <span class="s">apt-get update -y; apt-get install -y build-essential</span>    <span class="pi">-</span> <span class="s">bundle</span>    <span class="pi">-</span> <span class="s">JEKYLL_ENV=production bundle exec jekyll build --config _config.yml,_config.gitlab.yml -d ./public/</span>  <span class="na">pages</span><span class="pi">:</span>    <span class="na">publish</span><span class="pi">:</span> <span class="s">public</span>  <span class="na">rules</span><span class="pi">:</span>    <span class="pi">-</span> <span class="na">if</span><span class="pi">:</span> <span class="s">($CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH) &amp;&amp; ($CI_PIPELINE_SOURCE != "schedule")</span></code></pre></div></div><p>И отдельный <code class="language-plaintext highlighter-rouge">_config.gitlab.yml</code> — переопределяем только <strong>url</strong> :</p><div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">url</span><span class="pi">:</span> <span class="s">https://kinnalru.gitlab.io</span></code></pre></div></div><p><code class="language-plaintext highlighter-rouge">Jekyll</code> умеет мержить конфиги при сборке: <code class="language-plaintext highlighter-rouge">--config _config.yml,_config.gitlab.yml</code>. Всё, что идет справа, переопределяет то, что слева.</p><h3 id="чтоб-было-хорошо--намазал-сверху-rss-и-sitemap">Чтоб было “хорошо” — намазал сверху RSS и Sitemap</h3><p>Просто собирать HTML — это скучно. Мне нужно, чтоб поисковики индексировали, а читатели подписывались. Поэтому сверху еще и:</p><ul>  <li>ставим плагин <code class="language-plaintext highlighter-rouge">jekyll-sitemap</code>, и он автоматически генерирует <code class="language-plaintext highlighter-rouge">/sitemap.xml</code>. Но я не доверяю автоматике до конца, поэтому в <code class="language-plaintext highlighter-rouge">_includes/custom-head.html</code> добавил явную ссылку:</li></ul><div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"sitemap"</span> <span class="na">type=</span><span class="s">"application/xml"</span> <span class="na">title=</span><span class="s">"Sitemap"</span> <span class="na">href=</span><span class="s">"/sitemap.xml"</span> <span class="nt">/&gt;</span></code></pre></div></div><ul>  <li>ставим плагин <code class="language-plaintext highlighter-rouge">jekyll-feed</code> — он генерирует базовый <code class="language-plaintext highlighter-rouge">/feed.xml</code></li>  <li>но я решил, что хочу RSS с полным контентом статей и расширенными настройками, поэтому написал свой шаблон <a href="https://github.com/kinnalru/kinnalru.github.io/blob/master/feed.xml">feed.xml</a></li></ul><h3 id="кастомные-шаблоны">Кастомные шаблоны</h3><p>Стандартная minima хороша, но мне хотелось, чтоб на каждом посте была ссылка на оригинальную публикацию. Переопределил <code class="language-plaintext highlighter-rouge">_layouts/post.html</code> и <code class="language-plaintext highlighter-rouge">_layouts/home.html</code> — добавил <strong>canonical_url</strong> и пометку <em>“Моя публикация на …“</em>. Теперь зеркало не выглядит как украденный контент, а как полноценный агрегатор.</p><h3 id="что-в-итоге">Что в итоге?</h3><p>В результате мы имеем:</p><ul>  <li>Статический сайт на <code class="language-plaintext highlighter-rouge">Jekyll</code>, который собирается автоматически</li>  <li>Зеркало на <code class="language-plaintext highlighter-rouge">GitHub Pages</code> — <a href="https://kinnalru.github.io">kinnalru.github.io</a></li>  <li>Зеркало на <code class="language-plaintext highlighter-rouge">GitLab Pages</code> — <a href="https://kinnalru.gitlab.io">kinnalru.gitlab.io</a></li>  <li><code class="language-plaintext highlighter-rouge">RSS</code> с полным контентом для подписчиков и дальнейшей обработки</li>  <li><code class="language-plaintext highlighter-rouge">Sitemap</code> для поисковиков</li>  <li>Все ссылки на мои ресурсы в одном месте</li></ul><blockquote>  <p>Keep it simple, stupid — но при этом всё должно работать.</p></blockquote><hr /><table>  <tbody>    <tr>      <td><a href="https://teletype.in/@jerry_ru">Teletype</a></td>      <td><a href="https://t.me/architdev">Архи DEV</a></td>      <td><a href="https://dev.to/kinnalru">Dev.to</a></td>      <td><a href="https://github.com/kinnalru">GitHub</a></td>      <td><a href="https://kinnalru.blogspot.com">Blogger</a></td>    </tr>  </tbody></table> ]]></content:encoded>
        <yandex:full-text>Давно присматривался к GitHub Pages, но руки никак не доходили.Внезапно решил навести порядок на площадках, где публикую статьи и заметки, и решился-таки на статику (как доп. площадку).Всякие разные PagesGitHub Pages нативно поддерживает Jekyll — пушнул в репозиторий, и сайт сам собрался. Никаких докеров, никаких CI/CD (ну почти), никаких серверов.А чтобы было совсем хорошо, решил поддерживать и GitLab Pages — для симметрии. Чтоб не привязываться к одной платформе и чтоб было куда переключиться, если что. Но тут без CI/CD никак.Структура репозитория и настройкиРепозиторий называется как положено — kinnalru.github.io - тогда он будет доступен по этому доменному имени. Gemfile:gem "github-pages", group: :jekyll_pluginsgroup :jekyll_plugins do  gem "jekyll-feed"  gem "jekyll-sitemap"endОбратите внимание на github-pages — это мета-гем, который тянет Jekyll и все нужные плагины в совместимых версиях. Никаких “а у меня работает”, потому что GitHub Pages собирает ровно тем же набором.Заполняем конфигурацию _config.yml где выбираем тему, плагины, и всякие настройки. Тема minima — адаптивная под светлую/темную тему. И сразу же в конфиге ссылочки на все свои ресурсы: GitLab , GitHub , Teletype , Telegram , Dev.to. Чтоб гость зашел и сразу понял, где меня искать, а то вдруг нигде раньше не нашел ссылок :)GitLab Pages — симметрия ради симметрииДля GitLab Pages пришлось чуть заморочиться. GitHub Pages собирает Jekyll сам, а GitLab — нет, нужен .gitlab-ci.yml:image: ruby:3.2-slimbuild:  script:    - apt-get update -y; apt-get install -y build-essential    - bundle    - JEKYLL_ENV=production bundle exec jekyll build --config _config.yml,_config.gitlab.yml -d ./public/  pages:    publish: public  rules:    - if: ($CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH) &amp;&amp; ($CI_PIPELINE_SOURCE != "schedule")И отдельный _config.gitlab.yml — переопределяем только url :url: https://kinnalru.gitlab.ioJekyll умеет мержить конфиги при сборке: --config _config.yml,_config.gitlab.yml. Всё, что идет справа, переопределяет то, что слева.Чтоб было “хорошо” — намазал сверху RSS и SitemapПросто собирать HTML — это скучно. Мне нужно, чтоб поисковики индексировали, а читатели подписывались. Поэтому сверху еще и:  ставим плагин jekyll-sitemap, и он автоматически генерирует /sitemap.xml. Но я не доверяю автоматике до конца, поэтому в _includes/custom-head.html добавил явную ссылку:  &lt;link rel="sitemap" type="application/xml" title="Sitemap" href="/sitemap.xml" /&gt;  ставим плагин jekyll-feed — он генерирует базовый /feed.xml  но я решил, что хочу RSS с полным контентом статей и расширенными настройками, поэтому написал свой шаблон feed.xmlКастомные шаблоныСтандартная minima хороша, но мне хотелось, чтоб на каждом посте была ссылка на оригинальную публикацию. Переопределил _layouts/post.html и _layouts/home.html — добавил canonical_url и пометку “Моя публикация на …“. Теперь зеркало не выглядит как украденный контент, а как полноценный агрегатор.Что в итоге?В результате мы имеем:  Статический сайт на Jekyll, который собирается автоматически  Зеркало на GitHub Pages — kinnalru.github.io  Зеркало на GitLab Pages — kinnalru.gitlab.io  RSS с полным контентом для подписчиков и дальнейшей обработки  Sitemap для поисковиков  Все ссылки на мои ресурсы в одном месте  Keep it simple, stupid — но при этом всё должно работать.            Teletype      Архи DEV      Dev.to      GitHub      Blogger      </yandex:full-text>

        
        <category>блог</category>
        
        <category>synchronization</category>
        
        <category>jekyll</category>
        
      </item>
    
      <item>
        <guid isPermaLink="true">https://kinnalru.github.io/2026/07/31/opyat_sinhronizatsiya_no_teper_blogov.html</guid>
        <link>https://kinnalru.github.io/2026/07/31/opyat_sinhronizatsiya_no_teper_blogov.html</link>
        <pdalink>https://kinnalru.github.io/2026/07/31/opyat_sinhronizatsiya_no_teper_blogov.html</pdalink>
        <dc:creator>kinnalru</dc:creator>
        <author>kinnalru@gmail.com (Самойленко Юрий)</author>
        <title>Опять синхронизация, но теперь блогов</title>
        <pubDate>Fri, 31 Jul 2026 16:26:25 +0300</pubDate>
        <yandex:genre>article</yandex:genre>
        
        <description><![CDATA[ <p>Есть у меня небольшой фетиш по синхронизации… И в очередной раз, подойдя к проблеме блога, я наконец собрал разрозненные куски в единую систему :)</p><h3 id="кто-к-нам-с-чем-и-зачем">Кто к нам с чем и зачем?</h3><p>За годы у меня накопилась целая коллекция площадок, куда я (и не только я) что-то публикую:</p><ul>  <li><strong>Личный блог</strong> на <a href="https://teletype.in/@jerry_ru">Teletype</a> — сюда пишу руками</li>  <li><strong>Корпоративный блог</strong> <a href="https://blog.rnds.pro/">blog.rnds.pro</a> — пишем вместе с коллегами</li>  <li><strong>Dev.to</strong> — личный аккаунт <a href="https://dev.to/kinnalru">kinnalru</a> и корпоративная организация <a href="https://dev.to/rnds">RNDSOFT</a></li>  <li><strong>GitHub Pages</strong> — <a href="https://kinnalru.github.io">kinnalru.github.io</a></li>  <li><strong>Blogger (Blogspot)</strong> — <a href="https://kinnalru.blogspot.com">kinnalru.blogspot.com</a></li></ul><p>Раньше тут обитал ещё и <del>Hashnode</del> , но теперь у этих негодяев платное API для публикации — пусть сами к себе пишут.</p><h3 id="как-теперь-работает-схема">Как теперь работает схема</h3><ol>  <li>Пишу статью вручную в Teletype (личное) или в корпоративный блог.</li>  <li>Статьи автоматически улетают в черновики <code class="language-plaintext highlighter-rouge">Dev.to</code> через <code class="language-plaintext highlighter-rouge">RSS</code></li>  <li>Перед публикацией на Dev.to я добавляю <strong>FrontMatter</strong> <code class="language-plaintext highlighter-rouge">original_author=kinnalru</code>.</li>  <li>Публикуем: личные — от себя, корпоративные — от организации <a href="https://dev.to/rnds">RNDSOFT</a></li>  <li>По крону (через GitLab Pipelines) запускается скрипт-синхронизатор, который забирает только мои статьи (по фильтру <code class="language-plaintext highlighter-rouge">original_author</code>) и публикует их дальше:</li></ol><ul>  <li>на GitHub Pages</li>  <li>на Blogger</li></ul><h3 id="итог">Итог</h3><p>Написал небольшой синхронизатор, который делает всё ровно так, как мне нужно. Теперь дело за малым — писать сами статьи :)</p><hr /><table>  <tbody>    <tr>      <td><a href="https://teletype.in/@jerry_ru">Teletype</a></td>      <td><a href="https://t.me/architdev">Архи DEV</a></td>      <td><a href="https://dev.to/kinnalru">Dev.to</a></td>      <td><a href="https://kinnalru.github.io">GitHub</a></td>      <td><a href="https://kinnalru.blogspot.com">Blogger</a></td>    </tr>  </tbody></table> ]]></description>
        <content:encoded><![CDATA[ <p>Есть у меня небольшой фетиш по синхронизации… И в очередной раз, подойдя к проблеме блога, я наконец собрал разрозненные куски в единую систему :)</p><h3 id="кто-к-нам-с-чем-и-зачем">Кто к нам с чем и зачем?</h3><p>За годы у меня накопилась целая коллекция площадок, куда я (и не только я) что-то публикую:</p><ul>  <li><strong>Личный блог</strong> на <a href="https://teletype.in/@jerry_ru">Teletype</a> — сюда пишу руками</li>  <li><strong>Корпоративный блог</strong> <a href="https://blog.rnds.pro/">blog.rnds.pro</a> — пишем вместе с коллегами</li>  <li><strong>Dev.to</strong> — личный аккаунт <a href="https://dev.to/kinnalru">kinnalru</a> и корпоративная организация <a href="https://dev.to/rnds">RNDSOFT</a></li>  <li><strong>GitHub Pages</strong> — <a href="https://kinnalru.github.io">kinnalru.github.io</a></li>  <li><strong>Blogger (Blogspot)</strong> — <a href="https://kinnalru.blogspot.com">kinnalru.blogspot.com</a></li></ul><p>Раньше тут обитал ещё и <del>Hashnode</del> , но теперь у этих негодяев платное API для публикации — пусть сами к себе пишут.</p><h3 id="как-теперь-работает-схема">Как теперь работает схема</h3><ol>  <li>Пишу статью вручную в Teletype (личное) или в корпоративный блог.</li>  <li>Статьи автоматически улетают в черновики <code class="language-plaintext highlighter-rouge">Dev.to</code> через <code class="language-plaintext highlighter-rouge">RSS</code></li>  <li>Перед публикацией на Dev.to я добавляю <strong>FrontMatter</strong> <code class="language-plaintext highlighter-rouge">original_author=kinnalru</code>.</li>  <li>Публикуем: личные — от себя, корпоративные — от организации <a href="https://dev.to/rnds">RNDSOFT</a></li>  <li>По крону (через GitLab Pipelines) запускается скрипт-синхронизатор, который забирает только мои статьи (по фильтру <code class="language-plaintext highlighter-rouge">original_author</code>) и публикует их дальше:</li></ol><ul>  <li>на GitHub Pages</li>  <li>на Blogger</li></ul><h3 id="итог">Итог</h3><p>Написал небольшой синхронизатор, который делает всё ровно так, как мне нужно. Теперь дело за малым — писать сами статьи :)</p><hr /><table>  <tbody>    <tr>      <td><a href="https://teletype.in/@jerry_ru">Teletype</a></td>      <td><a href="https://t.me/architdev">Архи DEV</a></td>      <td><a href="https://dev.to/kinnalru">Dev.to</a></td>      <td><a href="https://kinnalru.github.io">GitHub</a></td>      <td><a href="https://kinnalru.blogspot.com">Blogger</a></td>    </tr>  </tbody></table> ]]></content:encoded>
        <yandex:full-text>Есть у меня небольшой фетиш по синхронизации… И в очередной раз, подойдя к проблеме блога, я наконец собрал разрозненные куски в единую систему :)Кто к нам с чем и зачем?За годы у меня накопилась целая коллекция площадок, куда я (и не только я) что-то публикую:  Личный блог на Teletype — сюда пишу руками  Корпоративный блог blog.rnds.pro — пишем вместе с коллегами  Dev.to — личный аккаунт kinnalru и корпоративная организация RNDSOFT  GitHub Pages — kinnalru.github.io  Blogger (Blogspot) — kinnalru.blogspot.comРаньше тут обитал ещё и Hashnode , но теперь у этих негодяев платное API для публикации — пусть сами к себе пишут.Как теперь работает схема  Пишу статью вручную в Teletype (личное) или в корпоративный блог.  Статьи автоматически улетают в черновики Dev.to через RSS  Перед публикацией на Dev.to я добавляю FrontMatter original_author=kinnalru.  Публикуем: личные — от себя, корпоративные — от организации RNDSOFT  По крону (через GitLab Pipelines) запускается скрипт-синхронизатор, который забирает только мои статьи (по фильтру original_author) и публикует их дальше:  на GitHub Pages  на BloggerИтогНаписал небольшой синхронизатор, который делает всё ровно так, как мне нужно. Теперь дело за малым — писать сами статьи :)            Teletype      Архи DEV      Dev.to      GitHub      Blogger      </yandex:full-text>

        
      </item>
    
      <item>
        <guid isPermaLink="true">https://kinnalru.github.io/2026/07/29/sqlite_v_docker_zhit_horosho_a_horosho_zhit_eschyo_luchshe_litestream.html</guid>
        <link>https://kinnalru.github.io/2026/07/29/sqlite_v_docker_zhit_horosho_a_horosho_zhit_eschyo_luchshe_litestream.html</link>
        <pdalink>https://kinnalru.github.io/2026/07/29/sqlite_v_docker_zhit_horosho_a_horosho_zhit_eschyo_luchshe_litestream.html</pdalink>
        <dc:creator>kinnalru</dc:creator>
        <author>kinnalru@gmail.com (Самойленко Юрий)</author>
        <title>SQLite в Docker: жить хорошо, а хорошо жить — ещё лучше (Litestream)</title>
        <pubDate>Wed, 29 Jul 2026 16:44:57 +0300</pubDate>
        <yandex:genre>article</yandex:genre>
        
        <media:content medium="image" url="https://img3.teletype.in/files/26/fa/26fa30c2-bbc2-4f91-9609-899d9f07a9b2.png"/>
        
        <description><![CDATA[ <p><img src="https://img3.teletype.in/files/26/fa/26fa30c2-bbc2-4f91-9609-899d9f07a9b2.png" alt="" /></p><p>Иногда хочется, чтобы всё было просто. Взял Rails-приложение, завернул в Docker, запустил — и оно работает. Без отдельной СУБД, без монтирования хранилища для картинок и пр.</p><p>Но вот незадача: если взять <code class="language-plaintext highlighter-rouge">SQLite</code>, положить её в контейнер и запустить — при <code class="language-plaintext highlighter-rouge">docker rm</code> ваша база улетает в никуда. Навсегда. Вместе с пользователями, настройками и всем, что вы так старательно назаполняли.</p><p>Сегодня расскажу, как я в pet-проекте <a href="https://runit.rnds.pro/">RuNiT</a> решил эту проблему с помощью <strong>Litestream</strong> — и просто кайфую.</p><h3 id="проблема">Проблема</h3><p>Классический путь деплоя pet-проекта выглядит так:</p><ol>  <li>Поднять <code class="language-plaintext highlighter-rouge">PostgreSQL</code> в отдельном контейнере (найти под него постоянное хранилище и сервак)</li>  <li>Прокинуть volume</li>  <li>Понять, что вы тратите больше времени на администрирование СУБД, и хранилище картинок, чем на разработку фич</li></ol><p>Для pet-проекта это ощутимый оверхед. <code class="language-plaintext highlighter-rouge">SQLite</code> решает первый пункт, но создаёт два других:</p><ul>  <li>файл базы живёт внутри контейнера и при пересоздании исчезает</li>  <li>бэкапов нет, и если сервер упал — всё, приплыли.</li></ul><p>Тут бы и сказать “ну, SQLite не для продакшена”, но мне как-то попадался проект <a href="https://litestream.io/">Litestream - Streaming SQLite Replication</a></p><h3 id="litestream">Litestream</h3><p><code class="language-plaintext highlighter-rouge">Litestream</code> — это standalone-утилита (написана на Go, ~14k stars на <a href="https://github.com/benbjohnson/litestream">GitHub</a>) для потоковой репликации <code class="language-plaintext highlighter-rouge">SQLite</code>. Она следит за WAL (Write-Ahead Log) вашей базы и в реальном времени отправляет изменения в S3-совместимое хранилище: <code class="language-plaintext highlighter-rouge">Yandex Object Storage</code>, <code class="language-plaintext highlighter-rouge">MinIO</code>, <code class="language-plaintext highlighter-rouge">RustFS</code> и многие другие.</p><p>Умеет:</p><ul>  <li><strong>Continuous backup</strong> — каждая транзакция стримится в облако почти мгновенно</li>  <li><strong>Restore при старте</strong> — при пересоздании контейнера база автоматически восстанавливается из реплики</li>  <li><strong>Point-in-time recovery</strong> — можно откатиться к состоянию на конкретную секунду.</li>  <li><strong>Ноль изменений в коде приложения</strong> — <code class="language-plaintext highlighter-rouge">Litestream</code> работает как отдельный процесс.</li>  <li><strong>Дёшево</strong> — object storage стоит копейки.</li></ul><p>Звучит как магия? По сути — да. Но магия очень простая и надёжная - разработчик говорит что работает в проде стабильно.</p><h3 id="как-запустить">Как запустить</h3><p>Тыкать надо тут следующее:</p><p>Устанавливаем <code class="language-plaintext highlighter-rouge">Litestream</code> прямо в образ:</p><div class="language-docker highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#Dockerfile</span><span class="k">RUN </span><span class="nb">set</span> <span class="nt">-ex</span>  &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; dpkg -i litestream.deb \  &amp;&amp; rm litestream.deb</code></pre></div></div><p>Передаём credentials для S3 и URL реплики:</p><div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>  <span class="na">app</span><span class="pi">:</span>    <span class="na">environment</span><span class="pi">:</span>      <span class="pi">-</span> <span class="s">LITESTREAM_ACCESS_KEY_ID=${LITESTREAM_ACCESS_KEY_ID-key}</span>      <span class="pi">-</span> <span class="s">LITESTREAM_SECRET_ACCESS_KEY=${LITESTREAM_SECRET_ACCESS_KEY-secret}</span>      <span class="pi">-</span> <span class="s">REMOTE_S3_FILE=${REMOTE_S3_FILE-s3://11.11.11.11:9000/}</span></code></pre></div></div><p>Главный трюк — в <code class="language-plaintext highlighter-rouge">docker/start_service.sh</code>, который является CMD в Dockerfile:</p><div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">set</span> <span class="nt">-ex</span><span class="c"># 1. Восстановить базу из S3, если она не существует локально</span>litestream restore  <span class="nt">-o</span> storage/<span class="k">${</span><span class="nv">RAILS_ENV</span><span class="k">}</span>.sqlite3  <span class="nt">-if-replica-exists</span>  <span class="nt">-if-db-not-exists</span>  <span class="k">${</span><span class="nv">REMOTE_S3_FILE</span><span class="k">}</span>/<span class="k">${</span><span class="nv">RAILS_ENV</span><span class="k">}</span>.sqlite3<span class="c"># 2. Стандартные Rails шаги (idempotent — безопасны для существующей базы)</span>bundle <span class="nb">exec </span>rails db:createbundle <span class="nb">exec </span>rails db:migratebundle <span class="nb">exec </span>rails db:seed<span class="c"># 3. Запускаем Rails ПОД управлением Litestream</span><span class="nb">exec </span>bundle <span class="nb">exec </span>litestream replicate  <span class="nt">-exec</span> <span class="s2">"rails s"</span>  storage/<span class="k">${</span><span class="nv">RAILS_ENV</span><span class="k">}</span>.sqlite3  <span class="k">${</span><span class="nv">REMOTE_S3_FILE</span><span class="k">}</span>/<span class="k">${</span><span class="nv">RAILS_ENV</span><span class="k">}</span>.sqlite3</code></pre></div></div><p>Что здесь происходит:</p><ol>  <li>    <p><strong>litestream restore</strong> — если локального файла <code class="language-plaintext highlighter-rouge">storage/production.sqlite3</code> нет, а в S3 есть бэкап, база скачивается. Флаги <code class="language-plaintext highlighter-rouge">-if-replica-exists</code> и <code class="language-plaintext highlighter-rouge">-if-db-not-exists</code> делают команду безопасной: она не упадёт, если бэкапа ещё нет (первый деплой), и не перезапишет существующую базу если она есть</p>  </li>  <li>    <p><strong>db:create db:migrate db:seed</strong> — классика Rails</p>  </li>  <li>    <p><strong>litestream replicate -exec “rails s”</strong> — <code class="language-plaintext highlighter-rouge">Litestream</code> запускается как supervisor. Он начинает реплицировать WAL в S3 и одновременно запускает дочерний процесс <code class="language-plaintext highlighter-rouge">rails s</code>. Если Rails упадёт, <code class="language-plaintext highlighter-rouge">Litestream</code> тоже завершится.</p>  </li></ol><p>Результат: контейнер можно остановить, удалить, пересоздать на другом хосте — база восстановится из S3 и продолжит реплицироваться. Красота!</p><h3 id="профит-и-подводные-камни">Профит и подводные камни</h3><p>Плюсы:</p><ul>  <li>Минимальная инфраструктура — один контейнер, без отдельной СУБД</li>  <li>Персистентность — данные не пропадают при удалении контейнера</li>  <li>Бэкап “из коробки” — каждая транзакция летит в S3, RPO стремится к нулю</li>  <li>Простое восстановление — <code class="language-plaintext highlighter-rouge">litestream restore </code>или автовосстановление при старте</li>  <li>Дёшево — <code class="language-plaintext highlighter-rouge">SQLite</code> бесплатна, object storage стоит копейки</li>  <li>Не требует изменений в коде приложения</li>  <li>Point-in-time recovery — можно восстановить базу на конкретный момент времени</li></ul><p>Минусы:</p><ul>  <li>Только один writer — <code class="language-plaintext highlighter-rouge">SQLite</code> поддерживает только одного писателя. Это значит, что приложение должно работать в единственном экземпляре (single-node). Ни о каком горизонтальном масштабировании речи не идёт, но в списке ссылок будет сюрприз :)</li>  <li>Зависимость от object storage — если S3 недоступен, <code class="language-plaintext highlighter-rouge">Litestream</code> накапливает WAL локально, но при длительном отключении может потребоваться внимание</li>  <li>Не замена PostgreSQL для высоконагруженных систем — для pet-проекта и небольших сервисов подходит идеально, для HA — нет, но можно посмотреть в ссылочки в конце статьи…</li></ul><p><code class="language-plaintext highlighter-rouge">SQLite</code> + <code class="language-plaintext highlighter-rouge">Litestream</code> — это мощная комбинация для «лёгкого» деплоя. Вы получаете полноценную реляционную базу данных в одном файле, непрерывный backup в S3 и автоматическое восстановление при перезапуске контейнера — без необходимости поднимать и обслуживать отдельную СУБД.</p><p>Для pet-проекта это часто идеальный баланс между простотой и надёжностью.</p><p>Пользуйтесь на здоровье! 🤝</p><h3 id="мои-любимые-ссылки-которые-стоит-почитать">Мои любимые ссылки, которые стоит почитать:</h3><ul>  <li><a href="https://litestream.io/">Litestream</a> и <a href="https://github.com/benbjohnson/litestream">GitHub</a> — официальный сайт и исходники</li>  <li>Вот он сюрприз: <a href="https://fly.io/docs/litefs/">LiteFS - Distributed SQLite</a> - как сделать, горизонтальное масштабирование с <code class="language-plaintext highlighter-rouge">SQLite</code>. Говорят, работает в проде :)</li>  <li>Ну и ссылочка на сам проект <a href="https://runit.rnds.pro/">Беговой Клуб -=БеГиМ=-</a></li></ul><hr /><tt-tags id="DhCr">    <tt-tag name="sqlite">#sqlite</tt-tag>    <tt-tag name="rails">#rails</tt-tag>    <tt-tag name="database">#database</tt-tag>    <tt-tag name="replicantion">#replicantion</tt-tag>  </tt-tags> ]]></description>
        <content:encoded><![CDATA[ <p><img src="https://img3.teletype.in/files/26/fa/26fa30c2-bbc2-4f91-9609-899d9f07a9b2.png" alt="" /></p><p>Иногда хочется, чтобы всё было просто. Взял Rails-приложение, завернул в Docker, запустил — и оно работает. Без отдельной СУБД, без монтирования хранилища для картинок и пр.</p><p>Но вот незадача: если взять <code class="language-plaintext highlighter-rouge">SQLite</code>, положить её в контейнер и запустить — при <code class="language-plaintext highlighter-rouge">docker rm</code> ваша база улетает в никуда. Навсегда. Вместе с пользователями, настройками и всем, что вы так старательно назаполняли.</p><p>Сегодня расскажу, как я в pet-проекте <a href="https://runit.rnds.pro/">RuNiT</a> решил эту проблему с помощью <strong>Litestream</strong> — и просто кайфую.</p><h3 id="проблема">Проблема</h3><p>Классический путь деплоя pet-проекта выглядит так:</p><ol>  <li>Поднять <code class="language-plaintext highlighter-rouge">PostgreSQL</code> в отдельном контейнере (найти под него постоянное хранилище и сервак)</li>  <li>Прокинуть volume</li>  <li>Понять, что вы тратите больше времени на администрирование СУБД, и хранилище картинок, чем на разработку фич</li></ol><p>Для pet-проекта это ощутимый оверхед. <code class="language-plaintext highlighter-rouge">SQLite</code> решает первый пункт, но создаёт два других:</p><ul>  <li>файл базы живёт внутри контейнера и при пересоздании исчезает</li>  <li>бэкапов нет, и если сервер упал — всё, приплыли.</li></ul><p>Тут бы и сказать “ну, SQLite не для продакшена”, но мне как-то попадался проект <a href="https://litestream.io/">Litestream - Streaming SQLite Replication</a></p><h3 id="litestream">Litestream</h3><p><code class="language-plaintext highlighter-rouge">Litestream</code> — это standalone-утилита (написана на Go, ~14k stars на <a href="https://github.com/benbjohnson/litestream">GitHub</a>) для потоковой репликации <code class="language-plaintext highlighter-rouge">SQLite</code>. Она следит за WAL (Write-Ahead Log) вашей базы и в реальном времени отправляет изменения в S3-совместимое хранилище: <code class="language-plaintext highlighter-rouge">Yandex Object Storage</code>, <code class="language-plaintext highlighter-rouge">MinIO</code>, <code class="language-plaintext highlighter-rouge">RustFS</code> и многие другие.</p><p>Умеет:</p><ul>  <li><strong>Continuous backup</strong> — каждая транзакция стримится в облако почти мгновенно</li>  <li><strong>Restore при старте</strong> — при пересоздании контейнера база автоматически восстанавливается из реплики</li>  <li><strong>Point-in-time recovery</strong> — можно откатиться к состоянию на конкретную секунду.</li>  <li><strong>Ноль изменений в коде приложения</strong> — <code class="language-plaintext highlighter-rouge">Litestream</code> работает как отдельный процесс.</li>  <li><strong>Дёшево</strong> — object storage стоит копейки.</li></ul><p>Звучит как магия? По сути — да. Но магия очень простая и надёжная - разработчик говорит что работает в проде стабильно.</p><h3 id="как-запустить">Как запустить</h3><p>Тыкать надо тут следующее:</p><p>Устанавливаем <code class="language-plaintext highlighter-rouge">Litestream</code> прямо в образ:</p><div class="language-docker highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#Dockerfile</span><span class="k">RUN </span><span class="nb">set</span> <span class="nt">-ex</span>  &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; dpkg -i litestream.deb \  &amp;&amp; rm litestream.deb</code></pre></div></div><p>Передаём credentials для S3 и URL реплики:</p><div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>  <span class="na">app</span><span class="pi">:</span>    <span class="na">environment</span><span class="pi">:</span>      <span class="pi">-</span> <span class="s">LITESTREAM_ACCESS_KEY_ID=${LITESTREAM_ACCESS_KEY_ID-key}</span>      <span class="pi">-</span> <span class="s">LITESTREAM_SECRET_ACCESS_KEY=${LITESTREAM_SECRET_ACCESS_KEY-secret}</span>      <span class="pi">-</span> <span class="s">REMOTE_S3_FILE=${REMOTE_S3_FILE-s3://11.11.11.11:9000/}</span></code></pre></div></div><p>Главный трюк — в <code class="language-plaintext highlighter-rouge">docker/start_service.sh</code>, который является CMD в Dockerfile:</p><div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">set</span> <span class="nt">-ex</span><span class="c"># 1. Восстановить базу из S3, если она не существует локально</span>litestream restore  <span class="nt">-o</span> storage/<span class="k">${</span><span class="nv">RAILS_ENV</span><span class="k">}</span>.sqlite3  <span class="nt">-if-replica-exists</span>  <span class="nt">-if-db-not-exists</span>  <span class="k">${</span><span class="nv">REMOTE_S3_FILE</span><span class="k">}</span>/<span class="k">${</span><span class="nv">RAILS_ENV</span><span class="k">}</span>.sqlite3<span class="c"># 2. Стандартные Rails шаги (idempotent — безопасны для существующей базы)</span>bundle <span class="nb">exec </span>rails db:createbundle <span class="nb">exec </span>rails db:migratebundle <span class="nb">exec </span>rails db:seed<span class="c"># 3. Запускаем Rails ПОД управлением Litestream</span><span class="nb">exec </span>bundle <span class="nb">exec </span>litestream replicate  <span class="nt">-exec</span> <span class="s2">"rails s"</span>  storage/<span class="k">${</span><span class="nv">RAILS_ENV</span><span class="k">}</span>.sqlite3  <span class="k">${</span><span class="nv">REMOTE_S3_FILE</span><span class="k">}</span>/<span class="k">${</span><span class="nv">RAILS_ENV</span><span class="k">}</span>.sqlite3</code></pre></div></div><p>Что здесь происходит:</p><ol>  <li>    <p><strong>litestream restore</strong> — если локального файла <code class="language-plaintext highlighter-rouge">storage/production.sqlite3</code> нет, а в S3 есть бэкап, база скачивается. Флаги <code class="language-plaintext highlighter-rouge">-if-replica-exists</code> и <code class="language-plaintext highlighter-rouge">-if-db-not-exists</code> делают команду безопасной: она не упадёт, если бэкапа ещё нет (первый деплой), и не перезапишет существующую базу если она есть</p>  </li>  <li>    <p><strong>db:create db:migrate db:seed</strong> — классика Rails</p>  </li>  <li>    <p><strong>litestream replicate -exec “rails s”</strong> — <code class="language-plaintext highlighter-rouge">Litestream</code> запускается как supervisor. Он начинает реплицировать WAL в S3 и одновременно запускает дочерний процесс <code class="language-plaintext highlighter-rouge">rails s</code>. Если Rails упадёт, <code class="language-plaintext highlighter-rouge">Litestream</code> тоже завершится.</p>  </li></ol><p>Результат: контейнер можно остановить, удалить, пересоздать на другом хосте — база восстановится из S3 и продолжит реплицироваться. Красота!</p><h3 id="профит-и-подводные-камни">Профит и подводные камни</h3><p>Плюсы:</p><ul>  <li>Минимальная инфраструктура — один контейнер, без отдельной СУБД</li>  <li>Персистентность — данные не пропадают при удалении контейнера</li>  <li>Бэкап “из коробки” — каждая транзакция летит в S3, RPO стремится к нулю</li>  <li>Простое восстановление — <code class="language-plaintext highlighter-rouge">litestream restore </code>или автовосстановление при старте</li>  <li>Дёшево — <code class="language-plaintext highlighter-rouge">SQLite</code> бесплатна, object storage стоит копейки</li>  <li>Не требует изменений в коде приложения</li>  <li>Point-in-time recovery — можно восстановить базу на конкретный момент времени</li></ul><p>Минусы:</p><ul>  <li>Только один writer — <code class="language-plaintext highlighter-rouge">SQLite</code> поддерживает только одного писателя. Это значит, что приложение должно работать в единственном экземпляре (single-node). Ни о каком горизонтальном масштабировании речи не идёт, но в списке ссылок будет сюрприз :)</li>  <li>Зависимость от object storage — если S3 недоступен, <code class="language-plaintext highlighter-rouge">Litestream</code> накапливает WAL локально, но при длительном отключении может потребоваться внимание</li>  <li>Не замена PostgreSQL для высоконагруженных систем — для pet-проекта и небольших сервисов подходит идеально, для HA — нет, но можно посмотреть в ссылочки в конце статьи…</li></ul><p><code class="language-plaintext highlighter-rouge">SQLite</code> + <code class="language-plaintext highlighter-rouge">Litestream</code> — это мощная комбинация для «лёгкого» деплоя. Вы получаете полноценную реляционную базу данных в одном файле, непрерывный backup в S3 и автоматическое восстановление при перезапуске контейнера — без необходимости поднимать и обслуживать отдельную СУБД.</p><p>Для pet-проекта это часто идеальный баланс между простотой и надёжностью.</p><p>Пользуйтесь на здоровье! 🤝</p><h3 id="мои-любимые-ссылки-которые-стоит-почитать">Мои любимые ссылки, которые стоит почитать:</h3><ul>  <li><a href="https://litestream.io/">Litestream</a> и <a href="https://github.com/benbjohnson/litestream">GitHub</a> — официальный сайт и исходники</li>  <li>Вот он сюрприз: <a href="https://fly.io/docs/litefs/">LiteFS - Distributed SQLite</a> - как сделать, горизонтальное масштабирование с <code class="language-plaintext highlighter-rouge">SQLite</code>. Говорят, работает в проде :)</li>  <li>Ну и ссылочка на сам проект <a href="https://runit.rnds.pro/">Беговой Клуб -=БеГиМ=-</a></li></ul><hr /><tt-tags id="DhCr">    <tt-tag name="sqlite">#sqlite</tt-tag>    <tt-tag name="rails">#rails</tt-tag>    <tt-tag name="database">#database</tt-tag>    <tt-tag name="replicantion">#replicantion</tt-tag>  </tt-tags> ]]></content:encoded>
        <yandex:full-text>Иногда хочется, чтобы всё было просто. Взял Rails-приложение, завернул в Docker, запустил — и оно работает. Без отдельной СУБД, без монтирования хранилища для картинок и пр.Но вот незадача: если взять SQLite, положить её в контейнер и запустить — при docker rm ваша база улетает в никуда. Навсегда. Вместе с пользователями, настройками и всем, что вы так старательно назаполняли.Сегодня расскажу, как я в pet-проекте RuNiT решил эту проблему с помощью Litestream — и просто кайфую.ПроблемаКлассический путь деплоя pet-проекта выглядит так:  Поднять PostgreSQL в отдельном контейнере (найти под него постоянное хранилище и сервак)  Прокинуть volume  Понять, что вы тратите больше времени на администрирование СУБД, и хранилище картинок, чем на разработку фичДля pet-проекта это ощутимый оверхед. SQLite решает первый пункт, но создаёт два других:  файл базы живёт внутри контейнера и при пересоздании исчезает  бэкапов нет, и если сервер упал — всё, приплыли.Тут бы и сказать “ну, SQLite не для продакшена”, но мне как-то попадался проект Litestream - Streaming SQLite ReplicationLitestreamLitestream — это standalone-утилита (написана на Go, ~14k stars на GitHub) для потоковой репликации SQLite. Она следит за WAL (Write-Ahead Log) вашей базы и в реальном времени отправляет изменения в S3-совместимое хранилище: Yandex Object Storage, MinIO, RustFS и многие другие.Умеет:  Continuous backup — каждая транзакция стримится в облако почти мгновенно  Restore при старте — при пересоздании контейнера база автоматически восстанавливается из реплики  Point-in-time recovery — можно откатиться к состоянию на конкретную секунду.  Ноль изменений в коде приложения — Litestream работает как отдельный процесс.  Дёшево — object storage стоит копейки.Звучит как магия? По сути — да. Но магия очень простая и надёжная - разработчик говорит что работает в проде стабильно.Как запуститьТыкать надо тут следующее:Устанавливаем Litestream прямо в образ:#DockerfileRUN set -ex  &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; dpkg -i litestream.deb \  &amp;&amp; rm litestream.debПередаём credentials для S3 и URL реплики:services:  app:    environment:      - LITESTREAM_ACCESS_KEY_ID=${LITESTREAM_ACCESS_KEY_ID-key}      - LITESTREAM_SECRET_ACCESS_KEY=${LITESTREAM_SECRET_ACCESS_KEY-secret}      - REMOTE_S3_FILE=${REMOTE_S3_FILE-s3://11.11.11.11:9000/}Главный трюк — в docker/start_service.sh, который является CMD в Dockerfile:set -ex# 1. Восстановить базу из S3, если она не существует локальноlitestream restore  -o storage/${RAILS_ENV}.sqlite3  -if-replica-exists  -if-db-not-exists  ${REMOTE_S3_FILE}/${RAILS_ENV}.sqlite3# 2. Стандартные Rails шаги (idempotent — безопасны для существующей базы)bundle exec rails db:createbundle exec rails db:migratebundle exec rails db:seed# 3. Запускаем Rails ПОД управлением Litestreamexec bundle exec litestream replicate  -exec "rails s"  storage/${RAILS_ENV}.sqlite3  ${REMOTE_S3_FILE}/${RAILS_ENV}.sqlite3Что здесь происходит:      litestream restore — если локального файла storage/production.sqlite3 нет, а в S3 есть бэкап, база скачивается. Флаги -if-replica-exists и -if-db-not-exists делают команду безопасной: она не упадёт, если бэкапа ещё нет (первый деплой), и не перезапишет существующую базу если она есть        db:create db:migrate db:seed — классика Rails        litestream replicate -exec “rails s” — Litestream запускается как supervisor. Он начинает реплицировать WAL в S3 и одновременно запускает дочерний процесс rails s. Если Rails упадёт, Litestream тоже завершится.  Результат: контейнер можно остановить, удалить, пересоздать на другом хосте — база восстановится из S3 и продолжит реплицироваться. Красота!Профит и подводные камниПлюсы:  Минимальная инфраструктура — один контейнер, без отдельной СУБД  Персистентность — данные не пропадают при удалении контейнера  Бэкап “из коробки” — каждая транзакция летит в S3, RPO стремится к нулю  Простое восстановление — litestream restore или автовосстановление при старте  Дёшево — SQLite бесплатна, object storage стоит копейки  Не требует изменений в коде приложения  Point-in-time recovery — можно восстановить базу на конкретный момент времениМинусы:  Только один writer — SQLite поддерживает только одного писателя. Это значит, что приложение должно работать в единственном экземпляре (single-node). Ни о каком горизонтальном масштабировании речи не идёт, но в списке ссылок будет сюрприз :)  Зависимость от object storage — если S3 недоступен, Litestream накапливает WAL локально, но при длительном отключении может потребоваться внимание  Не замена PostgreSQL для высоконагруженных систем — для pet-проекта и небольших сервисов подходит идеально, для HA — нет, но можно посмотреть в ссылочки в конце статьи…SQLite + Litestream — это мощная комбинация для «лёгкого» деплоя. Вы получаете полноценную реляционную базу данных в одном файле, непрерывный backup в S3 и автоматическое восстановление при перезапуске контейнера — без необходимости поднимать и обслуживать отдельную СУБД.Для pet-проекта это часто идеальный баланс между простотой и надёжностью.Пользуйтесь на здоровье! 🤝Мои любимые ссылки, которые стоит почитать:  Litestream и GitHub — официальный сайт и исходники  Вот он сюрприз: LiteFS - Distributed SQLite - как сделать, горизонтальное масштабирование с SQLite. Говорят, работает в проде :)  Ну и ссылочка на сам проект Беговой Клуб -=БеГиМ=-    #sqlite    #rails    #database    #replicantion  </yandex:full-text>

        
        <category>other</category>
        
      </item>
    
      <item>
        <guid isPermaLink="true">https://kinnalru.github.io/2026/07/21/syncerman_ot_bash_skriptov_k_go_cli_dlya_pyati_oblakov_odnovremenno.html</guid>
        <link>https://kinnalru.github.io/2026/07/21/syncerman_ot_bash_skriptov_k_go_cli_dlya_pyati_oblakov_odnovremenno.html</link>
        <pdalink>https://kinnalru.github.io/2026/07/21/syncerman_ot_bash_skriptov_k_go_cli_dlya_pyati_oblakov_odnovremenno.html</pdalink>
        <dc:creator>kinnalru</dc:creator>
        <author>kinnalru@gmail.com (Самойленко Юрий)</author>
        <title>Syncerman: от bash-скриптов к Go cli для пяти облаков одновременно</title>
        <pubDate>Tue, 21 Jul 2026 18:46:22 +0300</pubDate>
        <yandex:genre>article</yandex:genre>
        
        <media:content medium="image" url="https://img3.teletype.in/files/a3/78/a378884e-5416-4669-b2fb-a001bce9d848.png"/>
        
        <description><![CDATA[ <p><img src="https://img3.teletype.in/files/a3/78/a378884e-5416-4669-b2fb-a001bce9d848.png" alt="" /></p><p>Иногда, чтобы решить свою проблему, приходится написать инструмент. А иногда — чтобы написать инструмент, приходится решить свою проблему. Короче, как обычно, всё началось не с идеи сделать утилиту, а с того, что добавилась пара важных документов (по иппотке) к существующему зоопарку данных, которые надо хранить.</p><h3 id="как-я-к-этому-пришел">Как я к этому пришел</h3><p>У меня давно назрела проблема: синхронизировать разные файлы с разных рабочих мест — по крайней мере дома и на работе. Документы (важные), сохранения из игр, книги для читалки. Ну и надёжный архив всего этого добра, потому что терять даже что-то незначительное это боль 🤕.</p><p>Просто положить всё в одно облако — хорошо, но нужен ещё постоянный процесс актуализации. <code class="language-plaintext highlighter-rouge">Яндекс Диск</code> работает, <code class="language-plaintext highlighter-rouge">Google Drive</code> может изменить условия, <code class="language-plaintext highlighter-rouge">Dropbox</code> может внезапно ограничить доступ. Поэтому идея была простая: <strong>хранить копии везде</strong>. <code class="language-plaintext highlighter-rouge">Яндекс Диск</code>, <code class="language-plaintext highlighter-rouge">Google Drive</code>, <code class="language-plaintext highlighter-rouge">Mail.ru Cloud</code>, <code class="language-plaintext highlighter-rouge">Dropbox</code> — чем больше, тем надёжнее. Только файлы, только хардкор!</p><p>Раньше я обходился консольным демоном <code class="language-plaintext highlighter-rouge">yandex-disk</code> — он работал, но только с Яндексом. Потом перешёл на самописные bash-скрипты, которые вызывали <a href="https://rclone.org/">rclone</a> напрямую. Это работало… до поры до времени. Конфигурация разрасталась, порядок синхронизации был важен (нельзя синкать из пустого облака в другое до того, как туда прилетели файлы) и всякое подобное. Проблема!</p><p>Когда появились нормальные ИИ-ассистенты для кодинга, я решил, что пора пробовать, а мне нужен нормальный инструмент. Declarative configuration, predictable order, automatic error handling и пр. Так родился <a href="https://gitlab.com/kinnalru/syncerman">Syncerman</a>.</p><h3 id="из-говна-и-палок-архитектура-и-технологии"><del>Из говна и палок</del> архитектура и технологии</h3><p><code class="language-plaintext highlighter-rouge">Syncerman</code> написан на <strong>Go</strong> — выбрал его только за простоту распространения (один бинарник).</p><p>В основе лежит <a href="https://rclone.org/bisync/">rclone bisync</a> — двусторонняя синхронизация с сохранением метаданных. <code class="language-plaintext highlighter-rouge">Syncerman</code> не пытается переизобрести колесо синхронизации, а выступает оркестратором над <code class="language-plaintext highlighter-rouge">rclone</code>, предоставляя удобный CLI и YAML-конфигурацию. Никаких серверов и БД.</p><h3 id="yaml-конфигурация">YAML-конфигурация</h3><p>Вместо папок с ссылками на bash-скрипты — чёткая структура:</p><div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">jobs</span><span class="pi">:</span>  <span class="na">important</span><span class="pi">:</span>    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Важное"</span>    <span class="na">priority</span><span class="pi">:</span> <span class="m">10</span>    <span class="na">tasks</span><span class="pi">:</span>      <span class="pi">-</span> <span class="na">from</span><span class="pi">:</span> <span class="s2">"</span><span class="s">local:/home/jerry/cloud/mirror/Documents"</span>        <span class="na">to</span><span class="pi">:</span>          <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">gdrive:folders/Documents"</span>          <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ydisk:folders/Documents"</span>      <span class="pi">-</span> <span class="na">from</span><span class="pi">:</span> <span class="s">local:Наследие</span>        <span class="na">to</span><span class="pi">:</span>          <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">gdrive:Наследие/</span>            <span class="na">args</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">--force"</span><span class="pi">]</span>          <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">ydisk:Наследие/</span>            <span class="na">args</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">--force"</span><span class="pi">]</span>          <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">mailru:Наследие/</span>            <span class="na">args</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">--force"</span><span class="pi">]</span>  <span class="na">games</span><span class="pi">:</span>    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">игрули"</span>    <span class="na">priority</span><span class="pi">:</span> <span class="m">20</span>    <span class="na">tasks</span><span class="pi">:</span>      <span class="pi">-</span> <span class="na">from</span><span class="pi">:</span> <span class="s2">"</span><span class="s">local:games_sync"</span>        <span class="na">to</span><span class="pi">:</span>          <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ydisk:folders/games_sync"</span></code></pre></div></div><p>Порядок <strong>tasks</strong> и <strong>to</strong> внутри массивов сохраняется строго. Это критично для цепочек: <code class="language-plaintext highlighter-rouge">local → Google Drive → Yandex Disk</code>. Без этого — катастрофа.</p><h3 id="проблемасики">Проблемасики</h3><p><code class="language-plaintext highlighter-rouge">rclone bisync</code> требует state-файлы для работы (они лежат где-то в папке пользователя). При первом запуске на новой паре путей он падает с ошибкой <em>“cannot find prior Path1 or Path2 listings”</em>. Syncerman детектирует это по двум паттернам в выводе и автоматически перезапускает синхронизацию с флагом <code class="language-plaintext highlighter-rouge">--resync</code>. Пользователь даже не замечает проблемы. Раньше делал руками.</p><p>На ранних этапах обнаружился критический баг: из-за использования map в Go порядок выполнения целей был недетерминированным. Для линейных цепочек это катастрофа: если сначала синкать из пустого <code class="language-plaintext highlighter-rouge">Yandex</code> в <code class="language-plaintext highlighter-rouge">Google</code>, а потом из локальной папки в <code class="language-plaintext highlighter-rouge">Google</code> — быть беде. Проблема! Так появились массивы.</p><h3 id="результат">Результат</h3><p><a href="https://gitlab.com/kinnalru/syncerman">Syncerman</a> версии **0.3.6** сейчас работает в production — то есть на моих машинах :) Само собой нужны пруфы, и их есть у меня!</p><ul>  <li>бинарники для Linux и Windows (сборка сразу подо всё)</li>  <li>GitLab CI как в лучших домах</li>  <li>Покрытие тестами - самому было бы лень писать а тут юнит-тесты для всех ключевых пакетов (config, rclone, sync), интеграционные тесты (не просто интеграционные а целые <strong>bash-сценарии с десятком шагов</strong> ).</li></ul><p>Публичные репозитории (это мой фетиш):</p><ul>  <li>Gitlab <a href="https://gitlab.com/kinnalru/syncerman">https://gitlab.com/kinnalru/syncerman</a></li>  <li>Github <a href="https://github.com/kinnalru/syncerman">https://github.com/kinnalru/syncerman</a></li>  <li>Gitflic <a href="https://gitflic.ru/project/kinnalru/syncerman">https://gitflic.ru/project/kinnalru/syncerman</a></li>  <li>Gitverse <a href="https://gitverse.ru/kinnalru/syncerman">https://gitverse.ru/kinnalru/syncerman</a></li></ul><h3 id="использование">Использование</h3><p>Мой <code class="language-plaintext highlighter-rouge">crontab</code> выглядит так:</p><div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Синхронизация документов и сохранений 2 рааз в час и лог для ручной проверки</span>10,30 <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> /bin/sh <span class="nt">-lc</span> <span class="s2">"cd /home/jerry/cloud/mirror &amp;&amp; syncerman sync 2&gt;&amp;1 | tee ./lastrun.log"</span></code></pre></div></div><p>Конфигурация зеркалирует локальные папки во все облака (настроенные в <code class="language-plaintext highlighter-rouge">rclone</code>) одновременно, а затем синхронизирует облака между собой. Если одно облако отвалится — данные остаются в остальных. Если <code class="language-plaintext highlighter-rouge">rclone</code> выдаст <code class="language-plaintext highlighter-rouge">first-run</code> ошибку — <code class="language-plaintext highlighter-rouge">Syncerman</code> сам всё починит.</p><h3 id="вместо-вывода">Вместо вывода</h3><p>Появление ИИ-ассистентов снизило порог входа для написания таких инструментов. Раньше мне было лень писать то, что и так работает с помощью пары строк на bash. А сейчас это делаю не я :) — разбор выхлопов <code class="language-plaintext highlighter-rouge">rclone</code>, обработка edge cases и тестирование делаются очень легко. Я могу заняться решением проблем (архитектурой и требованиями), а рутина “делается сама”.</p><hr /><p>Второй фетиш это ссылочки:</p><ul>  <li><a href="https://rclone.org/bisync/">rclone bisync</a> - это база</li>  <li><a href="https://github.com/bcpierce00/unison">unison</a> - еще одна классная штука по теме синхронизации, она прочно заняла место в моём сердце, но об это в другой раз</li></ul> ]]></description>
        <content:encoded><![CDATA[ <p><img src="https://img3.teletype.in/files/a3/78/a378884e-5416-4669-b2fb-a001bce9d848.png" alt="" /></p><p>Иногда, чтобы решить свою проблему, приходится написать инструмент. А иногда — чтобы написать инструмент, приходится решить свою проблему. Короче, как обычно, всё началось не с идеи сделать утилиту, а с того, что добавилась пара важных документов (по иппотке) к существующему зоопарку данных, которые надо хранить.</p><h3 id="как-я-к-этому-пришел">Как я к этому пришел</h3><p>У меня давно назрела проблема: синхронизировать разные файлы с разных рабочих мест — по крайней мере дома и на работе. Документы (важные), сохранения из игр, книги для читалки. Ну и надёжный архив всего этого добра, потому что терять даже что-то незначительное это боль 🤕.</p><p>Просто положить всё в одно облако — хорошо, но нужен ещё постоянный процесс актуализации. <code class="language-plaintext highlighter-rouge">Яндекс Диск</code> работает, <code class="language-plaintext highlighter-rouge">Google Drive</code> может изменить условия, <code class="language-plaintext highlighter-rouge">Dropbox</code> может внезапно ограничить доступ. Поэтому идея была простая: <strong>хранить копии везде</strong>. <code class="language-plaintext highlighter-rouge">Яндекс Диск</code>, <code class="language-plaintext highlighter-rouge">Google Drive</code>, <code class="language-plaintext highlighter-rouge">Mail.ru Cloud</code>, <code class="language-plaintext highlighter-rouge">Dropbox</code> — чем больше, тем надёжнее. Только файлы, только хардкор!</p><p>Раньше я обходился консольным демоном <code class="language-plaintext highlighter-rouge">yandex-disk</code> — он работал, но только с Яндексом. Потом перешёл на самописные bash-скрипты, которые вызывали <a href="https://rclone.org/">rclone</a> напрямую. Это работало… до поры до времени. Конфигурация разрасталась, порядок синхронизации был важен (нельзя синкать из пустого облака в другое до того, как туда прилетели файлы) и всякое подобное. Проблема!</p><p>Когда появились нормальные ИИ-ассистенты для кодинга, я решил, что пора пробовать, а мне нужен нормальный инструмент. Declarative configuration, predictable order, automatic error handling и пр. Так родился <a href="https://gitlab.com/kinnalru/syncerman">Syncerman</a>.</p><h3 id="из-говна-и-палок-архитектура-и-технологии"><del>Из говна и палок</del> архитектура и технологии</h3><p><code class="language-plaintext highlighter-rouge">Syncerman</code> написан на <strong>Go</strong> — выбрал его только за простоту распространения (один бинарник).</p><p>В основе лежит <a href="https://rclone.org/bisync/">rclone bisync</a> — двусторонняя синхронизация с сохранением метаданных. <code class="language-plaintext highlighter-rouge">Syncerman</code> не пытается переизобрести колесо синхронизации, а выступает оркестратором над <code class="language-plaintext highlighter-rouge">rclone</code>, предоставляя удобный CLI и YAML-конфигурацию. Никаких серверов и БД.</p><h3 id="yaml-конфигурация">YAML-конфигурация</h3><p>Вместо папок с ссылками на bash-скрипты — чёткая структура:</p><div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">jobs</span><span class="pi">:</span>  <span class="na">important</span><span class="pi">:</span>    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Важное"</span>    <span class="na">priority</span><span class="pi">:</span> <span class="m">10</span>    <span class="na">tasks</span><span class="pi">:</span>      <span class="pi">-</span> <span class="na">from</span><span class="pi">:</span> <span class="s2">"</span><span class="s">local:/home/jerry/cloud/mirror/Documents"</span>        <span class="na">to</span><span class="pi">:</span>          <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">gdrive:folders/Documents"</span>          <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ydisk:folders/Documents"</span>      <span class="pi">-</span> <span class="na">from</span><span class="pi">:</span> <span class="s">local:Наследие</span>        <span class="na">to</span><span class="pi">:</span>          <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">gdrive:Наследие/</span>            <span class="na">args</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">--force"</span><span class="pi">]</span>          <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">ydisk:Наследие/</span>            <span class="na">args</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">--force"</span><span class="pi">]</span>          <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">mailru:Наследие/</span>            <span class="na">args</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">--force"</span><span class="pi">]</span>  <span class="na">games</span><span class="pi">:</span>    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">игрули"</span>    <span class="na">priority</span><span class="pi">:</span> <span class="m">20</span>    <span class="na">tasks</span><span class="pi">:</span>      <span class="pi">-</span> <span class="na">from</span><span class="pi">:</span> <span class="s2">"</span><span class="s">local:games_sync"</span>        <span class="na">to</span><span class="pi">:</span>          <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ydisk:folders/games_sync"</span></code></pre></div></div><p>Порядок <strong>tasks</strong> и <strong>to</strong> внутри массивов сохраняется строго. Это критично для цепочек: <code class="language-plaintext highlighter-rouge">local → Google Drive → Yandex Disk</code>. Без этого — катастрофа.</p><h3 id="проблемасики">Проблемасики</h3><p><code class="language-plaintext highlighter-rouge">rclone bisync</code> требует state-файлы для работы (они лежат где-то в папке пользователя). При первом запуске на новой паре путей он падает с ошибкой <em>“cannot find prior Path1 or Path2 listings”</em>. Syncerman детектирует это по двум паттернам в выводе и автоматически перезапускает синхронизацию с флагом <code class="language-plaintext highlighter-rouge">--resync</code>. Пользователь даже не замечает проблемы. Раньше делал руками.</p><p>На ранних этапах обнаружился критический баг: из-за использования map в Go порядок выполнения целей был недетерминированным. Для линейных цепочек это катастрофа: если сначала синкать из пустого <code class="language-plaintext highlighter-rouge">Yandex</code> в <code class="language-plaintext highlighter-rouge">Google</code>, а потом из локальной папки в <code class="language-plaintext highlighter-rouge">Google</code> — быть беде. Проблема! Так появились массивы.</p><h3 id="результат">Результат</h3><p><a href="https://gitlab.com/kinnalru/syncerman">Syncerman</a> версии **0.3.6** сейчас работает в production — то есть на моих машинах :) Само собой нужны пруфы, и их есть у меня!</p><ul>  <li>бинарники для Linux и Windows (сборка сразу подо всё)</li>  <li>GitLab CI как в лучших домах</li>  <li>Покрытие тестами - самому было бы лень писать а тут юнит-тесты для всех ключевых пакетов (config, rclone, sync), интеграционные тесты (не просто интеграционные а целые <strong>bash-сценарии с десятком шагов</strong> ).</li></ul><p>Публичные репозитории (это мой фетиш):</p><ul>  <li>Gitlab <a href="https://gitlab.com/kinnalru/syncerman">https://gitlab.com/kinnalru/syncerman</a></li>  <li>Github <a href="https://github.com/kinnalru/syncerman">https://github.com/kinnalru/syncerman</a></li>  <li>Gitflic <a href="https://gitflic.ru/project/kinnalru/syncerman">https://gitflic.ru/project/kinnalru/syncerman</a></li>  <li>Gitverse <a href="https://gitverse.ru/kinnalru/syncerman">https://gitverse.ru/kinnalru/syncerman</a></li></ul><h3 id="использование">Использование</h3><p>Мой <code class="language-plaintext highlighter-rouge">crontab</code> выглядит так:</p><div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Синхронизация документов и сохранений 2 рааз в час и лог для ручной проверки</span>10,30 <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> /bin/sh <span class="nt">-lc</span> <span class="s2">"cd /home/jerry/cloud/mirror &amp;&amp; syncerman sync 2&gt;&amp;1 | tee ./lastrun.log"</span></code></pre></div></div><p>Конфигурация зеркалирует локальные папки во все облака (настроенные в <code class="language-plaintext highlighter-rouge">rclone</code>) одновременно, а затем синхронизирует облака между собой. Если одно облако отвалится — данные остаются в остальных. Если <code class="language-plaintext highlighter-rouge">rclone</code> выдаст <code class="language-plaintext highlighter-rouge">first-run</code> ошибку — <code class="language-plaintext highlighter-rouge">Syncerman</code> сам всё починит.</p><h3 id="вместо-вывода">Вместо вывода</h3><p>Появление ИИ-ассистентов снизило порог входа для написания таких инструментов. Раньше мне было лень писать то, что и так работает с помощью пары строк на bash. А сейчас это делаю не я :) — разбор выхлопов <code class="language-plaintext highlighter-rouge">rclone</code>, обработка edge cases и тестирование делаются очень легко. Я могу заняться решением проблем (архитектурой и требованиями), а рутина “делается сама”.</p><hr /><p>Второй фетиш это ссылочки:</p><ul>  <li><a href="https://rclone.org/bisync/">rclone bisync</a> - это база</li>  <li><a href="https://github.com/bcpierce00/unison">unison</a> - еще одна классная штука по теме синхронизации, она прочно заняла место в моём сердце, но об это в другой раз</li></ul> ]]></content:encoded>
        <yandex:full-text>Иногда, чтобы решить свою проблему, приходится написать инструмент. А иногда — чтобы написать инструмент, приходится решить свою проблему. Короче, как обычно, всё началось не с идеи сделать утилиту, а с того, что добавилась пара важных документов (по иппотке) к существующему зоопарку данных, которые надо хранить.Как я к этому пришелУ меня давно назрела проблема: синхронизировать разные файлы с разных рабочих мест — по крайней мере дома и на работе. Документы (важные), сохранения из игр, книги для читалки. Ну и надёжный архив всего этого добра, потому что терять даже что-то незначительное это боль 🤕.Просто положить всё в одно облако — хорошо, но нужен ещё постоянный процесс актуализации. Яндекс Диск работает, Google Drive может изменить условия, Dropbox может внезапно ограничить доступ. Поэтому идея была простая: хранить копии везде. Яндекс Диск, Google Drive, Mail.ru Cloud, Dropbox — чем больше, тем надёжнее. Только файлы, только хардкор!Раньше я обходился консольным демоном yandex-disk — он работал, но только с Яндексом. Потом перешёл на самописные bash-скрипты, которые вызывали rclone напрямую. Это работало… до поры до времени. Конфигурация разрасталась, порядок синхронизации был важен (нельзя синкать из пустого облака в другое до того, как туда прилетели файлы) и всякое подобное. Проблема!Когда появились нормальные ИИ-ассистенты для кодинга, я решил, что пора пробовать, а мне нужен нормальный инструмент. Declarative configuration, predictable order, automatic error handling и пр. Так родился Syncerman.Из говна и палок архитектура и технологииSyncerman написан на Go — выбрал его только за простоту распространения (один бинарник).В основе лежит rclone bisync — двусторонняя синхронизация с сохранением метаданных. Syncerman не пытается переизобрести колесо синхронизации, а выступает оркестратором над rclone, предоставляя удобный CLI и YAML-конфигурацию. Никаких серверов и БД.YAML-конфигурацияВместо папок с ссылками на bash-скрипты — чёткая структура:jobs:  important:    name: "Важное"    priority: 10    tasks:      - from: "local:/home/jerry/cloud/mirror/Documents"        to:          - path: "gdrive:folders/Documents"          - path: "ydisk:folders/Documents"      - from: local:Наследие        to:          - path: gdrive:Наследие/            args: ["--force"]          - path: ydisk:Наследие/            args: ["--force"]          - path: mailru:Наследие/            args: ["--force"]  games:    name: "игрули"    priority: 20    tasks:      - from: "local:games_sync"        to:          - path: "ydisk:folders/games_sync"Порядок tasks и to внутри массивов сохраняется строго. Это критично для цепочек: local → Google Drive → Yandex Disk. Без этого — катастрофа.Проблемасикиrclone bisync требует state-файлы для работы (они лежат где-то в папке пользователя). При первом запуске на новой паре путей он падает с ошибкой “cannot find prior Path1 or Path2 listings”. Syncerman детектирует это по двум паттернам в выводе и автоматически перезапускает синхронизацию с флагом --resync. Пользователь даже не замечает проблемы. Раньше делал руками.На ранних этапах обнаружился критический баг: из-за использования map в Go порядок выполнения целей был недетерминированным. Для линейных цепочек это катастрофа: если сначала синкать из пустого Yandex в Google, а потом из локальной папки в Google — быть беде. Проблема! Так появились массивы.РезультатSyncerman версии **0.3.6** сейчас работает в production — то есть на моих машинах :) Само собой нужны пруфы, и их есть у меня!  бинарники для Linux и Windows (сборка сразу подо всё)  GitLab CI как в лучших домах  Покрытие тестами - самому было бы лень писать а тут юнит-тесты для всех ключевых пакетов (config, rclone, sync), интеграционные тесты (не просто интеграционные а целые bash-сценарии с десятком шагов ).Публичные репозитории (это мой фетиш):  Gitlab https://gitlab.com/kinnalru/syncerman  Github https://github.com/kinnalru/syncerman  Gitflic https://gitflic.ru/project/kinnalru/syncerman  Gitverse https://gitverse.ru/kinnalru/syncermanИспользованиеМой crontab выглядит так:# Синхронизация документов и сохранений 2 рааз в час и лог для ручной проверки10,30 * * * * /bin/sh -lc "cd /home/jerry/cloud/mirror &amp;&amp; syncerman sync 2&gt;&amp;1 | tee ./lastrun.log"Конфигурация зеркалирует локальные папки во все облака (настроенные в rclone) одновременно, а затем синхронизирует облака между собой. Если одно облако отвалится — данные остаются в остальных. Если rclone выдаст first-run ошибку — Syncerman сам всё починит.Вместо выводаПоявление ИИ-ассистентов снизило порог входа для написания таких инструментов. Раньше мне было лень писать то, что и так работает с помощью пары строк на bash. А сейчас это делаю не я :) — разбор выхлопов rclone, обработка edge cases и тестирование делаются очень легко. Я могу заняться решением проблем (архитектурой и требованиями), а рутина “делается сама”.Второй фетиш это ссылочки:  rclone bisync - это база  unison - еще одна классная штука по теме синхронизации, она прочно заняла место в моём сердце, но об это в другой раз</yandex:full-text>

        
      </item>
    
      <item>
        <guid isPermaLink="true">https://kinnalru.github.io/2025/02/11/devsecops_podkralsya_nezametno_hotya_zameten_byl_izdaleka.html</guid>
        <link>https://kinnalru.github.io/2025/02/11/devsecops_podkralsya_nezametno_hotya_zameten_byl_izdaleka.html</link>
        <pdalink>https://kinnalru.github.io/2025/02/11/devsecops_podkralsya_nezametno_hotya_zameten_byl_izdaleka.html</pdalink>
        <dc:creator>kinnalru</dc:creator>
        <author>kinnalru@gmail.com (Самойленко Юрий)</author>
        <title>DevSecOps подкрался незаметно, хотя заметен был издалека…</title>
        <pubDate>Tue, 11 Feb 2025 15:34:47 +0300</pubDate>
        <yandex:genre>article</yandex:genre>
        
        <media:content medium="image" url="https://img1.teletype.in/files/42/f0/42f0e9b3-d3f8-403f-ac95-bfd789c92278.jpeg"/>
        
        <description><![CDATA[ <p><img src="https://img1.teletype.in/files/42/f0/42f0e9b3-d3f8-403f-ac95-bfd789c92278.jpeg" alt="" /></p><p>DevSecOps уверенно шагает по нашей индустрии, и горе тому, кто попадёт под его поступь… Эта статья про <del>ультимативную</del> сборку базовых образов для Ruby для удовлетворения самых параноидальных потребностей ИБ. Да, именно об этом мы и расскажем - что такое “инсталляция ruby”, где, что, почему лежит и как с этим жить нашему пайплайну сборки и самому приложению.</p><section style="background-color:hsl(hsl(24, 24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">    <p id="CaKt">Статья подойдёт тем, кто хочет более глубоко понимать, какие процессы происходят в системе, когда вызывается <code>gem install</code>, <code>bundle install</code> или (не дай Бог) <code>gem update –system</code></p>  </section><h2 id="как-было-до">Как было до?</h2><p>С самого начала появления docker мы в RNDSOFT использовали парадигму “всё включено”. Для нас это означало, что в образ мы включаем всё, что нам надо не только для продуктовой эксплуатации, но и для проведения всех тестов. При прохождении CI пайплайна на следующие стадии продвигался образ целиком и, в конце концов, выкатывался на прод.</p><p>Это было очень удобно и позволяло быть максимально (насколько это вообще возможно) уверенным в работоспособности, однако имело ряд серьёзных минусов, с которыми мы прекрасно жили достаточно долгое время:</p><ul>  <li>размер образа был большим - от 1 ГБ;</li>  <li>образ включал большое количество неиспользуемого (в проде) ПО.</li></ul><h2 id="сканеры-сканировали-сканировали-да-не-высканировали">Сканеры сканировали, сканировали, да не высканировали…</h2><p>И вот однажды один (на данный момент уже далеко не один) наш клиент захотел устранения всех замечаний, которые смог выявить сканнер <a href="https://github.com/aquasecurity/trivy">Trivy</a>. А их, как не трудно догадаться, было достаточно много. И главная проблема заключается в том, что большая часть замечаний никак не связана с непосредственными зависимостями вашего приложения (теми, которые фиксируются в Gemfile.lock). <br />Так откуда же они берутся?</p><p>Если коротко отвечать - отовсюду :) При сборке проекта необходимо поставить его зависимости - это план минимум. Кроме того, часто появляется необходимость установить bundle определенной версии или даже обновить ruby целиком, выполнив команду <a href="https://guides.rubygems.org/command-reference/#gem-update">gem update –system</a>. В результате этих действий в ваш образ в разнообразные папки ставятся разнообразные гемы, но</p><section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">    <p id="gt1O">старые версии гемов и сама базовая "инсталляция ruby" остаётся в системе со всеми своими "устаревшими" и "уязвимыми" версиями. И эти версии очень нравятся сканерам для того, чтобы поднять тревогу. </p>  </section><p>И еще два слова о том, что же именно сканирует Trivy (касательно ruby конечно):</p><ul>  <li>сканирует <a href="https://guides.rubygems.org/specification-reference/">.gemspec файлы</a> на всей файловой системе и не важно, установлен гем или нет - если в .gemspec указана версия “с уязвимостью” - алярм;</li>  <li>сканирует .gem файлы на всей файловой системе и не важно, установлен гем, лежит просто в папке cache, используется или нет в вашем приложении.</li></ul><p>Значит для удовлетворения хотелок сканера, надо сделать так, чтоб нигде не лежало ничего лишнего или не используемого, <strong>включая</strong> <a href="https://docs.ruby-lang.org/en/3.2/standard_library_rdoc.html">stdlib</a> - стандартную библиотеку ruby.</p><p>Проблема ясна, задача очевидна - можно приступать к решению и начинать надо с базовых образов. Их надо собрать так, чтобы сами базовые образы уже не содержали никаких уязвимостей.</p><p><img src="https://img3.teletype.in/files/e1/7a/e17a3417-a64b-4027-adf2-971f332cf9dd.png" alt="" /><em>Немного удручающее зрелище, особенно для ИБ клиента</em></p><h2 id="что-такое-инсталляция-ruby">Что такое инсталляция ruby?</h2><p>Если мы возьмём любой дистрибутив с установленным там ruby, то увидим, что само ruby будет находиться где-то в районе:</p><ul>  <li>/usr/lib/ruby - например в alpine после <code class="language-plaintext highlighter-rouge">apk add ruby</code></li>  <li>/usr/local/lib/ruby - например в <code class="language-plaintext highlighter-rouge">ruby:alpine</code>, где руби собирается отдельно от apk</li></ul><p>А дальше начинается интересное. Если посмотреть в финальный образ, в котором сделано много различных операций (обновление ruby, установка гемов через <code class="language-plaintext highlighter-rouge">gem install</code>, установка через <code class="language-plaintext highlighter-rouge">bundle install</code>), то в корневой папке ruby обнаружится несколько папок, по которым тем или иным способом будут распределены установленные вами гемы:</p><div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/usr/local/lib/ruby├── 3.2.0├── gems/3.2.0├── site_ruby/3.2.0└── vendor_ruby/3.2.0</code></pre></div></div><p>Сразу добавим к этому списку другие папки (которые можно посмотреть в выводе команды <code class="language-plaintext highlighter-rouge">gem env</code>):</p><div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="pi">-</span> <span class="na">INSTALLATION DIRECTORY</span><span class="pi">:</span> <span class="s">/usr/local/bundle</span> <span class="pi">-</span> <span class="na">USER INSTALLATION DIRECTORY</span><span class="pi">:</span> <span class="s">/root/.local/share/gem/ruby/3.3.0</span> <span class="pi">-</span> <span class="na">SPEC CACHE DIRECTORY</span><span class="pi">:</span> <span class="s">/root/.cache/gem/specs</span> <span class="pi">-</span> <span class="na">GEM PATHS</span><span class="pi">:</span>    <span class="pi">-</span> <span class="s">/usr/lib/ruby/gems/3.3.0</span>    <span class="pi">-</span> <span class="s">/root/.local/share/gem/ruby/3.3.0</span></code></pre></div></div><p>И вишенкой на торте будет настройка вашего bundle, если вы используете кеширование сборки (<a href="https://bundler.io/man/bundle-cache.1.html">bundle cache</a> или <code class="language-plaintext highlighter-rouge">bundle config set cache_path vendor/cache</code>), и используемые в этом зоопарке переменные окружения <code class="language-plaintext highlighter-rouge">GEM_HOME</code>, <code class="language-plaintext highlighter-rouge">BUNDLE_CACHE_PATH</code>, <code class="language-plaintext highlighter-rouge">BUNDLE_PATH</code> и скорее всего еще какие-то скрытые в глубинах экосистемы ruby.</p><p>Немного путано и в результате беспорядочных <del>связей</del> установок и обновлений во всех этих папках могут (и будут) появляться гемы. Надо исправить!</p><p>Постараюсь дать верхнеуровневое описание, что же именно это за папочки, не углубляясь в подробности и исключения:</p><h3 id="usrlocallibruby330">/usr/local/lib/ruby/3.3.0</h3><p>Именно в этой папке установлены основные файлы ruby и stdlib, а также компилируемые расширения в папке x86_64-linux-musl (в нашем случае собранные alpine для x86_64). Эти файлы принадлежат условной “системе” и никак не будут изменяться при дальнейших модификациях, например при установке или обновлении гемов через <code class="language-plaintext highlighter-rouge">gem install</code>. Вместо этого библиотеки будут ставиться в папку <code class="language-plaintext highlighter-rouge">/usr/local/lib/ruby/gems/3.3.0</code></p><h3 id="usrlocallibrubygems330">/usr/local/lib/ruby/gems/3.3.0</h3><p>Тут собрано всё, что вы ставите (без модификации <code class="language-plaintext highlighter-rouge">GEM_PATH</code>) командами <code class="language-plaintext highlighter-rouge">gem install,</code> включая компилируемые расширения.</p><h3 id="usrlocallibrubyvendor_ruby330">/usr/local/lib/ruby/vendor_ruby/3.3.0</h3><p>Сюда <strong>должны</strong> ставиться дополнительные гемы и/или патчи от команды мейнтейнеров дистрибутива. Об этом сложно найти информацию, но несколько слов есть в книге <code class="language-plaintext highlighter-rouge">The Ruby Programming Language</code> или, что интереснее, в changelog для <a href="https://docs.ruby-lang.org/en/2.3.0/NEWS-1_8_7.html">NEWS for Ruby 1.8.7</a> (да, окаменелое…):</p><section style="background-color:hsl(hsl(24, 24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">    <p id="VAZR"><strong>vendor_ruby</strong> directory<br />A new library directory named <code>vendor_ruby</code> is introduced in addition to <code>site_ruby</code>. The idea is to separate libraries installed by the package system (<code>vendor</code>) from manually (<code>site</code>) installed libraries preventing the former from getting overwritten by the latter, while preserving the user option to override vendor libraries with site libraries. (<code>site_ruby</code> takes precedence over <code>vendor_ruby</code>)<br />If you are a package maintainer, make each library package configure the library passing the <code>--vendor</code> option to <code>extconf.rb</code> so that the library files will get installed under <code>vendor_ruby</code>.<br />You can change the directory locations using configure options such as<code> --with-sitedir=DIR</code> and <code>--with-vendordir=DIR</code>.</p>  </section><h3 id="usrlocallibrubysite_ruby330">/usr/local/lib/ruby/site_ruby/3.3.0</h3><p>Сюда будут ставиться “системные” файлы, но не от OS, а от самого ruby, например после выполнения <code class="language-plaintext highlighter-rouge">gem update –system</code>:</p><div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>site_ruby/└── 3.4.0   ├── bundler   ├── rubygems   └── x86_64-linux-musl</code></pre></div></div><h3 id="usrlocallibrubygems330specifications">/usr/local/lib/ruby/gems/3.3.0/specifications/</h3><p>…а также INSTALLATION DIRECTORY <code class="language-plaintext highlighter-rouge">/usr/local/bundle/specifications</code>…<br />…а также USER INSTALLATION DIRECTORY: <code class="language-plaintext highlighter-rouge">/root/.local/share/gem/ruby/3.3.0</code>…<br />…а также SPEC CACHE DIRECTORY <code class="language-plaintext highlighter-rouge">/root/.cache/gem/specs</code>…<br />…а также BUNDLE_CACHE_PATH …</p><p>Сюда ставятся .gemspec файлы, которые собственно и говорят пакетному менеджеру ruby (gem и bundle), какие именно версии каких гемов установлены.</p><h3 id="usrlocallibrubygems330specificationsdefault">/usr/local/lib/ruby/gems/3.3.0/specifications/default</h3><p>Default, Карл… Default - это особое состояние гема, и эти гемы нельзя удалить. Если вы обновили гем до новой версии, то старая всё равно останется, и Trivy вам этого не простит. Весьма вредная папка с точки зрения сканирования.</p><h3 id="usrlocallibrubygems330cache">/usr/local/lib/ruby/gems/3.3.0/cache/</h3><p>…а также INSTALLATION DIRECTORY <code class="language-plaintext highlighter-rouge">/usr/local/bundle/cache</code>…<br />…а также BUNDLE_PATH …</p><p>Сюда пакетный менеджер ruby (gem или bundle) скачивает гемы (допустим вы ставите faraday.gem) перед установкой. Этот кеш очень часто используется для ускорения сборки, например <a href="https://docs.gitlab.com/ee/ci/caching/#cache-ruby-dependencies">в вашем Gitlab</a>.</p><h2 id="что-здесь-у-вас-происходит">Что здесь у вас происходит?!</h2><p>После того как мы в процессе исследования детально разобрались и увидели всё это многообразие мест, где могут находиться файлы, вызывающие панические атаки у Trivy, мы решили радикально решить эту проблему: свести все файлы, все гемы и все спеки (.gemspec) в одно место. Это позволит легко следить за всеми (всеми!) фактическими зависимостями и эффективно пользоваться командами <code class="language-plaintext highlighter-rouge">gem cleanup</code> и <code class="language-plaintext highlighter-rouge">bundle clean</code>. Тут надо отметить, что для вашей рабочей OS (системы общего назначения) такое решение приведёт к поломке системного пакетного менеджера (<a href="https://ru.wikipedia.org/wiki/Portage">Gentoo Portage</a>, <a href="https://ru.wikipedia.org/wiki/Dpkg">dpkg/apt</a>, <a href="https://ru.wikipedia.org/wiki/RPM">rpm/zypper</a> и пр.), но мы ведь говорим о конкретной сборке ruby под ваш конкретный проект - и тут никаких проблем не будет.</p><section style="background-color:hsl(hsl(24, 24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">    <p id="ma0i">Для того чтобы узнать что куда, когда и зачем ставится, мы использовали git прямо на корне файловой системы внутри контейнера (ruby:alpine, просто alpine, ruby:debian и другие образы для сравнения) и фиксировали изменения после различных команд ⏳</p>  </section><p>Но это еще не всё. Остаётся еще проблема с default gemspec, но её относительно легко решить:</p><div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mv</span> /usr/local/lib/ruby/gems/3.3.0/specifications/default/<span class="k">*</span> /usr/local/lib/ruby/gems/3.3.0/specifications/</code></pre></div></div><p>Теперь мы можем сформулировать План:</p><ul>  <li>Все дороги ведут в Рим - делаем ссылочки для <code class="language-plaintext highlighter-rouge">vendor_ruby</code>, <code class="language-plaintext highlighter-rouge">site_ruby</code> и пр. Также явно прописываем системные переменные <code class="language-plaintext highlighter-rouge">GEM_HOME</code> и <code class="language-plaintext highlighter-rouge">BUNDLE_APP_CONFIG.</code></li>  <li>Разбираемся с default gems.</li>  <li>Обновляем ruby (имеется в виду stdlib) до последней требуемой версии.</li>  <li>Обновляем bundle до нужной версии.</li>  <li>Удаляем лишние гемы (например rdoc).</li>  <li>Переставляем (!) системные (на текущий момент сборки базового образа - <strong>все</strong> ) гемы, потому что, как оказалось, <code class="language-plaintext highlighter-rouge">mv</code> для default gemspec имеет не очень хорошие последствия.</li>  <li>profit!</li></ul><p>Сказано - сделано, и добро пожаловать под кат!</p><div class="language-docker highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ARG</span><span class="s"> BASE_RUBY=3.2</span><span class="k">ARG</span><span class="s"> BASE_ALPINE=alpine3.16</span><span class="k">ARG</span><span class="s"> BASE_IMAGE=ruby:${BASE_RUBY}-${BASE_ALPINE}</span><span class="k">FROM</span><span class="s"> ${BASE_IMAGE}</span><span class="k">ARG</span><span class="s"> BASE_RUBY=3.2</span><span class="k">ARG</span><span class="s"> RUBYGEMS_VERSION=3.5.20</span><span class="k">ARG</span><span class="s"> BUNDLER_VERSION=2.5.20</span><span class="c"># эта переменная есть в старом alpine но нет в debian и новом</span><span class="c"># добавляем потому что она очень нужна для работы с папочками</span><span class="k">ENV</span><span class="s"> RUBY_MAJOR=${BASE_RUBY}</span><span class="k">ENV</span><span class="s"> RUBYGEMS_VERSION=${RUBYGEMS_VERSION} \</span>    BUNDLER_VERSION=${BUNDLER_VERSION}<span class="k">RUN </span>apk update <span class="o">&amp;&amp;</span> apk upgrade<span class="c"># Это наш костыльный скрипт который удаляет всякие лишние кеши,</span><span class="c"># man-файлы и прочий мусор. </span><span class="k">COPY</span><span class="s"> common/scripts/cleanallbuilds.sh /usr/bin/</span><span class="c"># dumb-init всегда используем как PID-1 но это немного другая история</span><span class="k">RUN </span><span class="nb">set</span> <span class="nt">-ex</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> apk add dumb-init <span class="se">\</span> <span class="o">&amp;&amp;</span> cleanallbuilds.sh<span class="c">###### </span><span class="c"># Надругиваемся над диструбутивом, чтоб иметь строго</span><span class="c"># одну версию руби в систему и управлять ею целиком через gem/bundle</span><span class="c"># все пути ведут в Рим</span><span class="k">ENV</span><span class="s"> GEM_HOME=/usr/local/lib/ruby/gems/${RUBY_MAJOR}.0/</span><span class="k">ENV</span><span class="s"> BUNDLE_APP_CONFIG=/usr/local/lib/ruby/gems/${RUBY_MAJOR}.0/</span><span class="c"># Сводим vendor_ruby, site_ruby и GEM_HOME в одно место</span><span class="k">RUN </span><span class="nb">set</span> <span class="nt">-ex</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">rm</span> <span class="nt">-rf</span> /usr/local/lib/ruby/site_ruby /usr/local/lib/ruby/vendor_ruby <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">ln</span> <span class="nt">-sf</span> /usr/local/lib/ruby /usr/local/lib/ruby/site_ruby <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">ln</span> <span class="nt">-sf</span> /usr/local/lib/ruby /usr/local/lib/ruby/vendor_ruby <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">mkdir</span> <span class="nt">-p</span> /root/.local/share/gem/ruby/ <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">ln</span> <span class="nt">-sf</span> <span class="k">${</span><span class="nv">GEM_HOME</span><span class="k">}</span> /root/.local/share/gem/ruby/<span class="k">${</span><span class="nv">RUBY_MAJOR</span><span class="k">}</span>.0<span class="c"># Чутка тюним bundle config чтоб в дальнейшем не забыть</span><span class="k">RUN </span><span class="nb">set</span> <span class="nt">-ex</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> bundle config <span class="nt">--local</span> disable_version_check <span class="nb">true</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> bundle config <span class="nt">--local</span> clean <span class="nb">false</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> bundle config <span class="nt">--local</span> no_prune <span class="nb">false</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> bundle config <span class="nt">--local</span> disable_local_branch_check <span class="nb">true</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> bundle config <span class="nt">--local</span> <span class="nb">jobs </span>2 <span class="se">\</span> <span class="o">&amp;&amp;</span> bundle config <span class="nt">--local</span> allow_offline_install <span class="nb">true</span><span class="c"># настраиваем .gemrc чтоб не было ничего лишнего, вклюячая rdoc</span><span class="k">RUN </span><span class="nb">set</span> <span class="nt">-ex</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s1">'gem: --no-document'</span> <span class="o">&gt;</span> /usr/local/etc/gemrc <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s1">'update_sources: false'</span> <span class="o">&gt;&gt;</span> /usr/local/etc/gemrc <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s1">'verbose: false'</span> <span class="o">&gt;&gt;</span> /usr/local/etc/gemrc <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s1">'update: --no-suggestions'</span> <span class="o">&gt;&gt;</span> /usr/local/etc/gemrc <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s1">'install: --no-suggestions --conservative'</span> <span class="o">&gt;&gt;</span> /usr/local/etc/gemrc<span class="c"># пытаемся удалить ненужные гемы с самого начала - попытка не пытка</span><span class="k">RUN </span><span class="nb">set</span> <span class="nt">-ex</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> gem uninstall <span class="nt">-a</span> <span class="nt">-x</span> <span class="nt">--quiet</span> <span class="nt">--force</span> <span class="sb">`</span>gem list | <span class="nb">cut</span> <span class="nt">-f</span> 1 <span class="nt">-d</span> <span class="s2">" "</span><span class="sb">`</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> gem cleanup<span class="k">RUN </span><span class="nb">set</span> <span class="nt">-ex</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> apk add <span class="nt">--virtual</span> .build-deps <span class="se">\</span>   autoconf <span class="se">\</span>   bison <span class="se">\</span>   bzip2 <span class="se">\</span>   bzip2-dev <span class="se">\</span>   coreutils <span class="se">\</span>   curl-dev <span class="se">\</span>   dpkg-dev dpkg <span class="se">\</span>   g++ <span class="se">\</span>   gcc <span class="se">\</span>   gdbm-dev <span class="se">\</span>   git <span class="se">\</span>   glib-dev <span class="se">\</span>   libc-dev <span class="se">\</span>   libffi-dev <span class="se">\</span>   libxml2-dev <span class="se">\</span>   libxslt-dev <span class="se">\</span>   linux-headers <span class="se">\</span>   make <span class="se">\</span>   ncurses-dev <span class="se">\</span>   procps <span class="se">\</span>   readline-dev <span class="se">\</span>   <span class="nb">tar</span> <span class="se">\</span>   xz <span class="se">\</span>   yaml-dev <span class="se">\</span>   zlib-dev <span class="se">\</span>   shared-mime-info <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">mv</span> /usr/local/lib/ruby/gems/<span class="k">${</span><span class="nv">RUBY_MAJOR</span><span class="k">}</span>.0/specifications/default/<span class="k">*</span> /usr/local/lib/ruby/gems/<span class="k">${</span><span class="nv">RUBY_MAJOR</span><span class="k">}</span>.0/specifications/ <span class="se">\</span> <span class="o">&amp;&amp;</span> gem cleanup <span class="se">\</span> <span class="o">&amp;&amp;</span> gem update <span class="nt">--system</span> <span class="s2">"</span><span class="k">${</span><span class="nv">RUBYGEMS_VERSION</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> gem uninstall bundler <span class="nt">--all</span> <span class="nt">--silent</span> <span class="o">||</span> <span class="nb">true</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> gem <span class="nb">install </span>bundler <span class="nt">-v</span> <span class="s2">"</span><span class="k">${</span><span class="nv">BUNDLER_VERSION</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> gem uninstall rdoc <span class="nt">--all</span> <span class="nt">--silent</span> <span class="o">||</span> <span class="nb">true</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nv">GMS</span><span class="o">=</span><span class="sb">`</span>gem list | <span class="nb">sed </span>s/default:<span class="se">\ </span>// | <span class="nb">sed</span> <span class="nt">-E</span> <span class="s1">'s/\ \((.*)\)/:\1/'</span> | <span class="nb">sort</span><span class="sb">`</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nv">DEBUG_FLAGS</span><span class="o">=</span><span class="s2">"-Wno-calloc-transposed-args"</span> gem pristine <span class="nt">--all</span> <span class="nt">--extensions</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nv">commands</span><span class="o">=</span><span class="si">$(</span><span class="k">for </span>x <span class="k">in</span> <span class="nv">$GMS</span><span class="p">;</span> <span class="k">do</span> <span class="se">\</span>   <span class="nv">g</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$x</span><span class="s2">"</span> | <span class="nb">cut</span> <span class="nt">-f</span> 1 <span class="nt">-d</span> <span class="s2">":"</span><span class="si">)</span><span class="p">;</span> <span class="se">\</span>   <span class="nv">v</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$x</span><span class="s2">"</span> | <span class="nb">cut</span> <span class="nt">-f</span> 2 <span class="nt">-d</span> <span class="s2">":"</span><span class="si">)</span><span class="p">;</span> <span class="se">\</span>   <span class="nb">echo </span>gem pristine <span class="nv">$g</span> <span class="nt">--version</span> <span class="s2">"</span><span class="nv">$v</span><span class="s2">"</span><span class="p">;</span> <span class="se">\</span> <span class="k">done</span><span class="si">)</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="nv">$commands</span><span class="s2">"</span> | xargs <span class="nt">-I</span> CMD <span class="nt">-P</span> 3 bash <span class="nt">-c</span> CMD<span class="se">\</span> <span class="o">&amp;&amp;</span> gem cleanup <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">rm</span> <span class="nt">-rf</span> root/.local/share/gem/specs <span class="se">\</span> <span class="o">&amp;&amp;</span> apk del .build-deps <span class="se">\</span> <span class="o">&amp;&amp;</span> cleanallbuilds.sh<span class="k">WORKDIR</span><span class="s"> /home/app</span><span class="k">RUN </span><span class="nb">set</span> <span class="nt">-ex</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> adduser <span class="nt">-D</span> <span class="nt">-s</span> /sbin/nologin app <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">chown</span> <span class="nt">-R</span> app:app /home/app<span class="k">ENTRYPOINT</span><span class="s"> ["/usr/bin/entrypoint.sh"]</span><span class="k">SHELL</span><span class="s"> ["/bin/sh", "-c"] </span></code></pre></div></div><p>Самое интересное, конечно, находится в одном слое самого пухленького RUN, и по некоторым командам надо дать пояснения:</p><ul>  <li>    <table>      <tbody>        <tr>          <td><code class="language-plaintext highlighter-rouge">GMS=</code>gem list</td>          <td>sed s/default:\ //</td>          <td>sed -E ‘s/\ ((.*))/:\1/’</td>          <td>sort`` - получаем список установленных гемов с версиями в формате “yaml:0.4.0 zlib:3.2.1”. Это нам потребуется дальше из-за странного поведения <code class="language-plaintext highlighter-rouge">gem pristine</code></td>        </tr>      </tbody>    </table>  </li>  <li><code class="language-plaintext highlighter-rouge">gem pristine --all --extensions</code> - должен для всех установленных гемов сделать “чистовую” установку, включая скачивание и перекомпиляцию расширений. Но нет. На самом деле он делает не для всех и не всё :(</li>  <li><code class="language-plaintext highlighter-rouge">DEBUG_FLAGS="-Wno-calloc-transposed-args"</code> - мы используем один Dockerfile для сборки базовых образов ruby 2.7, 3.0, 3.1, 3.2, и по умолчанию все расширения должны компилироваться без единого <del>разрыва</del> ворнинга компиляции, но для некоторых версий руби (кажется 3.0) это не так, и именно этот ворнинг всё портит, поэтому его подавляем без затей.</li>  <li><code class="language-plaintext highlighter-rouge">commands=$(for x in $GMS; do …</code> - а вы знали, что в bash тоже есть пул потоков? Строго говоря, он <a href="https://stackoverflow.com/questions/6441509/how-to-write-a-process-pool-bash-shell">пул процессов</a>, и не в баше, а в xargs, но это не важно. В общем тут список <code class="language-plaintext highlighter-rouge">$GMS</code> в виде “yaml:0.4.0 zlib:3.2.1” трансформируется в список <code class="language-plaintext highlighter-rouge">$commands</code>, потому что в такой форме <code class="language-plaintext highlighter-rouge">gem pristine</code> работает именно так как надо:</li></ul><div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem pristine yaml <span class="nt">--version</span> <span class="s2">"0.4.0"</span>gem pristine zlib <span class="nt">--version</span> <span class="s2">"3.2.1"</span>...</code></pre></div></div><ul>  <li><code class="language-plaintext highlighter-rouge">echo -e "$commands" | xargs -I CMD -P 3 bash -c CMD</code> - отправляем <code class="language-plaintext highlighter-rouge">$commands</code> в пул из трех процессов для одновременной установки гемов.</li>  <li><code class="language-plaintext highlighter-rouge">cleanallbuilds.sh</code> - зовём в самом конце кустарный скрипт, который удаляет всякий мусор из системы:</li></ul><div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span><span class="c"># ruby (alpine + debian)</span><span class="c"># Скрипт единый идля debian и для alpine, поэтому ошибки,</span><span class="c"># возникающие из-за разницы дистрибутивов просто игнорируем</span><span class="nb">rm</span> <span class="nt">-rf</span> /usr/src/ruby/ <span class="o">||</span> <span class="nb">truerm</span> <span class="nt">-rf</span> /root/.local/share/gem/specs/ <span class="o">||</span> <span class="nb">truerm</span> <span class="nt">-rf</span> /root/.local/state/ <span class="o">||</span> <span class="nb">truerm</span> <span class="nt">-rf</span> /root/.cache/gem/ <span class="o">||</span> <span class="nb">truerm</span> <span class="nt">-rf</span> /usr/local/lib/ruby/gems/3.2.0/cache/ <span class="o">||</span> <span class="nb">truerm</span> <span class="nt">-rf</span> /root/.bundle/cache/ <span class="o">||</span> <span class="nb">true</span>gem cleanup <span class="o">||</span> <span class="nb">true</span><span class="c"># alpine</span>apk cache clean <span class="o">||</span> <span class="nb">true</span><span class="c"># debian</span>apt-get clean autoclean <span class="o">||</span> <span class="nb">true</span>apt-get autoclean <span class="nt">--yes</span> <span class="o">||</span> <span class="nb">true</span>apt-get autoremove <span class="nt">--yes</span> <span class="o">||</span> <span class="nb">true</span>find /var/log/ <span class="nt">-type</span> f <span class="nt">-delete</span> <span class="o">||</span> <span class="nb">true</span>find /var/lib/log/ <span class="nt">-type</span> f <span class="nt">-delete</span> <span class="o">||</span> <span class="nb">true</span>find /usr/share/doc/ <span class="nt">-type</span> f <span class="nt">-delete</span> <span class="o">||</span> <span class="nb">true</span></code></pre></div></div><h2 id="результат">Результат</h2><p>Старый образ занимал 900MB, новый - 152MB (не спрашивайте…)</p><p><img src="https://img1.teletype.in/files/c3/95/c395d60f-18a0-45a0-9757-f7b7f779949c.png" alt="" /><em>Значительно лучше. Особенно для базового образа</em></p><h2 id="что-дальше">Что дальше?</h2><p>Теперь эти базовые образы надо начать использовать непосредственно в CI пайплайнах боевых сервисов и, конечно, процедуру сборки и тестирования придётся поменять. Точнее, мы уже перешли на новые образы и сборку уже пару недель назад - дело теперь только за статьёй. А в качестве приманки приведу результаты перехода для одного из самых “толстых” наших сервисов:</p><div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>image:old size:1.49GB vulns:255image:new-prod size:259MB vulns:1 (Вчерашняя!! СVE-2025-25186)image:new-test size:269MB vulns:1 (СVE-2025-25186)</code></pre></div></div><p>Если взглянуть ретроспективно, то совсем не весело - так что:</p><section style="background-color:hsl(hsl(24, 24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">    <p id="tf2v">профилируйте чаще не только ваш код и инфраструктуру сборки.</p>  </section><tt-tags id="HMo0">    <tt-tag name="docker">#docker</tt-tag>    <tt-tag name="devsecops">#devsecops</tt-tag>    <tt-tag name="ruby">#ruby</tt-tag>    <tt-tag name="cicd">#cicd</tt-tag>  </tt-tags> ]]></description>
        <content:encoded><![CDATA[ <p><img src="https://img1.teletype.in/files/42/f0/42f0e9b3-d3f8-403f-ac95-bfd789c92278.jpeg" alt="" /></p><p>DevSecOps уверенно шагает по нашей индустрии, и горе тому, кто попадёт под его поступь… Эта статья про <del>ультимативную</del> сборку базовых образов для Ruby для удовлетворения самых параноидальных потребностей ИБ. Да, именно об этом мы и расскажем - что такое “инсталляция ruby”, где, что, почему лежит и как с этим жить нашему пайплайну сборки и самому приложению.</p><section style="background-color:hsl(hsl(24, 24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">    <p id="CaKt">Статья подойдёт тем, кто хочет более глубоко понимать, какие процессы происходят в системе, когда вызывается <code>gem install</code>, <code>bundle install</code> или (не дай Бог) <code>gem update –system</code></p>  </section><h2 id="как-было-до">Как было до?</h2><p>С самого начала появления docker мы в RNDSOFT использовали парадигму “всё включено”. Для нас это означало, что в образ мы включаем всё, что нам надо не только для продуктовой эксплуатации, но и для проведения всех тестов. При прохождении CI пайплайна на следующие стадии продвигался образ целиком и, в конце концов, выкатывался на прод.</p><p>Это было очень удобно и позволяло быть максимально (насколько это вообще возможно) уверенным в работоспособности, однако имело ряд серьёзных минусов, с которыми мы прекрасно жили достаточно долгое время:</p><ul>  <li>размер образа был большим - от 1 ГБ;</li>  <li>образ включал большое количество неиспользуемого (в проде) ПО.</li></ul><h2 id="сканеры-сканировали-сканировали-да-не-высканировали">Сканеры сканировали, сканировали, да не высканировали…</h2><p>И вот однажды один (на данный момент уже далеко не один) наш клиент захотел устранения всех замечаний, которые смог выявить сканнер <a href="https://github.com/aquasecurity/trivy">Trivy</a>. А их, как не трудно догадаться, было достаточно много. И главная проблема заключается в том, что большая часть замечаний никак не связана с непосредственными зависимостями вашего приложения (теми, которые фиксируются в Gemfile.lock). <br />Так откуда же они берутся?</p><p>Если коротко отвечать - отовсюду :) При сборке проекта необходимо поставить его зависимости - это план минимум. Кроме того, часто появляется необходимость установить bundle определенной версии или даже обновить ruby целиком, выполнив команду <a href="https://guides.rubygems.org/command-reference/#gem-update">gem update –system</a>. В результате этих действий в ваш образ в разнообразные папки ставятся разнообразные гемы, но</p><section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">    <p id="gt1O">старые версии гемов и сама базовая "инсталляция ruby" остаётся в системе со всеми своими "устаревшими" и "уязвимыми" версиями. И эти версии очень нравятся сканерам для того, чтобы поднять тревогу. </p>  </section><p>И еще два слова о том, что же именно сканирует Trivy (касательно ruby конечно):</p><ul>  <li>сканирует <a href="https://guides.rubygems.org/specification-reference/">.gemspec файлы</a> на всей файловой системе и не важно, установлен гем или нет - если в .gemspec указана версия “с уязвимостью” - алярм;</li>  <li>сканирует .gem файлы на всей файловой системе и не важно, установлен гем, лежит просто в папке cache, используется или нет в вашем приложении.</li></ul><p>Значит для удовлетворения хотелок сканера, надо сделать так, чтоб нигде не лежало ничего лишнего или не используемого, <strong>включая</strong> <a href="https://docs.ruby-lang.org/en/3.2/standard_library_rdoc.html">stdlib</a> - стандартную библиотеку ruby.</p><p>Проблема ясна, задача очевидна - можно приступать к решению и начинать надо с базовых образов. Их надо собрать так, чтобы сами базовые образы уже не содержали никаких уязвимостей.</p><p><img src="https://img3.teletype.in/files/e1/7a/e17a3417-a64b-4027-adf2-971f332cf9dd.png" alt="" /><em>Немного удручающее зрелище, особенно для ИБ клиента</em></p><h2 id="что-такое-инсталляция-ruby">Что такое инсталляция ruby?</h2><p>Если мы возьмём любой дистрибутив с установленным там ruby, то увидим, что само ruby будет находиться где-то в районе:</p><ul>  <li>/usr/lib/ruby - например в alpine после <code class="language-plaintext highlighter-rouge">apk add ruby</code></li>  <li>/usr/local/lib/ruby - например в <code class="language-plaintext highlighter-rouge">ruby:alpine</code>, где руби собирается отдельно от apk</li></ul><p>А дальше начинается интересное. Если посмотреть в финальный образ, в котором сделано много различных операций (обновление ruby, установка гемов через <code class="language-plaintext highlighter-rouge">gem install</code>, установка через <code class="language-plaintext highlighter-rouge">bundle install</code>), то в корневой папке ruby обнаружится несколько папок, по которым тем или иным способом будут распределены установленные вами гемы:</p><div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/usr/local/lib/ruby├── 3.2.0├── gems/3.2.0├── site_ruby/3.2.0└── vendor_ruby/3.2.0</code></pre></div></div><p>Сразу добавим к этому списку другие папки (которые можно посмотреть в выводе команды <code class="language-plaintext highlighter-rouge">gem env</code>):</p><div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="pi">-</span> <span class="na">INSTALLATION DIRECTORY</span><span class="pi">:</span> <span class="s">/usr/local/bundle</span> <span class="pi">-</span> <span class="na">USER INSTALLATION DIRECTORY</span><span class="pi">:</span> <span class="s">/root/.local/share/gem/ruby/3.3.0</span> <span class="pi">-</span> <span class="na">SPEC CACHE DIRECTORY</span><span class="pi">:</span> <span class="s">/root/.cache/gem/specs</span> <span class="pi">-</span> <span class="na">GEM PATHS</span><span class="pi">:</span>    <span class="pi">-</span> <span class="s">/usr/lib/ruby/gems/3.3.0</span>    <span class="pi">-</span> <span class="s">/root/.local/share/gem/ruby/3.3.0</span></code></pre></div></div><p>И вишенкой на торте будет настройка вашего bundle, если вы используете кеширование сборки (<a href="https://bundler.io/man/bundle-cache.1.html">bundle cache</a> или <code class="language-plaintext highlighter-rouge">bundle config set cache_path vendor/cache</code>), и используемые в этом зоопарке переменные окружения <code class="language-plaintext highlighter-rouge">GEM_HOME</code>, <code class="language-plaintext highlighter-rouge">BUNDLE_CACHE_PATH</code>, <code class="language-plaintext highlighter-rouge">BUNDLE_PATH</code> и скорее всего еще какие-то скрытые в глубинах экосистемы ruby.</p><p>Немного путано и в результате беспорядочных <del>связей</del> установок и обновлений во всех этих папках могут (и будут) появляться гемы. Надо исправить!</p><p>Постараюсь дать верхнеуровневое описание, что же именно это за папочки, не углубляясь в подробности и исключения:</p><h3 id="usrlocallibruby330">/usr/local/lib/ruby/3.3.0</h3><p>Именно в этой папке установлены основные файлы ruby и stdlib, а также компилируемые расширения в папке x86_64-linux-musl (в нашем случае собранные alpine для x86_64). Эти файлы принадлежат условной “системе” и никак не будут изменяться при дальнейших модификациях, например при установке или обновлении гемов через <code class="language-plaintext highlighter-rouge">gem install</code>. Вместо этого библиотеки будут ставиться в папку <code class="language-plaintext highlighter-rouge">/usr/local/lib/ruby/gems/3.3.0</code></p><h3 id="usrlocallibrubygems330">/usr/local/lib/ruby/gems/3.3.0</h3><p>Тут собрано всё, что вы ставите (без модификации <code class="language-plaintext highlighter-rouge">GEM_PATH</code>) командами <code class="language-plaintext highlighter-rouge">gem install,</code> включая компилируемые расширения.</p><h3 id="usrlocallibrubyvendor_ruby330">/usr/local/lib/ruby/vendor_ruby/3.3.0</h3><p>Сюда <strong>должны</strong> ставиться дополнительные гемы и/или патчи от команды мейнтейнеров дистрибутива. Об этом сложно найти информацию, но несколько слов есть в книге <code class="language-plaintext highlighter-rouge">The Ruby Programming Language</code> или, что интереснее, в changelog для <a href="https://docs.ruby-lang.org/en/2.3.0/NEWS-1_8_7.html">NEWS for Ruby 1.8.7</a> (да, окаменелое…):</p><section style="background-color:hsl(hsl(24, 24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">    <p id="VAZR"><strong>vendor_ruby</strong> directory<br />A new library directory named <code>vendor_ruby</code> is introduced in addition to <code>site_ruby</code>. The idea is to separate libraries installed by the package system (<code>vendor</code>) from manually (<code>site</code>) installed libraries preventing the former from getting overwritten by the latter, while preserving the user option to override vendor libraries with site libraries. (<code>site_ruby</code> takes precedence over <code>vendor_ruby</code>)<br />If you are a package maintainer, make each library package configure the library passing the <code>--vendor</code> option to <code>extconf.rb</code> so that the library files will get installed under <code>vendor_ruby</code>.<br />You can change the directory locations using configure options such as<code> --with-sitedir=DIR</code> and <code>--with-vendordir=DIR</code>.</p>  </section><h3 id="usrlocallibrubysite_ruby330">/usr/local/lib/ruby/site_ruby/3.3.0</h3><p>Сюда будут ставиться “системные” файлы, но не от OS, а от самого ruby, например после выполнения <code class="language-plaintext highlighter-rouge">gem update –system</code>:</p><div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>site_ruby/└── 3.4.0   ├── bundler   ├── rubygems   └── x86_64-linux-musl</code></pre></div></div><h3 id="usrlocallibrubygems330specifications">/usr/local/lib/ruby/gems/3.3.0/specifications/</h3><p>…а также INSTALLATION DIRECTORY <code class="language-plaintext highlighter-rouge">/usr/local/bundle/specifications</code>…<br />…а также USER INSTALLATION DIRECTORY: <code class="language-plaintext highlighter-rouge">/root/.local/share/gem/ruby/3.3.0</code>…<br />…а также SPEC CACHE DIRECTORY <code class="language-plaintext highlighter-rouge">/root/.cache/gem/specs</code>…<br />…а также BUNDLE_CACHE_PATH …</p><p>Сюда ставятся .gemspec файлы, которые собственно и говорят пакетному менеджеру ruby (gem и bundle), какие именно версии каких гемов установлены.</p><h3 id="usrlocallibrubygems330specificationsdefault">/usr/local/lib/ruby/gems/3.3.0/specifications/default</h3><p>Default, Карл… Default - это особое состояние гема, и эти гемы нельзя удалить. Если вы обновили гем до новой версии, то старая всё равно останется, и Trivy вам этого не простит. Весьма вредная папка с точки зрения сканирования.</p><h3 id="usrlocallibrubygems330cache">/usr/local/lib/ruby/gems/3.3.0/cache/</h3><p>…а также INSTALLATION DIRECTORY <code class="language-plaintext highlighter-rouge">/usr/local/bundle/cache</code>…<br />…а также BUNDLE_PATH …</p><p>Сюда пакетный менеджер ruby (gem или bundle) скачивает гемы (допустим вы ставите faraday.gem) перед установкой. Этот кеш очень часто используется для ускорения сборки, например <a href="https://docs.gitlab.com/ee/ci/caching/#cache-ruby-dependencies">в вашем Gitlab</a>.</p><h2 id="что-здесь-у-вас-происходит">Что здесь у вас происходит?!</h2><p>После того как мы в процессе исследования детально разобрались и увидели всё это многообразие мест, где могут находиться файлы, вызывающие панические атаки у Trivy, мы решили радикально решить эту проблему: свести все файлы, все гемы и все спеки (.gemspec) в одно место. Это позволит легко следить за всеми (всеми!) фактическими зависимостями и эффективно пользоваться командами <code class="language-plaintext highlighter-rouge">gem cleanup</code> и <code class="language-plaintext highlighter-rouge">bundle clean</code>. Тут надо отметить, что для вашей рабочей OS (системы общего назначения) такое решение приведёт к поломке системного пакетного менеджера (<a href="https://ru.wikipedia.org/wiki/Portage">Gentoo Portage</a>, <a href="https://ru.wikipedia.org/wiki/Dpkg">dpkg/apt</a>, <a href="https://ru.wikipedia.org/wiki/RPM">rpm/zypper</a> и пр.), но мы ведь говорим о конкретной сборке ruby под ваш конкретный проект - и тут никаких проблем не будет.</p><section style="background-color:hsl(hsl(24, 24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">    <p id="ma0i">Для того чтобы узнать что куда, когда и зачем ставится, мы использовали git прямо на корне файловой системы внутри контейнера (ruby:alpine, просто alpine, ruby:debian и другие образы для сравнения) и фиксировали изменения после различных команд ⏳</p>  </section><p>Но это еще не всё. Остаётся еще проблема с default gemspec, но её относительно легко решить:</p><div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mv</span> /usr/local/lib/ruby/gems/3.3.0/specifications/default/<span class="k">*</span> /usr/local/lib/ruby/gems/3.3.0/specifications/</code></pre></div></div><p>Теперь мы можем сформулировать План:</p><ul>  <li>Все дороги ведут в Рим - делаем ссылочки для <code class="language-plaintext highlighter-rouge">vendor_ruby</code>, <code class="language-plaintext highlighter-rouge">site_ruby</code> и пр. Также явно прописываем системные переменные <code class="language-plaintext highlighter-rouge">GEM_HOME</code> и <code class="language-plaintext highlighter-rouge">BUNDLE_APP_CONFIG.</code></li>  <li>Разбираемся с default gems.</li>  <li>Обновляем ruby (имеется в виду stdlib) до последней требуемой версии.</li>  <li>Обновляем bundle до нужной версии.</li>  <li>Удаляем лишние гемы (например rdoc).</li>  <li>Переставляем (!) системные (на текущий момент сборки базового образа - <strong>все</strong> ) гемы, потому что, как оказалось, <code class="language-plaintext highlighter-rouge">mv</code> для default gemspec имеет не очень хорошие последствия.</li>  <li>profit!</li></ul><p>Сказано - сделано, и добро пожаловать под кат!</p><div class="language-docker highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ARG</span><span class="s"> BASE_RUBY=3.2</span><span class="k">ARG</span><span class="s"> BASE_ALPINE=alpine3.16</span><span class="k">ARG</span><span class="s"> BASE_IMAGE=ruby:${BASE_RUBY}-${BASE_ALPINE}</span><span class="k">FROM</span><span class="s"> ${BASE_IMAGE}</span><span class="k">ARG</span><span class="s"> BASE_RUBY=3.2</span><span class="k">ARG</span><span class="s"> RUBYGEMS_VERSION=3.5.20</span><span class="k">ARG</span><span class="s"> BUNDLER_VERSION=2.5.20</span><span class="c"># эта переменная есть в старом alpine но нет в debian и новом</span><span class="c"># добавляем потому что она очень нужна для работы с папочками</span><span class="k">ENV</span><span class="s"> RUBY_MAJOR=${BASE_RUBY}</span><span class="k">ENV</span><span class="s"> RUBYGEMS_VERSION=${RUBYGEMS_VERSION} \</span>    BUNDLER_VERSION=${BUNDLER_VERSION}<span class="k">RUN </span>apk update <span class="o">&amp;&amp;</span> apk upgrade<span class="c"># Это наш костыльный скрипт который удаляет всякие лишние кеши,</span><span class="c"># man-файлы и прочий мусор. </span><span class="k">COPY</span><span class="s"> common/scripts/cleanallbuilds.sh /usr/bin/</span><span class="c"># dumb-init всегда используем как PID-1 но это немного другая история</span><span class="k">RUN </span><span class="nb">set</span> <span class="nt">-ex</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> apk add dumb-init <span class="se">\</span> <span class="o">&amp;&amp;</span> cleanallbuilds.sh<span class="c">###### </span><span class="c"># Надругиваемся над диструбутивом, чтоб иметь строго</span><span class="c"># одну версию руби в систему и управлять ею целиком через gem/bundle</span><span class="c"># все пути ведут в Рим</span><span class="k">ENV</span><span class="s"> GEM_HOME=/usr/local/lib/ruby/gems/${RUBY_MAJOR}.0/</span><span class="k">ENV</span><span class="s"> BUNDLE_APP_CONFIG=/usr/local/lib/ruby/gems/${RUBY_MAJOR}.0/</span><span class="c"># Сводим vendor_ruby, site_ruby и GEM_HOME в одно место</span><span class="k">RUN </span><span class="nb">set</span> <span class="nt">-ex</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">rm</span> <span class="nt">-rf</span> /usr/local/lib/ruby/site_ruby /usr/local/lib/ruby/vendor_ruby <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">ln</span> <span class="nt">-sf</span> /usr/local/lib/ruby /usr/local/lib/ruby/site_ruby <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">ln</span> <span class="nt">-sf</span> /usr/local/lib/ruby /usr/local/lib/ruby/vendor_ruby <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">mkdir</span> <span class="nt">-p</span> /root/.local/share/gem/ruby/ <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">ln</span> <span class="nt">-sf</span> <span class="k">${</span><span class="nv">GEM_HOME</span><span class="k">}</span> /root/.local/share/gem/ruby/<span class="k">${</span><span class="nv">RUBY_MAJOR</span><span class="k">}</span>.0<span class="c"># Чутка тюним bundle config чтоб в дальнейшем не забыть</span><span class="k">RUN </span><span class="nb">set</span> <span class="nt">-ex</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> bundle config <span class="nt">--local</span> disable_version_check <span class="nb">true</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> bundle config <span class="nt">--local</span> clean <span class="nb">false</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> bundle config <span class="nt">--local</span> no_prune <span class="nb">false</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> bundle config <span class="nt">--local</span> disable_local_branch_check <span class="nb">true</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> bundle config <span class="nt">--local</span> <span class="nb">jobs </span>2 <span class="se">\</span> <span class="o">&amp;&amp;</span> bundle config <span class="nt">--local</span> allow_offline_install <span class="nb">true</span><span class="c"># настраиваем .gemrc чтоб не было ничего лишнего, вклюячая rdoc</span><span class="k">RUN </span><span class="nb">set</span> <span class="nt">-ex</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s1">'gem: --no-document'</span> <span class="o">&gt;</span> /usr/local/etc/gemrc <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s1">'update_sources: false'</span> <span class="o">&gt;&gt;</span> /usr/local/etc/gemrc <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s1">'verbose: false'</span> <span class="o">&gt;&gt;</span> /usr/local/etc/gemrc <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s1">'update: --no-suggestions'</span> <span class="o">&gt;&gt;</span> /usr/local/etc/gemrc <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s1">'install: --no-suggestions --conservative'</span> <span class="o">&gt;&gt;</span> /usr/local/etc/gemrc<span class="c"># пытаемся удалить ненужные гемы с самого начала - попытка не пытка</span><span class="k">RUN </span><span class="nb">set</span> <span class="nt">-ex</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> gem uninstall <span class="nt">-a</span> <span class="nt">-x</span> <span class="nt">--quiet</span> <span class="nt">--force</span> <span class="sb">`</span>gem list | <span class="nb">cut</span> <span class="nt">-f</span> 1 <span class="nt">-d</span> <span class="s2">" "</span><span class="sb">`</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> gem cleanup<span class="k">RUN </span><span class="nb">set</span> <span class="nt">-ex</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> apk add <span class="nt">--virtual</span> .build-deps <span class="se">\</span>   autoconf <span class="se">\</span>   bison <span class="se">\</span>   bzip2 <span class="se">\</span>   bzip2-dev <span class="se">\</span>   coreutils <span class="se">\</span>   curl-dev <span class="se">\</span>   dpkg-dev dpkg <span class="se">\</span>   g++ <span class="se">\</span>   gcc <span class="se">\</span>   gdbm-dev <span class="se">\</span>   git <span class="se">\</span>   glib-dev <span class="se">\</span>   libc-dev <span class="se">\</span>   libffi-dev <span class="se">\</span>   libxml2-dev <span class="se">\</span>   libxslt-dev <span class="se">\</span>   linux-headers <span class="se">\</span>   make <span class="se">\</span>   ncurses-dev <span class="se">\</span>   procps <span class="se">\</span>   readline-dev <span class="se">\</span>   <span class="nb">tar</span> <span class="se">\</span>   xz <span class="se">\</span>   yaml-dev <span class="se">\</span>   zlib-dev <span class="se">\</span>   shared-mime-info <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">mv</span> /usr/local/lib/ruby/gems/<span class="k">${</span><span class="nv">RUBY_MAJOR</span><span class="k">}</span>.0/specifications/default/<span class="k">*</span> /usr/local/lib/ruby/gems/<span class="k">${</span><span class="nv">RUBY_MAJOR</span><span class="k">}</span>.0/specifications/ <span class="se">\</span> <span class="o">&amp;&amp;</span> gem cleanup <span class="se">\</span> <span class="o">&amp;&amp;</span> gem update <span class="nt">--system</span> <span class="s2">"</span><span class="k">${</span><span class="nv">RUBYGEMS_VERSION</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> gem uninstall bundler <span class="nt">--all</span> <span class="nt">--silent</span> <span class="o">||</span> <span class="nb">true</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> gem <span class="nb">install </span>bundler <span class="nt">-v</span> <span class="s2">"</span><span class="k">${</span><span class="nv">BUNDLER_VERSION</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> gem uninstall rdoc <span class="nt">--all</span> <span class="nt">--silent</span> <span class="o">||</span> <span class="nb">true</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nv">GMS</span><span class="o">=</span><span class="sb">`</span>gem list | <span class="nb">sed </span>s/default:<span class="se">\ </span>// | <span class="nb">sed</span> <span class="nt">-E</span> <span class="s1">'s/\ \((.*)\)/:\1/'</span> | <span class="nb">sort</span><span class="sb">`</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nv">DEBUG_FLAGS</span><span class="o">=</span><span class="s2">"-Wno-calloc-transposed-args"</span> gem pristine <span class="nt">--all</span> <span class="nt">--extensions</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nv">commands</span><span class="o">=</span><span class="si">$(</span><span class="k">for </span>x <span class="k">in</span> <span class="nv">$GMS</span><span class="p">;</span> <span class="k">do</span> <span class="se">\</span>   <span class="nv">g</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$x</span><span class="s2">"</span> | <span class="nb">cut</span> <span class="nt">-f</span> 1 <span class="nt">-d</span> <span class="s2">":"</span><span class="si">)</span><span class="p">;</span> <span class="se">\</span>   <span class="nv">v</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$x</span><span class="s2">"</span> | <span class="nb">cut</span> <span class="nt">-f</span> 2 <span class="nt">-d</span> <span class="s2">":"</span><span class="si">)</span><span class="p">;</span> <span class="se">\</span>   <span class="nb">echo </span>gem pristine <span class="nv">$g</span> <span class="nt">--version</span> <span class="s2">"</span><span class="nv">$v</span><span class="s2">"</span><span class="p">;</span> <span class="se">\</span> <span class="k">done</span><span class="si">)</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="nv">$commands</span><span class="s2">"</span> | xargs <span class="nt">-I</span> CMD <span class="nt">-P</span> 3 bash <span class="nt">-c</span> CMD<span class="se">\</span> <span class="o">&amp;&amp;</span> gem cleanup <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">rm</span> <span class="nt">-rf</span> root/.local/share/gem/specs <span class="se">\</span> <span class="o">&amp;&amp;</span> apk del .build-deps <span class="se">\</span> <span class="o">&amp;&amp;</span> cleanallbuilds.sh<span class="k">WORKDIR</span><span class="s"> /home/app</span><span class="k">RUN </span><span class="nb">set</span> <span class="nt">-ex</span> <span class="se">\</span> <span class="o">&amp;&amp;</span> adduser <span class="nt">-D</span> <span class="nt">-s</span> /sbin/nologin app <span class="se">\</span> <span class="o">&amp;&amp;</span> <span class="nb">chown</span> <span class="nt">-R</span> app:app /home/app<span class="k">ENTRYPOINT</span><span class="s"> ["/usr/bin/entrypoint.sh"]</span><span class="k">SHELL</span><span class="s"> ["/bin/sh", "-c"] </span></code></pre></div></div><p>Самое интересное, конечно, находится в одном слое самого пухленького RUN, и по некоторым командам надо дать пояснения:</p><ul>  <li>    <table>      <tbody>        <tr>          <td><code class="language-plaintext highlighter-rouge">GMS=</code>gem list</td>          <td>sed s/default:\ //</td>          <td>sed -E ‘s/\ ((.*))/:\1/’</td>          <td>sort`` - получаем список установленных гемов с версиями в формате “yaml:0.4.0 zlib:3.2.1”. Это нам потребуется дальше из-за странного поведения <code class="language-plaintext highlighter-rouge">gem pristine</code></td>        </tr>      </tbody>    </table>  </li>  <li><code class="language-plaintext highlighter-rouge">gem pristine --all --extensions</code> - должен для всех установленных гемов сделать “чистовую” установку, включая скачивание и перекомпиляцию расширений. Но нет. На самом деле он делает не для всех и не всё :(</li>  <li><code class="language-plaintext highlighter-rouge">DEBUG_FLAGS="-Wno-calloc-transposed-args"</code> - мы используем один Dockerfile для сборки базовых образов ruby 2.7, 3.0, 3.1, 3.2, и по умолчанию все расширения должны компилироваться без единого <del>разрыва</del> ворнинга компиляции, но для некоторых версий руби (кажется 3.0) это не так, и именно этот ворнинг всё портит, поэтому его подавляем без затей.</li>  <li><code class="language-plaintext highlighter-rouge">commands=$(for x in $GMS; do …</code> - а вы знали, что в bash тоже есть пул потоков? Строго говоря, он <a href="https://stackoverflow.com/questions/6441509/how-to-write-a-process-pool-bash-shell">пул процессов</a>, и не в баше, а в xargs, но это не важно. В общем тут список <code class="language-plaintext highlighter-rouge">$GMS</code> в виде “yaml:0.4.0 zlib:3.2.1” трансформируется в список <code class="language-plaintext highlighter-rouge">$commands</code>, потому что в такой форме <code class="language-plaintext highlighter-rouge">gem pristine</code> работает именно так как надо:</li></ul><div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem pristine yaml <span class="nt">--version</span> <span class="s2">"0.4.0"</span>gem pristine zlib <span class="nt">--version</span> <span class="s2">"3.2.1"</span>...</code></pre></div></div><ul>  <li><code class="language-plaintext highlighter-rouge">echo -e "$commands" | xargs -I CMD -P 3 bash -c CMD</code> - отправляем <code class="language-plaintext highlighter-rouge">$commands</code> в пул из трех процессов для одновременной установки гемов.</li>  <li><code class="language-plaintext highlighter-rouge">cleanallbuilds.sh</code> - зовём в самом конце кустарный скрипт, который удаляет всякий мусор из системы:</li></ul><div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span><span class="c"># ruby (alpine + debian)</span><span class="c"># Скрипт единый идля debian и для alpine, поэтому ошибки,</span><span class="c"># возникающие из-за разницы дистрибутивов просто игнорируем</span><span class="nb">rm</span> <span class="nt">-rf</span> /usr/src/ruby/ <span class="o">||</span> <span class="nb">truerm</span> <span class="nt">-rf</span> /root/.local/share/gem/specs/ <span class="o">||</span> <span class="nb">truerm</span> <span class="nt">-rf</span> /root/.local/state/ <span class="o">||</span> <span class="nb">truerm</span> <span class="nt">-rf</span> /root/.cache/gem/ <span class="o">||</span> <span class="nb">truerm</span> <span class="nt">-rf</span> /usr/local/lib/ruby/gems/3.2.0/cache/ <span class="o">||</span> <span class="nb">truerm</span> <span class="nt">-rf</span> /root/.bundle/cache/ <span class="o">||</span> <span class="nb">true</span>gem cleanup <span class="o">||</span> <span class="nb">true</span><span class="c"># alpine</span>apk cache clean <span class="o">||</span> <span class="nb">true</span><span class="c"># debian</span>apt-get clean autoclean <span class="o">||</span> <span class="nb">true</span>apt-get autoclean <span class="nt">--yes</span> <span class="o">||</span> <span class="nb">true</span>apt-get autoremove <span class="nt">--yes</span> <span class="o">||</span> <span class="nb">true</span>find /var/log/ <span class="nt">-type</span> f <span class="nt">-delete</span> <span class="o">||</span> <span class="nb">true</span>find /var/lib/log/ <span class="nt">-type</span> f <span class="nt">-delete</span> <span class="o">||</span> <span class="nb">true</span>find /usr/share/doc/ <span class="nt">-type</span> f <span class="nt">-delete</span> <span class="o">||</span> <span class="nb">true</span></code></pre></div></div><h2 id="результат">Результат</h2><p>Старый образ занимал 900MB, новый - 152MB (не спрашивайте…)</p><p><img src="https://img1.teletype.in/files/c3/95/c395d60f-18a0-45a0-9757-f7b7f779949c.png" alt="" /><em>Значительно лучше. Особенно для базового образа</em></p><h2 id="что-дальше">Что дальше?</h2><p>Теперь эти базовые образы надо начать использовать непосредственно в CI пайплайнах боевых сервисов и, конечно, процедуру сборки и тестирования придётся поменять. Точнее, мы уже перешли на новые образы и сборку уже пару недель назад - дело теперь только за статьёй. А в качестве приманки приведу результаты перехода для одного из самых “толстых” наших сервисов:</p><div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>image:old size:1.49GB vulns:255image:new-prod size:259MB vulns:1 (Вчерашняя!! СVE-2025-25186)image:new-test size:269MB vulns:1 (СVE-2025-25186)</code></pre></div></div><p>Если взглянуть ретроспективно, то совсем не весело - так что:</p><section style="background-color:hsl(hsl(24, 24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">    <p id="tf2v">профилируйте чаще не только ваш код и инфраструктуру сборки.</p>  </section><tt-tags id="HMo0">    <tt-tag name="docker">#docker</tt-tag>    <tt-tag name="devsecops">#devsecops</tt-tag>    <tt-tag name="ruby">#ruby</tt-tag>    <tt-tag name="cicd">#cicd</tt-tag>  </tt-tags> ]]></content:encoded>
        <yandex:full-text>DevSecOps уверенно шагает по нашей индустрии, и горе тому, кто попадёт под его поступь… Эта статья про ультимативную сборку базовых образов для Ruby для удовлетворения самых параноидальных потребностей ИБ. Да, именно об этом мы и расскажем - что такое “инсталляция ruby”, где, что, почему лежит и как с этим жить нашему пайплайну сборки и самому приложению.    Статья подойдёт тем, кто хочет более глубоко понимать, какие процессы происходят в системе, когда вызывается gem install, bundle install или (не дай Бог) gem update –system  Как было до?С самого начала появления docker мы в RNDSOFT использовали парадигму “всё включено”. Для нас это означало, что в образ мы включаем всё, что нам надо не только для продуктовой эксплуатации, но и для проведения всех тестов. При прохождении CI пайплайна на следующие стадии продвигался образ целиком и, в конце концов, выкатывался на прод.Это было очень удобно и позволяло быть максимально (насколько это вообще возможно) уверенным в работоспособности, однако имело ряд серьёзных минусов, с которыми мы прекрасно жили достаточно долгое время:  размер образа был большим - от 1 ГБ;  образ включал большое количество неиспользуемого (в проде) ПО.Сканеры сканировали, сканировали, да не высканировали…И вот однажды один (на данный момент уже далеко не один) наш клиент захотел устранения всех замечаний, которые смог выявить сканнер Trivy. А их, как не трудно догадаться, было достаточно много. И главная проблема заключается в том, что большая часть замечаний никак не связана с непосредственными зависимостями вашего приложения (теми, которые фиксируются в Gemfile.lock). Так откуда же они берутся?Если коротко отвечать - отовсюду :) При сборке проекта необходимо поставить его зависимости - это план минимум. Кроме того, часто появляется необходимость установить bundle определенной версии или даже обновить ruby целиком, выполнив команду gem update –system. В результате этих действий в ваш образ в разнообразные папки ставятся разнообразные гемы, но    старые версии гемов и сама базовая "инсталляция ruby" остаётся в системе со всеми своими "устаревшими" и "уязвимыми" версиями. И эти версии очень нравятся сканерам для того, чтобы поднять тревогу.   И еще два слова о том, что же именно сканирует Trivy (касательно ruby конечно):  сканирует .gemspec файлы на всей файловой системе и не важно, установлен гем или нет - если в .gemspec указана версия “с уязвимостью” - алярм;  сканирует .gem файлы на всей файловой системе и не важно, установлен гем, лежит просто в папке cache, используется или нет в вашем приложении.Значит для удовлетворения хотелок сканера, надо сделать так, чтоб нигде не лежало ничего лишнего или не используемого, включая stdlib - стандартную библиотеку ruby.Проблема ясна, задача очевидна - можно приступать к решению и начинать надо с базовых образов. Их надо собрать так, чтобы сами базовые образы уже не содержали никаких уязвимостей.Немного удручающее зрелище, особенно для ИБ клиентаЧто такое инсталляция ruby?Если мы возьмём любой дистрибутив с установленным там ruby, то увидим, что само ruby будет находиться где-то в районе:  /usr/lib/ruby - например в alpine после apk add ruby  /usr/local/lib/ruby - например в ruby:alpine, где руби собирается отдельно от apkА дальше начинается интересное. Если посмотреть в финальный образ, в котором сделано много различных операций (обновление ruby, установка гемов через gem install, установка через bundle install), то в корневой папке ruby обнаружится несколько папок, по которым тем или иным способом будут распределены установленные вами гемы:/usr/local/lib/ruby├── 3.2.0├── gems/3.2.0├── site_ruby/3.2.0└── vendor_ruby/3.2.0Сразу добавим к этому списку другие папки (которые можно посмотреть в выводе команды gem env): - INSTALLATION DIRECTORY: /usr/local/bundle - USER INSTALLATION DIRECTORY: /root/.local/share/gem/ruby/3.3.0 - SPEC CACHE DIRECTORY: /root/.cache/gem/specs - GEM PATHS:    - /usr/lib/ruby/gems/3.3.0    - /root/.local/share/gem/ruby/3.3.0И вишенкой на торте будет настройка вашего bundle, если вы используете кеширование сборки (bundle cache или bundle config set cache_path vendor/cache), и используемые в этом зоопарке переменные окружения GEM_HOME, BUNDLE_CACHE_PATH, BUNDLE_PATH и скорее всего еще какие-то скрытые в глубинах экосистемы ruby.Немного путано и в результате беспорядочных связей установок и обновлений во всех этих папках могут (и будут) появляться гемы. Надо исправить!Постараюсь дать верхнеуровневое описание, что же именно это за папочки, не углубляясь в подробности и исключения:/usr/local/lib/ruby/3.3.0Именно в этой папке установлены основные файлы ruby и stdlib, а также компилируемые расширения в папке x86_64-linux-musl (в нашем случае собранные alpine для x86_64). Эти файлы принадлежат условной “системе” и никак не будут изменяться при дальнейших модификациях, например при установке или обновлении гемов через gem install. Вместо этого библиотеки будут ставиться в папку /usr/local/lib/ruby/gems/3.3.0/usr/local/lib/ruby/gems/3.3.0Тут собрано всё, что вы ставите (без модификации GEM_PATH) командами gem install, включая компилируемые расширения./usr/local/lib/ruby/vendor_ruby/3.3.0Сюда должны ставиться дополнительные гемы и/или патчи от команды мейнтейнеров дистрибутива. Об этом сложно найти информацию, но несколько слов есть в книге The Ruby Programming Language или, что интереснее, в changelog для NEWS for Ruby 1.8.7 (да, окаменелое…):    vendor_ruby directoryA new library directory named vendor_ruby is introduced in addition to site_ruby. The idea is to separate libraries installed by the package system (vendor) from manually (site) installed libraries preventing the former from getting overwritten by the latter, while preserving the user option to override vendor libraries with site libraries. (site_ruby takes precedence over vendor_ruby)If you are a package maintainer, make each library package configure the library passing the --vendor option to extconf.rb so that the library files will get installed under vendor_ruby.You can change the directory locations using configure options such as --with-sitedir=DIR and --with-vendordir=DIR.  /usr/local/lib/ruby/site_ruby/3.3.0Сюда будут ставиться “системные” файлы, но не от OS, а от самого ruby, например после выполнения gem update –system:site_ruby/└── 3.4.0   ├── bundler   ├── rubygems   └── x86_64-linux-musl/usr/local/lib/ruby/gems/3.3.0/specifications/…а также INSTALLATION DIRECTORY /usr/local/bundle/specifications……а также USER INSTALLATION DIRECTORY: /root/.local/share/gem/ruby/3.3.0……а также SPEC CACHE DIRECTORY /root/.cache/gem/specs……а также BUNDLE_CACHE_PATH …Сюда ставятся .gemspec файлы, которые собственно и говорят пакетному менеджеру ruby (gem и bundle), какие именно версии каких гемов установлены./usr/local/lib/ruby/gems/3.3.0/specifications/defaultDefault, Карл… Default - это особое состояние гема, и эти гемы нельзя удалить. Если вы обновили гем до новой версии, то старая всё равно останется, и Trivy вам этого не простит. Весьма вредная папка с точки зрения сканирования./usr/local/lib/ruby/gems/3.3.0/cache/…а также INSTALLATION DIRECTORY /usr/local/bundle/cache……а также BUNDLE_PATH …Сюда пакетный менеджер ruby (gem или bundle) скачивает гемы (допустим вы ставите faraday.gem) перед установкой. Этот кеш очень часто используется для ускорения сборки, например в вашем Gitlab.Что здесь у вас происходит?!После того как мы в процессе исследования детально разобрались и увидели всё это многообразие мест, где могут находиться файлы, вызывающие панические атаки у Trivy, мы решили радикально решить эту проблему: свести все файлы, все гемы и все спеки (.gemspec) в одно место. Это позволит легко следить за всеми (всеми!) фактическими зависимостями и эффективно пользоваться командами gem cleanup и bundle clean. Тут надо отметить, что для вашей рабочей OS (системы общего назначения) такое решение приведёт к поломке системного пакетного менеджера (Gentoo Portage, dpkg/apt, rpm/zypper и пр.), но мы ведь говорим о конкретной сборке ruby под ваш конкретный проект - и тут никаких проблем не будет.    Для того чтобы узнать что куда, когда и зачем ставится, мы использовали git прямо на корне файловой системы внутри контейнера (ruby:alpine, просто alpine, ruby:debian и другие образы для сравнения) и фиксировали изменения после различных команд ⏳  Но это еще не всё. Остаётся еще проблема с default gemspec, но её относительно легко решить:mv /usr/local/lib/ruby/gems/3.3.0/specifications/default/* /usr/local/lib/ruby/gems/3.3.0/specifications/Теперь мы можем сформулировать План:  Все дороги ведут в Рим - делаем ссылочки для vendor_ruby, site_ruby и пр. Также явно прописываем системные переменные GEM_HOME и BUNDLE_APP_CONFIG.  Разбираемся с default gems.  Обновляем ruby (имеется в виду stdlib) до последней требуемой версии.  Обновляем bundle до нужной версии.  Удаляем лишние гемы (например rdoc).  Переставляем (!) системные (на текущий момент сборки базового образа - все ) гемы, потому что, как оказалось, mv для default gemspec имеет не очень хорошие последствия.  profit!Сказано - сделано, и добро пожаловать под кат!ARG BASE_RUBY=3.2ARG BASE_ALPINE=alpine3.16ARG BASE_IMAGE=ruby:${BASE_RUBY}-${BASE_ALPINE}FROM ${BASE_IMAGE}ARG BASE_RUBY=3.2ARG RUBYGEMS_VERSION=3.5.20ARG BUNDLER_VERSION=2.5.20# эта переменная есть в старом alpine но нет в debian и новом# добавляем потому что она очень нужна для работы с папочкамиENV RUBY_MAJOR=${BASE_RUBY}ENV RUBYGEMS_VERSION=${RUBYGEMS_VERSION} \    BUNDLER_VERSION=${BUNDLER_VERSION}RUN apk update &amp;&amp; apk upgrade# Это наш костыльный скрипт который удаляет всякие лишние кеши,# man-файлы и прочий мусор. COPY common/scripts/cleanallbuilds.sh /usr/bin/# dumb-init всегда используем как PID-1 но это немного другая историяRUN set -ex \ &amp;&amp; apk add dumb-init \ &amp;&amp; cleanallbuilds.sh###### # Надругиваемся над диструбутивом, чтоб иметь строго# одну версию руби в систему и управлять ею целиком через gem/bundle# все пути ведут в РимENV GEM_HOME=/usr/local/lib/ruby/gems/${RUBY_MAJOR}.0/ENV BUNDLE_APP_CONFIG=/usr/local/lib/ruby/gems/${RUBY_MAJOR}.0/# Сводим vendor_ruby, site_ruby и GEM_HOME в одно местоRUN set -ex \ &amp;&amp; rm -rf /usr/local/lib/ruby/site_ruby /usr/local/lib/ruby/vendor_ruby \ &amp;&amp; ln -sf /usr/local/lib/ruby /usr/local/lib/ruby/site_ruby \ &amp;&amp; ln -sf /usr/local/lib/ruby /usr/local/lib/ruby/vendor_ruby \ &amp;&amp; mkdir -p /root/.local/share/gem/ruby/ \ &amp;&amp; ln -sf ${GEM_HOME} /root/.local/share/gem/ruby/${RUBY_MAJOR}.0# Чутка тюним bundle config чтоб в дальнейшем не забытьRUN set -ex \ &amp;&amp; bundle config --local disable_version_check true \ &amp;&amp; bundle config --local clean false \ &amp;&amp; bundle config --local no_prune false \ &amp;&amp; bundle config --local disable_local_branch_check true \ &amp;&amp; bundle config --local jobs 2 \ &amp;&amp; bundle config --local allow_offline_install true# настраиваем .gemrc чтоб не было ничего лишнего, вклюячая rdocRUN set -ex \ &amp;&amp; echo 'gem: --no-document' &gt; /usr/local/etc/gemrc \ &amp;&amp; echo 'update_sources: false' &gt;&gt; /usr/local/etc/gemrc \ &amp;&amp; echo 'verbose: false' &gt;&gt; /usr/local/etc/gemrc \ &amp;&amp; echo 'update: --no-suggestions' &gt;&gt; /usr/local/etc/gemrc \ &amp;&amp; echo 'install: --no-suggestions --conservative' &gt;&gt; /usr/local/etc/gemrc# пытаемся удалить ненужные гемы с самого начала - попытка не пыткаRUN set -ex \ &amp;&amp; gem uninstall -a -x --quiet --force `gem list | cut -f 1 -d " "` \ &amp;&amp; gem cleanupRUN set -ex \ &amp;&amp; apk add --virtual .build-deps \   autoconf \   bison \   bzip2 \   bzip2-dev \   coreutils \   curl-dev \   dpkg-dev dpkg \   g++ \   gcc \   gdbm-dev \   git \   glib-dev \   libc-dev \   libffi-dev \   libxml2-dev \   libxslt-dev \   linux-headers \   make \   ncurses-dev \   procps \   readline-dev \   tar \   xz \   yaml-dev \   zlib-dev \   shared-mime-info \ &amp;&amp; mv /usr/local/lib/ruby/gems/${RUBY_MAJOR}.0/specifications/default/* /usr/local/lib/ruby/gems/${RUBY_MAJOR}.0/specifications/ \ &amp;&amp; gem cleanup \ &amp;&amp; gem update --system "${RUBYGEMS_VERSION}" \ &amp;&amp; gem uninstall bundler --all --silent || true \ &amp;&amp; gem install bundler -v "${BUNDLER_VERSION}" \ &amp;&amp; gem uninstall rdoc --all --silent || true \ &amp;&amp; GMS=`gem list | sed s/default:\ // | sed -E 's/\ \((.*)\)/:\1/' | sort` \ &amp;&amp; DEBUG_FLAGS="-Wno-calloc-transposed-args" gem pristine --all --extensions \ &amp;&amp; commands=$(for x in $GMS; do \   g=$(echo "$x" | cut -f 1 -d ":"); \   v=$(echo "$x" | cut -f 2 -d ":"); \   echo gem pristine $g --version "$v"; \ done) \ &amp;&amp; echo -e "$commands" | xargs -I CMD -P 3 bash -c CMD\ &amp;&amp; gem cleanup \ &amp;&amp; rm -rf root/.local/share/gem/specs \ &amp;&amp; apk del .build-deps \ &amp;&amp; cleanallbuilds.shWORKDIR /home/appRUN set -ex \ &amp;&amp; adduser -D -s /sbin/nologin app \ &amp;&amp; chown -R app:app /home/appENTRYPOINT ["/usr/bin/entrypoint.sh"]SHELL ["/bin/sh", "-c"] Самое интересное, конечно, находится в одном слое самого пухленького RUN, и по некоторым командам надо дать пояснения:                              GMS=gem list          sed s/default:\ //          sed -E ‘s/\ ((.*))/:\1/’          sort`` - получаем список установленных гемов с версиями в формате “yaml:0.4.0 zlib:3.2.1”. Это нам потребуется дальше из-за странного поведения gem pristine                      gem pristine --all --extensions - должен для всех установленных гемов сделать “чистовую” установку, включая скачивание и перекомпиляцию расширений. Но нет. На самом деле он делает не для всех и не всё :(  DEBUG_FLAGS="-Wno-calloc-transposed-args" - мы используем один Dockerfile для сборки базовых образов ruby 2.7, 3.0, 3.1, 3.2, и по умолчанию все расширения должны компилироваться без единого разрыва ворнинга компиляции, но для некоторых версий руби (кажется 3.0) это не так, и именно этот ворнинг всё портит, поэтому его подавляем без затей.  commands=$(for x in $GMS; do … - а вы знали, что в bash тоже есть пул потоков? Строго говоря, он пул процессов, и не в баше, а в xargs, но это не важно. В общем тут список $GMS в виде “yaml:0.4.0 zlib:3.2.1” трансформируется в список $commands, потому что в такой форме gem pristine работает именно так как надо:gem pristine yaml --version "0.4.0"gem pristine zlib --version "3.2.1"...  echo -e "$commands" | xargs -I CMD -P 3 bash -c CMD - отправляем $commands в пул из трех процессов для одновременной установки гемов.  cleanallbuilds.sh - зовём в самом конце кустарный скрипт, который удаляет всякий мусор из системы:#!/bin/sh# ruby (alpine + debian)# Скрипт единый идля debian и для alpine, поэтому ошибки,# возникающие из-за разницы дистрибутивов просто игнорируемrm -rf /usr/src/ruby/ || truerm -rf /root/.local/share/gem/specs/ || truerm -rf /root/.local/state/ || truerm -rf /root/.cache/gem/ || truerm -rf /usr/local/lib/ruby/gems/3.2.0/cache/ || truerm -rf /root/.bundle/cache/ || truegem cleanup || true# alpineapk cache clean || true# debianapt-get clean autoclean || trueapt-get autoclean --yes || trueapt-get autoremove --yes || truefind /var/log/ -type f -delete || truefind /var/lib/log/ -type f -delete || truefind /usr/share/doc/ -type f -delete || trueРезультатСтарый образ занимал 900MB, новый - 152MB (не спрашивайте…)Значительно лучше. Особенно для базового образаЧто дальше?Теперь эти базовые образы надо начать использовать непосредственно в CI пайплайнах боевых сервисов и, конечно, процедуру сборки и тестирования придётся поменять. Точнее, мы уже перешли на новые образы и сборку уже пару недель назад - дело теперь только за статьёй. А в качестве приманки приведу результаты перехода для одного из самых “толстых” наших сервисов:image:old size:1.49GB vulns:255image:new-prod size:259MB vulns:1 (Вчерашняя!! СVE-2025-25186)image:new-test size:269MB vulns:1 (СVE-2025-25186)Если взглянуть ретроспективно, то совсем не весело - так что:    профилируйте чаще не только ваш код и инфраструктуру сборки.      #docker    #devsecops    #ruby    #cicd  </yandex:full-text>

        
      </item>
    
      <item>
        <guid isPermaLink="true">https://kinnalru.github.io/2022/09/30/ispolzovanie_scientist_dlya_refaktoringa_kriticheskih_uchastkov_ruby_on_rails_prilozheniya.html</guid>
        <link>https://kinnalru.github.io/2022/09/30/ispolzovanie_scientist_dlya_refaktoringa_kriticheskih_uchastkov_ruby_on_rails_prilozheniya.html</link>
        <pdalink>https://kinnalru.github.io/2022/09/30/ispolzovanie_scientist_dlya_refaktoringa_kriticheskih_uchastkov_ruby_on_rails_prilozheniya.html</pdalink>
        <dc:creator>kinnalru</dc:creator>
        <author>kinnalru@gmail.com (Самойленко Юрий)</author>
        <title>Использование Scientist для рефакторинга критических участков Ruby on Rails приложения</title>
        <pubDate>Fri, 30 Sep 2022 15:19:06 +0300</pubDate>
        <yandex:genre>article</yandex:genre>
        
        <media:content medium="image" url="https://img4.teletype.in/files/fe/ce/fece8ad1-1d4a-45ab-924c-b3d16342aa98.jpeg"/>
        
        <description><![CDATA[ <p><a href="https://blog.appsignal.com/2022/05/18/using-scientist-to-refactor-critical-ruby-on-rails-code.html">Перевод статьи <em>“Using Scientist to Refactor Critical Ruby on Rails Code”</em></a><br /><a href="https://blog.appsignal.com/authors/darren-broemmer">Darren Broemmer</a> от 18 мая 2022 года.</p><p><img src="https://img4.teletype.in/files/fe/ce/fece8ad1-1d4a-45ab-924c-b3d16342aa98.jpeg" alt="" /></p><p>Попросите любого программиста просмотреть ключевые участки продуктового кода, и он обязательно укажет на несколько моментов, требующих рефакторинга. Почему же так много плохого, ненадежного или непонятного кода продолжает работать в продуктовом окружении?</p><p>Ответ прост: инженеры боятся его трогать. Задачи для рефакторинга обнаруживаются и добавляются в бэклог, но редко попадают в текущий спринт.</p><p>Для этого есть множество причин. Код мог быть написан программистом, давно покинувшим команду и теперь в нём никто не разбирается. Бывает что этот код имеет решающее значение для бизнеса и из-за этого никто не хочет нести ответственность за потенциальный сбой или потерю дохода.</p><p>В данном посте мы рассмотрим, как можно использовать гем <a href="https://github.com/github/scientist">Scientist</a> для уверенной миграции, рефакторинга и изменения критического продуктового Ruby-кода.</p><p>Но сначала вы спросите — а нельзя ли использовать тесты для поиска ошибок?</p><h2 id="это-ведь-то-самое-для-чего-нужны-тесты-в-rails-верно">Это ведь то самое для чего нужны тесты в Rails, верно?</h2><p>И да и нет. Часто бывает трудно получить полную уверенность в новых изменениях до развертывания. Допустим модульные (Unit) и системные тесты проходят. Этого достаточно?</p><p>Реальность такова, что ничто не может заменить реальный мир, то есть продуктовую эксплуатацию. А если качество тестовых данных плохое или тесты отсутствуют? А будет ли новое ПО работать достаточно хорошо, чтобы справляться с нагрузкой?</p><p>Команды с публичными сервисами иногда обнаруживают, что им нужно решать проблемы «совместимости с ошибками» (bugwards compatibility). Когда ошибка существует в рабочей среде некоторое время, клиенты могут обходить её таким способом, который зависит от привычного неправильного поведения. Клиенты часто используют ваш софт неожиданным образом.</p><h2 id="с-scientist-можно-следить-за-правками-в-в-ruby-и-rails-сразу-в-боевом-окружении">с Scientist можно следить за правками в в Ruby и Rails сразу в боевом окружении</h2><p>Если продуктовое окружение это лучшее место, где можно обрести уверенность в своих правках, можно подумать о том, чтобы понаблюдать за поведением кода именно там. Поначалу это может показаться пугающим, ведь идея «тестирования в бою» противоречит классическим методам разработки программного обеспечения.</p><p>Однако хорошая новость заключается в том, что это можно легко и безопасно сделать в Ruby и Rails с помощью гема <a href="https://github.com/github/scientist">Scientist</a>. Название утилиты основано на научном методе проведения экспериментов для проверки гипотез. В данном случае наша гипотеза состоит в том, что новый код работает, а его использование это “эксперимент”.</p><p>Причина, по которой мы можем безопасно использовать этот подход, заключается в том, что в экспериментах по-прежнему используется результат существующего кода. Новый код оценивается только в целях наблюдения и сравнения, как на точность, так и на производительность. Мы уменьшаем проблемы с тестовым покрытием, которые обсуждались ранее, оценивая производительность с использованием реальных данных и параметров в боевом окружении. Эксперименты обычно проводят с определённой частотой, чтобы свести к минимуму влияние на производительность. Однако при желании можно оценить и каждый вызов.</p><p>Давайте теперь кратко рассмотрим, как Scientist работает с техникой “<a href="https://martinfowler.com/bliki/BranchByAbstraction.html">Branch by Abstraction</a>”.</p><h2 id="паттерн-branch-by-abstraction-в-геме-scientist">Паттерн “Branch by Abstraction” в геме Scientist</h2><p>Работа Scientist начинается с паттерна <a href="https://martinfowler.com/bliki/BranchByAbstraction.html">Branch by Abstraction</a>, описанного Мартином Фаулером как «постепенное крупномасштабное изменение программной системы».</p><p>Мы вводим уровень абстракции, чтобы изолировать обновляемый код. Этот уровень решает, какую реализацию использовать, чтобы эксперимент был прозрачен для остальной части системы. Данный метод связан с использованием флага (feature flag), который определяет какая из ветвей кода будет исполнена.</p><p><a href="https://github.blog/2016-02-03-scientist/">Гем Scientist, созданный в Github</a>, реализует этот паттерн с помощью “эксперимента”. Существующий код называется “контрольным”, а новая реализация — “кандидатом”. Обе части кода выполняются в случайном порядке, но клиенту возвращается только результат ”контрольной” части.</p><h2 id="использование-scientist-для-рефакторинга-сервиса-в-ruby">Использование Scientist для рефакторинга сервиса в Ruby</h2><p>Рассмотрим <a href="https://github.com/dbroemme/scientist-labtech-example/blob/master/app/helpers/prime_factor_helper.rb#L20">Ruby-сервис</a>, который возвращает наибольший простой множитель для заданного числа. Предположим, мы определили способы оптимизации для сокращения необходимого набора кандидатов, что ускорит работу сервиса.</p><p>Однако владельцы сервиса хотят быть уверены, что при оптимизации не вкралась ошибка. Они также хотят увидеть все улучшения производительности. Давайте напишем <a href="https://github.com/dbroemme/scientist-labtech-example/blob/master/app/helpers/prime_factor_helper.rb#L3">следующий код</a>, для вызова этого метода:</p><div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'scientist'</span> <span class="k">def</span> <span class="nf">largest_prime_factor</span><span class="p">(</span><span class="n">number</span><span class="p">)</span>  <span class="n">science</span> <span class="s2">"prime-factors"</span> <span class="k">do</span> <span class="o">|</span><span class="n">experiment</span><span class="o">|</span>    <span class="n">experiment</span><span class="p">.</span><span class="nf">use</span> <span class="p">{</span> <span class="n">find_largest_prime_factor</span><span class="p">(</span><span class="n">number</span><span class="p">)</span> <span class="p">}</span> <span class="c1"># old way</span>    <span class="n">experiment</span><span class="p">.</span><span class="nf">try</span> <span class="p">{</span> <span class="n">improved_largest_prime_factor</span><span class="p">(</span><span class="n">number</span><span class="p">)</span> <span class="p">}</span> <span class="c1"># new way</span>  <span class="k">end</span> <span class="c1"># returns the control value</span><span class="k">end</span></code></pre></div></div><p>В этот момент вызывается только выражение <code class="language-plaintext highlighter-rouge">use</code> (контрольное). Но чтобы сделать эксперимент полезным, надо определить пользовательский класс <code class="language-plaintext highlighter-rouge">Experiment</code>, чтобы включить его и опубликовать результаты (в данном случае просто логирование). Scientist генерирует очень полезные данные, но по умолчанию ничего с ними не делает. Эта часть остается на ваше усмотрение.</p><div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'scientist/experiment'</span><span class="nb">require</span> <span class="s1">'pp'</span> <span class="k">class</span> <span class="nc">MyExperiment</span>  <span class="kp">include</span> <span class="no">Scientist</span><span class="o">::</span><span class="no">Experiment</span>   <span class="nb">attr_accessor</span> <span class="ss">:name</span>   <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="nb">name</span><span class="p">)</span>    <span class="vi">@name</span> <span class="o">=</span> <span class="nb">name</span>  <span class="k">end</span>   <span class="k">def</span> <span class="nf">enabled?</span>    <span class="kp">true</span>  <span class="k">end</span>   <span class="k">def</span> <span class="nf">raised</span><span class="p">(</span><span class="n">operation</span><span class="p">,</span> <span class="n">error</span><span class="p">)</span>    <span class="nb">p</span> <span class="s2">"Operation '</span><span class="si">#{</span><span class="n">operation</span><span class="si">}</span><span class="s2">' failed with error '</span><span class="si">#{</span><span class="n">error</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2">'"</span>    <span class="k">super</span> <span class="c1"># will re-raise</span>  <span class="k">end</span>   <span class="k">def</span> <span class="nf">publish</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>    <span class="n">pp</span> <span class="n">result</span>  <span class="k">end</span><span class="k">end</span></code></pre></div></div><p>Результаты эксперимента будут зарегистрированы, и мы сможем со временем вносить улучшения на основе анализа. Как только новый код будет соответствовать требованиям и доверие к нему станет высоким, осуществим переход на новую реализацию, просто заменив код делегированием в новую реализацию.</p><h2 id="labtech-для-упрощения-использования-scientist-в-ruby-on-rails">LabTech для упрощения использования Scientist в Ruby on Rails</h2><p>Есть <a href="https://github.com/RealGeeks/lab_tech">гем LabTech</a>, который может помочь настроить Scientist в Rails приложении и удобнее обрабатывать результаты.</p><p>Приложения, использующие <code class="language-plaintext highlighter-rouge">AppSignal</code>, могут использовать <a href="https://docs.appsignal.com/ruby/instrumentation/instrumentation.html">вспомогательный инструментарий</a> <code class="language-plaintext highlighter-rouge">Appsignal.instrument</code>, чтобы отслеживать, сколько времени требуется для выполнения событий Scientist. Обернув в него код эксперимента, можно увидеть, как события появляются в <code class="language-plaintext highlighter-rouge">AppSIgnal</code>.</p><p>Теперь вернемся к LabTech — пример ниже просто принимает число для разложения.</p><p><img src="https://img2.teletype.in/files/9d/db/9ddbfb11-6a14-40c1-b318-5ddda1392c70.png" alt="" /></p><p>Приступить к работе легко, если у вас есть доступ к консоли. Сначала надо добавить гем LabTech в Gemfile и запустить <code class="language-plaintext highlighter-rouge">bundle install</code>.</p><div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem 'lab_tech'</code></pre></div></div><p>Результаты и конфигурация эксперимента хранятся в БД, поэтому требуется миграция.</p><div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails lab_tech:install:migrations db:migrate</code></pre></div></div><p>Уровень абстракции тот же, за исключением того, что используется модуль LabTech. Полный код доступен на GitHub.</p><div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">largest_prime_factor</span><span class="p">(</span><span class="n">number</span><span class="p">)</span>    <span class="no">LabTech</span><span class="p">.</span><span class="nf">science</span> <span class="s2">"prime-factors"</span> <span class="k">do</span> <span class="o">|</span><span class="n">experiment</span><span class="o">|</span>      <span class="o">...</span>    <span class="k">end</span><span class="k">end</span></code></pre></div></div><p>На данный момент эксперимент отключен, поэтому воспользуемся консолью, чтобы включить его во всех случаях или настроить процент исполнения.</p><div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bin/rails consoleLabTech.enable <span class="s2">"prime-factors"</span>LabTech.enable <span class="s2">"prime-factors"</span>, percent: 5</code></pre></div></div><p>Теперь можно запустить тесты, и эксперимент будет выполнен. Для текстового представления результатов есть следующие команды в консоли Rails.</p><div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">LabTech</span><span class="p">.</span><span class="nf">summarize_results</span> <span class="s2">"prime-factors"</span><span class="no">LabTech</span><span class="p">.</span><span class="nf">summarize_errors</span> <span class="s2">"prime-factors"</span></code></pre></div></div><p>После нескольких успешных прогонов и одной синтетической ошибки получим пример того, как выглядит сводка результатов. Существует обзор успехов и ошибок, а также диаграмма ASCII, показывающая различия в производительности.</p><div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-Earliest results: 2022-04-27T02:42:45ZLatest result: 2022-05-01T17:27:39Z (5 days) 3 of 4 (75.00%) correct1 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----</code></pre></div></div><p>Для лёгкого и удобного анализа результатов есть гем <a href="https://github.com/ankane/blazer">Blazer</a>. Он прост в установке и позволяет выполнять SQL-запросы. Запрос здесь показывает, что реализация-кандидат значительно быстрее оригинала.</p><p><img src="https://img3.teletype.in/files/a1/92/a192082e-2e7f-4b39-a978-d4f9857ed56a.png" alt="" /></p><p>В примере нашего сервиса, возвращающего наибольший простой делитель, ускорение в улучшенной реализации происходит за счет эвристики, исключающей некоторые возможные факторы, которые необходимо учитывать. Поскольку мы рассматриваем большие числа и находим простой множитель, мы можем прекратить поиск после того, как доберемся до целевого числа, делённого на этот множитель. Новый код добавляет только <a href="https://github.com/dbroemme/scientist-labtech-example/blob/master/app/helpers/prime_factor_helper.rb#L38">одну инструкцию</a> для достижения этой цели.</p><p>Мы также можем увидеть сокращение времени выполнения используя Blazer.</p><p><img src="https://img3.teletype.in/files/6b/06/6b06ffce-40d8-42ed-b1e0-7a9a4190a876.png" alt="" /></p><h2 id="варианты-использования-и-ограничения-scientist">Варианты использования и ограничения Scientist</h2><p>Оптимальное использование Scientist включает задачи поиска, вычисления и код, который не имеет побочных эффектов. Код, включающий транзакционные обновления или внешние интеграции, такие как электронная почта, не совсем вписывается в модель, поскольку изменяемое действие выполняется дважды (как в старой, так и в новой реализации).</p><p>Это не тривиальное ограничение. Оно блокирует несколько вариантов использования. Однако есть обходные пути, если эксперимент очень важен. Стоит подумать имеют ли значение побочные эффекты или дублирование или они не являются проблемой. Например, в некоторых случаях может не иметь значения, будут ли отправлены два электронных письма во время оценки. Другой вариант — сделать так, чтобы новый код определял результат, но не сохранял его. Это не позволит сравнить производительность, но позволит проверить точность.</p><p>Другие ограничения проистекают из того, что Scientist фокусируется на возвращаемых значениях. В некоторых случаях действительные результаты могут отличаться с течением времени, независимо от того, включают ли они просто временные метки в ответ или изменяются сторонние факторы. Во многих случаях мы можем написать пользовательскую логику сравнения в эксперименте, чтобы проверить точность, выходящую за рамки обычного сравнения строк.</p><p>Наконец, ограничение LabTech заключается в том, что он не был портирован на Rails 7 на момент написания.</p><h2 id="лучшие-практики-для-эффективных-scientist-экспериментов-в-rails">Лучшие практики для эффективных Scientist-экспериментов в Rails</h2><p>При проведении экспериментов стоит учитывать следующие моменты:</p><ul>  <li>В Rails-проектах Scientist может быть настроен либо в инициализаторе, либо в оболочке, такой как гем Rails LabTech. Большинство приложений Rails уже имеют базу данных, поэтому LabTech использует ActiveRecord для хранения результатов.</li>  <li>Чтобы не замедлять разработку и тестирование, включайте эксперимент только в промежуточной (staging) и продуктовой средах.</li>  <li>Чтобы свести к минимуму любое потенциальное влияние на продуктовую среду, запускайте эксперимент только для некоторого процента запросов. LabTech поддерживает это из коробки как необязательный параметр при включении эксперимента (изначально он отключен по умолчанию). Используя чистый Scientist, эту логику легко реализовать через <code class="language-plaintext highlighter-rouge">enabled?</code> метод.</li>  <li>Некоторая логика требует больших ресурсов, поэтому хорошей отправной точкой может быть низкая частота дискретизации. По мере того, как вы обретете уверенность в результатах, увеличивайте процент оцениваемых запросов.</li>  <li>Вы можете добавить атрибуты контекста, чтобы получить максимальную отдачу от результатов. В качестве контекста эксперимента может быть задан хэш с параметрами, который затем становится доступным в опубликованных результатах, например:</li></ul><div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">experiment</span><span class="p">.</span><span class="nf">context</span> <span class="ss">:user</span> <span class="o">=&gt;</span> <span class="n">user</span></code></pre></div></div><h2 id="подводя-итоги-наблюдайте-и-контролируйте-свое-приложение-ruby-с-помощью-scientist">Подводя итоги: наблюдайте и контролируйте свое приложение Ruby с помощью Scientist</h2><p>В этом посте мы рассмотрели, как использовать гем Scientist для изменения, переноса и рефакторинга кода Ruby в продуктовой среде.</p><p>Мы рассмотрели место Scientist в паттерне Branch by Abstraction, и погрузились в рефакторинг. Затем увидели, как LabTech помогает со сбором результатов и конфигурацией Scientist.</p><p>Мы коснулись некоторых ограничений Scientist, прежде чем, наконец, изложить несколько лучших практик.</p><p>Вам необходимо наблюдать и контролировать то, что происходит в системе. Интегрируйте Scientist в процесс разработки, чтобы с большей уверенностью вносить критические изменения в код Ruby.</p><p>Happy coding!</p><p><a href="https://blog.appsignal.com/2022/05/18/using-scientist-to-refactor-critical-ruby-on-rails-code.html">Перевод статьи <em>“Using Scientist to Refactor Critical Ruby on Rails Code”</em></a><br /><a href="https://blog.appsignal.com/authors/darren-broemmer">Darren Broemmer</a> от 18 мая 2022 года.</p> ]]></description>
        <content:encoded><![CDATA[ <p><a href="https://blog.appsignal.com/2022/05/18/using-scientist-to-refactor-critical-ruby-on-rails-code.html">Перевод статьи <em>“Using Scientist to Refactor Critical Ruby on Rails Code”</em></a><br /><a href="https://blog.appsignal.com/authors/darren-broemmer">Darren Broemmer</a> от 18 мая 2022 года.</p><p><img src="https://img4.teletype.in/files/fe/ce/fece8ad1-1d4a-45ab-924c-b3d16342aa98.jpeg" alt="" /></p><p>Попросите любого программиста просмотреть ключевые участки продуктового кода, и он обязательно укажет на несколько моментов, требующих рефакторинга. Почему же так много плохого, ненадежного или непонятного кода продолжает работать в продуктовом окружении?</p><p>Ответ прост: инженеры боятся его трогать. Задачи для рефакторинга обнаруживаются и добавляются в бэклог, но редко попадают в текущий спринт.</p><p>Для этого есть множество причин. Код мог быть написан программистом, давно покинувшим команду и теперь в нём никто не разбирается. Бывает что этот код имеет решающее значение для бизнеса и из-за этого никто не хочет нести ответственность за потенциальный сбой или потерю дохода.</p><p>В данном посте мы рассмотрим, как можно использовать гем <a href="https://github.com/github/scientist">Scientist</a> для уверенной миграции, рефакторинга и изменения критического продуктового Ruby-кода.</p><p>Но сначала вы спросите — а нельзя ли использовать тесты для поиска ошибок?</p><h2 id="это-ведь-то-самое-для-чего-нужны-тесты-в-rails-верно">Это ведь то самое для чего нужны тесты в Rails, верно?</h2><p>И да и нет. Часто бывает трудно получить полную уверенность в новых изменениях до развертывания. Допустим модульные (Unit) и системные тесты проходят. Этого достаточно?</p><p>Реальность такова, что ничто не может заменить реальный мир, то есть продуктовую эксплуатацию. А если качество тестовых данных плохое или тесты отсутствуют? А будет ли новое ПО работать достаточно хорошо, чтобы справляться с нагрузкой?</p><p>Команды с публичными сервисами иногда обнаруживают, что им нужно решать проблемы «совместимости с ошибками» (bugwards compatibility). Когда ошибка существует в рабочей среде некоторое время, клиенты могут обходить её таким способом, который зависит от привычного неправильного поведения. Клиенты часто используют ваш софт неожиданным образом.</p><h2 id="с-scientist-можно-следить-за-правками-в-в-ruby-и-rails-сразу-в-боевом-окружении">с Scientist можно следить за правками в в Ruby и Rails сразу в боевом окружении</h2><p>Если продуктовое окружение это лучшее место, где можно обрести уверенность в своих правках, можно подумать о том, чтобы понаблюдать за поведением кода именно там. Поначалу это может показаться пугающим, ведь идея «тестирования в бою» противоречит классическим методам разработки программного обеспечения.</p><p>Однако хорошая новость заключается в том, что это можно легко и безопасно сделать в Ruby и Rails с помощью гема <a href="https://github.com/github/scientist">Scientist</a>. Название утилиты основано на научном методе проведения экспериментов для проверки гипотез. В данном случае наша гипотеза состоит в том, что новый код работает, а его использование это “эксперимент”.</p><p>Причина, по которой мы можем безопасно использовать этот подход, заключается в том, что в экспериментах по-прежнему используется результат существующего кода. Новый код оценивается только в целях наблюдения и сравнения, как на точность, так и на производительность. Мы уменьшаем проблемы с тестовым покрытием, которые обсуждались ранее, оценивая производительность с использованием реальных данных и параметров в боевом окружении. Эксперименты обычно проводят с определённой частотой, чтобы свести к минимуму влияние на производительность. Однако при желании можно оценить и каждый вызов.</p><p>Давайте теперь кратко рассмотрим, как Scientist работает с техникой “<a href="https://martinfowler.com/bliki/BranchByAbstraction.html">Branch by Abstraction</a>”.</p><h2 id="паттерн-branch-by-abstraction-в-геме-scientist">Паттерн “Branch by Abstraction” в геме Scientist</h2><p>Работа Scientist начинается с паттерна <a href="https://martinfowler.com/bliki/BranchByAbstraction.html">Branch by Abstraction</a>, описанного Мартином Фаулером как «постепенное крупномасштабное изменение программной системы».</p><p>Мы вводим уровень абстракции, чтобы изолировать обновляемый код. Этот уровень решает, какую реализацию использовать, чтобы эксперимент был прозрачен для остальной части системы. Данный метод связан с использованием флага (feature flag), который определяет какая из ветвей кода будет исполнена.</p><p><a href="https://github.blog/2016-02-03-scientist/">Гем Scientist, созданный в Github</a>, реализует этот паттерн с помощью “эксперимента”. Существующий код называется “контрольным”, а новая реализация — “кандидатом”. Обе части кода выполняются в случайном порядке, но клиенту возвращается только результат ”контрольной” части.</p><h2 id="использование-scientist-для-рефакторинга-сервиса-в-ruby">Использование Scientist для рефакторинга сервиса в Ruby</h2><p>Рассмотрим <a href="https://github.com/dbroemme/scientist-labtech-example/blob/master/app/helpers/prime_factor_helper.rb#L20">Ruby-сервис</a>, который возвращает наибольший простой множитель для заданного числа. Предположим, мы определили способы оптимизации для сокращения необходимого набора кандидатов, что ускорит работу сервиса.</p><p>Однако владельцы сервиса хотят быть уверены, что при оптимизации не вкралась ошибка. Они также хотят увидеть все улучшения производительности. Давайте напишем <a href="https://github.com/dbroemme/scientist-labtech-example/blob/master/app/helpers/prime_factor_helper.rb#L3">следующий код</a>, для вызова этого метода:</p><div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'scientist'</span> <span class="k">def</span> <span class="nf">largest_prime_factor</span><span class="p">(</span><span class="n">number</span><span class="p">)</span>  <span class="n">science</span> <span class="s2">"prime-factors"</span> <span class="k">do</span> <span class="o">|</span><span class="n">experiment</span><span class="o">|</span>    <span class="n">experiment</span><span class="p">.</span><span class="nf">use</span> <span class="p">{</span> <span class="n">find_largest_prime_factor</span><span class="p">(</span><span class="n">number</span><span class="p">)</span> <span class="p">}</span> <span class="c1"># old way</span>    <span class="n">experiment</span><span class="p">.</span><span class="nf">try</span> <span class="p">{</span> <span class="n">improved_largest_prime_factor</span><span class="p">(</span><span class="n">number</span><span class="p">)</span> <span class="p">}</span> <span class="c1"># new way</span>  <span class="k">end</span> <span class="c1"># returns the control value</span><span class="k">end</span></code></pre></div></div><p>В этот момент вызывается только выражение <code class="language-plaintext highlighter-rouge">use</code> (контрольное). Но чтобы сделать эксперимент полезным, надо определить пользовательский класс <code class="language-plaintext highlighter-rouge">Experiment</code>, чтобы включить его и опубликовать результаты (в данном случае просто логирование). Scientist генерирует очень полезные данные, но по умолчанию ничего с ними не делает. Эта часть остается на ваше усмотрение.</p><div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'scientist/experiment'</span><span class="nb">require</span> <span class="s1">'pp'</span> <span class="k">class</span> <span class="nc">MyExperiment</span>  <span class="kp">include</span> <span class="no">Scientist</span><span class="o">::</span><span class="no">Experiment</span>   <span class="nb">attr_accessor</span> <span class="ss">:name</span>   <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="nb">name</span><span class="p">)</span>    <span class="vi">@name</span> <span class="o">=</span> <span class="nb">name</span>  <span class="k">end</span>   <span class="k">def</span> <span class="nf">enabled?</span>    <span class="kp">true</span>  <span class="k">end</span>   <span class="k">def</span> <span class="nf">raised</span><span class="p">(</span><span class="n">operation</span><span class="p">,</span> <span class="n">error</span><span class="p">)</span>    <span class="nb">p</span> <span class="s2">"Operation '</span><span class="si">#{</span><span class="n">operation</span><span class="si">}</span><span class="s2">' failed with error '</span><span class="si">#{</span><span class="n">error</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2">'"</span>    <span class="k">super</span> <span class="c1"># will re-raise</span>  <span class="k">end</span>   <span class="k">def</span> <span class="nf">publish</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>    <span class="n">pp</span> <span class="n">result</span>  <span class="k">end</span><span class="k">end</span></code></pre></div></div><p>Результаты эксперимента будут зарегистрированы, и мы сможем со временем вносить улучшения на основе анализа. Как только новый код будет соответствовать требованиям и доверие к нему станет высоким, осуществим переход на новую реализацию, просто заменив код делегированием в новую реализацию.</p><h2 id="labtech-для-упрощения-использования-scientist-в-ruby-on-rails">LabTech для упрощения использования Scientist в Ruby on Rails</h2><p>Есть <a href="https://github.com/RealGeeks/lab_tech">гем LabTech</a>, который может помочь настроить Scientist в Rails приложении и удобнее обрабатывать результаты.</p><p>Приложения, использующие <code class="language-plaintext highlighter-rouge">AppSignal</code>, могут использовать <a href="https://docs.appsignal.com/ruby/instrumentation/instrumentation.html">вспомогательный инструментарий</a> <code class="language-plaintext highlighter-rouge">Appsignal.instrument</code>, чтобы отслеживать, сколько времени требуется для выполнения событий Scientist. Обернув в него код эксперимента, можно увидеть, как события появляются в <code class="language-plaintext highlighter-rouge">AppSIgnal</code>.</p><p>Теперь вернемся к LabTech — пример ниже просто принимает число для разложения.</p><p><img src="https://img2.teletype.in/files/9d/db/9ddbfb11-6a14-40c1-b318-5ddda1392c70.png" alt="" /></p><p>Приступить к работе легко, если у вас есть доступ к консоли. Сначала надо добавить гем LabTech в Gemfile и запустить <code class="language-plaintext highlighter-rouge">bundle install</code>.</p><div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem 'lab_tech'</code></pre></div></div><p>Результаты и конфигурация эксперимента хранятся в БД, поэтому требуется миграция.</p><div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails lab_tech:install:migrations db:migrate</code></pre></div></div><p>Уровень абстракции тот же, за исключением того, что используется модуль LabTech. Полный код доступен на GitHub.</p><div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">largest_prime_factor</span><span class="p">(</span><span class="n">number</span><span class="p">)</span>    <span class="no">LabTech</span><span class="p">.</span><span class="nf">science</span> <span class="s2">"prime-factors"</span> <span class="k">do</span> <span class="o">|</span><span class="n">experiment</span><span class="o">|</span>      <span class="o">...</span>    <span class="k">end</span><span class="k">end</span></code></pre></div></div><p>На данный момент эксперимент отключен, поэтому воспользуемся консолью, чтобы включить его во всех случаях или настроить процент исполнения.</p><div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bin/rails consoleLabTech.enable <span class="s2">"prime-factors"</span>LabTech.enable <span class="s2">"prime-factors"</span>, percent: 5</code></pre></div></div><p>Теперь можно запустить тесты, и эксперимент будет выполнен. Для текстового представления результатов есть следующие команды в консоли Rails.</p><div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">LabTech</span><span class="p">.</span><span class="nf">summarize_results</span> <span class="s2">"prime-factors"</span><span class="no">LabTech</span><span class="p">.</span><span class="nf">summarize_errors</span> <span class="s2">"prime-factors"</span></code></pre></div></div><p>После нескольких успешных прогонов и одной синтетической ошибки получим пример того, как выглядит сводка результатов. Существует обзор успехов и ошибок, а также диаграмма ASCII, показывающая различия в производительности.</p><div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-Earliest results: 2022-04-27T02:42:45ZLatest result: 2022-05-01T17:27:39Z (5 days) 3 of 4 (75.00%) correct1 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----</code></pre></div></div><p>Для лёгкого и удобного анализа результатов есть гем <a href="https://github.com/ankane/blazer">Blazer</a>. Он прост в установке и позволяет выполнять SQL-запросы. Запрос здесь показывает, что реализация-кандидат значительно быстрее оригинала.</p><p><img src="https://img3.teletype.in/files/a1/92/a192082e-2e7f-4b39-a978-d4f9857ed56a.png" alt="" /></p><p>В примере нашего сервиса, возвращающего наибольший простой делитель, ускорение в улучшенной реализации происходит за счет эвристики, исключающей некоторые возможные факторы, которые необходимо учитывать. Поскольку мы рассматриваем большие числа и находим простой множитель, мы можем прекратить поиск после того, как доберемся до целевого числа, делённого на этот множитель. Новый код добавляет только <a href="https://github.com/dbroemme/scientist-labtech-example/blob/master/app/helpers/prime_factor_helper.rb#L38">одну инструкцию</a> для достижения этой цели.</p><p>Мы также можем увидеть сокращение времени выполнения используя Blazer.</p><p><img src="https://img3.teletype.in/files/6b/06/6b06ffce-40d8-42ed-b1e0-7a9a4190a876.png" alt="" /></p><h2 id="варианты-использования-и-ограничения-scientist">Варианты использования и ограничения Scientist</h2><p>Оптимальное использование Scientist включает задачи поиска, вычисления и код, который не имеет побочных эффектов. Код, включающий транзакционные обновления или внешние интеграции, такие как электронная почта, не совсем вписывается в модель, поскольку изменяемое действие выполняется дважды (как в старой, так и в новой реализации).</p><p>Это не тривиальное ограничение. Оно блокирует несколько вариантов использования. Однако есть обходные пути, если эксперимент очень важен. Стоит подумать имеют ли значение побочные эффекты или дублирование или они не являются проблемой. Например, в некоторых случаях может не иметь значения, будут ли отправлены два электронных письма во время оценки. Другой вариант — сделать так, чтобы новый код определял результат, но не сохранял его. Это не позволит сравнить производительность, но позволит проверить точность.</p><p>Другие ограничения проистекают из того, что Scientist фокусируется на возвращаемых значениях. В некоторых случаях действительные результаты могут отличаться с течением времени, независимо от того, включают ли они просто временные метки в ответ или изменяются сторонние факторы. Во многих случаях мы можем написать пользовательскую логику сравнения в эксперименте, чтобы проверить точность, выходящую за рамки обычного сравнения строк.</p><p>Наконец, ограничение LabTech заключается в том, что он не был портирован на Rails 7 на момент написания.</p><h2 id="лучшие-практики-для-эффективных-scientist-экспериментов-в-rails">Лучшие практики для эффективных Scientist-экспериментов в Rails</h2><p>При проведении экспериментов стоит учитывать следующие моменты:</p><ul>  <li>В Rails-проектах Scientist может быть настроен либо в инициализаторе, либо в оболочке, такой как гем Rails LabTech. Большинство приложений Rails уже имеют базу данных, поэтому LabTech использует ActiveRecord для хранения результатов.</li>  <li>Чтобы не замедлять разработку и тестирование, включайте эксперимент только в промежуточной (staging) и продуктовой средах.</li>  <li>Чтобы свести к минимуму любое потенциальное влияние на продуктовую среду, запускайте эксперимент только для некоторого процента запросов. LabTech поддерживает это из коробки как необязательный параметр при включении эксперимента (изначально он отключен по умолчанию). Используя чистый Scientist, эту логику легко реализовать через <code class="language-plaintext highlighter-rouge">enabled?</code> метод.</li>  <li>Некоторая логика требует больших ресурсов, поэтому хорошей отправной точкой может быть низкая частота дискретизации. По мере того, как вы обретете уверенность в результатах, увеличивайте процент оцениваемых запросов.</li>  <li>Вы можете добавить атрибуты контекста, чтобы получить максимальную отдачу от результатов. В качестве контекста эксперимента может быть задан хэш с параметрами, который затем становится доступным в опубликованных результатах, например:</li></ul><div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">experiment</span><span class="p">.</span><span class="nf">context</span> <span class="ss">:user</span> <span class="o">=&gt;</span> <span class="n">user</span></code></pre></div></div><h2 id="подводя-итоги-наблюдайте-и-контролируйте-свое-приложение-ruby-с-помощью-scientist">Подводя итоги: наблюдайте и контролируйте свое приложение Ruby с помощью Scientist</h2><p>В этом посте мы рассмотрели, как использовать гем Scientist для изменения, переноса и рефакторинга кода Ruby в продуктовой среде.</p><p>Мы рассмотрели место Scientist в паттерне Branch by Abstraction, и погрузились в рефакторинг. Затем увидели, как LabTech помогает со сбором результатов и конфигурацией Scientist.</p><p>Мы коснулись некоторых ограничений Scientist, прежде чем, наконец, изложить несколько лучших практик.</p><p>Вам необходимо наблюдать и контролировать то, что происходит в системе. Интегрируйте Scientist в процесс разработки, чтобы с большей уверенностью вносить критические изменения в код Ruby.</p><p>Happy coding!</p><p><a href="https://blog.appsignal.com/2022/05/18/using-scientist-to-refactor-critical-ruby-on-rails-code.html">Перевод статьи <em>“Using Scientist to Refactor Critical Ruby on Rails Code”</em></a><br /><a href="https://blog.appsignal.com/authors/darren-broemmer">Darren Broemmer</a> от 18 мая 2022 года.</p> ]]></content:encoded>
        <yandex:full-text>Перевод статьи “Using Scientist to Refactor Critical Ruby on Rails Code”Darren Broemmer от 18 мая 2022 года.Попросите любого программиста просмотреть ключевые участки продуктового кода, и он обязательно укажет на несколько моментов, требующих рефакторинга. Почему же так много плохого, ненадежного или непонятного кода продолжает работать в продуктовом окружении?Ответ прост: инженеры боятся его трогать. Задачи для рефакторинга обнаруживаются и добавляются в бэклог, но редко попадают в текущий спринт.Для этого есть множество причин. Код мог быть написан программистом, давно покинувшим команду и теперь в нём никто не разбирается. Бывает что этот код имеет решающее значение для бизнеса и из-за этого никто не хочет нести ответственность за потенциальный сбой или потерю дохода.В данном посте мы рассмотрим, как можно использовать гем Scientist для уверенной миграции, рефакторинга и изменения критического продуктового Ruby-кода.Но сначала вы спросите — а нельзя ли использовать тесты для поиска ошибок?Это ведь то самое для чего нужны тесты в Rails, верно?И да и нет. Часто бывает трудно получить полную уверенность в новых изменениях до развертывания. Допустим модульные (Unit) и системные тесты проходят. Этого достаточно?Реальность такова, что ничто не может заменить реальный мир, то есть продуктовую эксплуатацию. А если качество тестовых данных плохое или тесты отсутствуют? А будет ли новое ПО работать достаточно хорошо, чтобы справляться с нагрузкой?Команды с публичными сервисами иногда обнаруживают, что им нужно решать проблемы «совместимости с ошибками» (bugwards compatibility). Когда ошибка существует в рабочей среде некоторое время, клиенты могут обходить её таким способом, который зависит от привычного неправильного поведения. Клиенты часто используют ваш софт неожиданным образом.с Scientist можно следить за правками в в Ruby и Rails сразу в боевом окруженииЕсли продуктовое окружение это лучшее место, где можно обрести уверенность в своих правках, можно подумать о том, чтобы понаблюдать за поведением кода именно там. Поначалу это может показаться пугающим, ведь идея «тестирования в бою» противоречит классическим методам разработки программного обеспечения.Однако хорошая новость заключается в том, что это можно легко и безопасно сделать в Ruby и Rails с помощью гема Scientist. Название утилиты основано на научном методе проведения экспериментов для проверки гипотез. В данном случае наша гипотеза состоит в том, что новый код работает, а его использование это “эксперимент”.Причина, по которой мы можем безопасно использовать этот подход, заключается в том, что в экспериментах по-прежнему используется результат существующего кода. Новый код оценивается только в целях наблюдения и сравнения, как на точность, так и на производительность. Мы уменьшаем проблемы с тестовым покрытием, которые обсуждались ранее, оценивая производительность с использованием реальных данных и параметров в боевом окружении. Эксперименты обычно проводят с определённой частотой, чтобы свести к минимуму влияние на производительность. Однако при желании можно оценить и каждый вызов.Давайте теперь кратко рассмотрим, как Scientist работает с техникой “Branch by Abstraction”.Паттерн “Branch by Abstraction” в геме ScientistРабота Scientist начинается с паттерна Branch by Abstraction, описанного Мартином Фаулером как «постепенное крупномасштабное изменение программной системы».Мы вводим уровень абстракции, чтобы изолировать обновляемый код. Этот уровень решает, какую реализацию использовать, чтобы эксперимент был прозрачен для остальной части системы. Данный метод связан с использованием флага (feature flag), который определяет какая из ветвей кода будет исполнена.Гем Scientist, созданный в Github, реализует этот паттерн с помощью “эксперимента”. Существующий код называется “контрольным”, а новая реализация — “кандидатом”. Обе части кода выполняются в случайном порядке, но клиенту возвращается только результат ”контрольной” части.Использование Scientist для рефакторинга сервиса в RubyРассмотрим Ruby-сервис, который возвращает наибольший простой множитель для заданного числа. Предположим, мы определили способы оптимизации для сокращения необходимого набора кандидатов, что ускорит работу сервиса.Однако владельцы сервиса хотят быть уверены, что при оптимизации не вкралась ошибка. Они также хотят увидеть все улучшения производительности. Давайте напишем следующий код, для вызова этого метода: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 valueendВ этот момент вызывается только выражение use (контрольное). Но чтобы сделать эксперимент полезным, надо определить пользовательский класс Experiment, чтобы включить его и опубликовать результаты (в данном случае просто логирование). Scientist генерирует очень полезные данные, но по умолчанию ничего с ними не делает. Эта часть остается на ваше усмотрение.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  endendРезультаты эксперимента будут зарегистрированы, и мы сможем со временем вносить улучшения на основе анализа. Как только новый код будет соответствовать требованиям и доверие к нему станет высоким, осуществим переход на новую реализацию, просто заменив код делегированием в новую реализацию.LabTech для упрощения использования Scientist в Ruby on RailsЕсть гем LabTech, который может помочь настроить Scientist в Rails приложении и удобнее обрабатывать результаты.Приложения, использующие AppSignal, могут использовать вспомогательный инструментарий Appsignal.instrument, чтобы отслеживать, сколько времени требуется для выполнения событий Scientist. Обернув в него код эксперимента, можно увидеть, как события появляются в AppSIgnal.Теперь вернемся к LabTech — пример ниже просто принимает число для разложения.Приступить к работе легко, если у вас есть доступ к консоли. Сначала надо добавить гем LabTech в Gemfile и запустить bundle install.gem 'lab_tech'Результаты и конфигурация эксперимента хранятся в БД, поэтому требуется миграция.rails lab_tech:install:migrations db:migrateУровень абстракции тот же, за исключением того, что используется модуль LabTech. Полный код доступен на GitHub.def largest_prime_factor(number)    LabTech.science "prime-factors" do |experiment|      ...    endendНа данный момент эксперимент отключен, поэтому воспользуемся консолью, чтобы включить его во всех случаях или настроить процент исполнения.bin/rails consoleLabTech.enable "prime-factors"LabTech.enable "prime-factors", percent: 5Теперь можно запустить тесты, и эксперимент будет выполнен. Для текстового представления результатов есть следующие команды в консоли Rails.LabTech.summarize_results "prime-factors"LabTech.summarize_errors "prime-factors"После нескольких успешных прогонов и одной синтетической ошибки получим пример того, как выглядит сводка результатов. Существует обзор успехов и ошибок, а также диаграмма ASCII, показывающая различия в производительности.-Earliest results: 2022-04-27T02:42:45ZLatest result: 2022-05-01T17:27:39Z (5 days) 3 of 4 (75.00%) correct1 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----Для лёгкого и удобного анализа результатов есть гем Blazer. Он прост в установке и позволяет выполнять SQL-запросы. Запрос здесь показывает, что реализация-кандидат значительно быстрее оригинала.В примере нашего сервиса, возвращающего наибольший простой делитель, ускорение в улучшенной реализации происходит за счет эвристики, исключающей некоторые возможные факторы, которые необходимо учитывать. Поскольку мы рассматриваем большие числа и находим простой множитель, мы можем прекратить поиск после того, как доберемся до целевого числа, делённого на этот множитель. Новый код добавляет только одну инструкцию для достижения этой цели.Мы также можем увидеть сокращение времени выполнения используя Blazer.Варианты использования и ограничения ScientistОптимальное использование Scientist включает задачи поиска, вычисления и код, который не имеет побочных эффектов. Код, включающий транзакционные обновления или внешние интеграции, такие как электронная почта, не совсем вписывается в модель, поскольку изменяемое действие выполняется дважды (как в старой, так и в новой реализации).Это не тривиальное ограничение. Оно блокирует несколько вариантов использования. Однако есть обходные пути, если эксперимент очень важен. Стоит подумать имеют ли значение побочные эффекты или дублирование или они не являются проблемой. Например, в некоторых случаях может не иметь значения, будут ли отправлены два электронных письма во время оценки. Другой вариант — сделать так, чтобы новый код определял результат, но не сохранял его. Это не позволит сравнить производительность, но позволит проверить точность.Другие ограничения проистекают из того, что Scientist фокусируется на возвращаемых значениях. В некоторых случаях действительные результаты могут отличаться с течением времени, независимо от того, включают ли они просто временные метки в ответ или изменяются сторонние факторы. Во многих случаях мы можем написать пользовательскую логику сравнения в эксперименте, чтобы проверить точность, выходящую за рамки обычного сравнения строк.Наконец, ограничение LabTech заключается в том, что он не был портирован на Rails 7 на момент написания.Лучшие практики для эффективных Scientist-экспериментов в RailsПри проведении экспериментов стоит учитывать следующие моменты:  В Rails-проектах Scientist может быть настроен либо в инициализаторе, либо в оболочке, такой как гем Rails LabTech. Большинство приложений Rails уже имеют базу данных, поэтому LabTech использует ActiveRecord для хранения результатов.  Чтобы не замедлять разработку и тестирование, включайте эксперимент только в промежуточной (staging) и продуктовой средах.  Чтобы свести к минимуму любое потенциальное влияние на продуктовую среду, запускайте эксперимент только для некоторого процента запросов. LabTech поддерживает это из коробки как необязательный параметр при включении эксперимента (изначально он отключен по умолчанию). Используя чистый Scientist, эту логику легко реализовать через enabled? метод.  Некоторая логика требует больших ресурсов, поэтому хорошей отправной точкой может быть низкая частота дискретизации. По мере того, как вы обретете уверенность в результатах, увеличивайте процент оцениваемых запросов.  Вы можете добавить атрибуты контекста, чтобы получить максимальную отдачу от результатов. В качестве контекста эксперимента может быть задан хэш с параметрами, который затем становится доступным в опубликованных результатах, например:experiment.context :user =&gt; userПодводя итоги: наблюдайте и контролируйте свое приложение Ruby с помощью ScientistВ этом посте мы рассмотрели, как использовать гем Scientist для изменения, переноса и рефакторинга кода Ruby в продуктовой среде.Мы рассмотрели место Scientist в паттерне Branch by Abstraction, и погрузились в рефакторинг. Затем увидели, как LabTech помогает со сбором результатов и конфигурацией Scientist.Мы коснулись некоторых ограничений Scientist, прежде чем, наконец, изложить несколько лучших практик.Вам необходимо наблюдать и контролировать то, что происходит в системе. Интегрируйте Scientist в процесс разработки, чтобы с большей уверенностью вносить критические изменения в код Ruby.Happy coding!Перевод статьи “Using Scientist to Refactor Critical Ruby on Rails Code”Darren Broemmer от 18 мая 2022 года.</yandex:full-text>

        
        <category>rails</category>
        
        <category>ruby</category>
        
      </item>
    
      <item>
        <guid isPermaLink="true">https://kinnalru.github.io/2022/08/11/chistka_build_agentov_gitlab.html</guid>
        <link>https://kinnalru.github.io/2022/08/11/chistka_build_agentov_gitlab.html</link>
        <pdalink>https://kinnalru.github.io/2022/08/11/chistka_build_agentov_gitlab.html</pdalink>
        <dc:creator>kinnalru</dc:creator>
        <author>kinnalru@gmail.com (Самойленко Юрий)</author>
        <title>Чистка build-агентов Gitlab</title>
        <pubDate>Thu, 11 Aug 2022 12:52:28 +0300</pubDate>
        <yandex:genre>article</yandex:genre>
        
        <media:content medium="image" url="https://img4.teletype.in/files/37/38/37383f40-cc8b-4523-b616-a1705311242e.jpeg"/>
        
        <description><![CDATA[ <p><img src="https://img4.teletype.in/files/37/38/37383f40-cc8b-4523-b616-a1705311242e.jpeg" alt="" /></p><p><em>Оригинал: <a href="https://blog.rnds.pro/030-gitlab-janitor?utm_source=devto&amp;utm_medium=feed_rss&amp;utm_campaign=rnds">https://blog.rnds.pro/030-gitlab-janitor</a></em></p><p>Спешим поделиться с вами нашим инструментом для поддержания чистоты и порядка на наших (RNDSOFT) сборочных серверах <a href="https://github.com/RND-SOFT/gitlab-janitor">gitlab-janitor</a>. О том, как мы к нему пришли, и каков первый опыт - далее по тексту.</p><p>С каждым годом инфраструктура RNDSOFT, обеспечивающая процессы CI/CD, растёт. Появляются новые проекты, усложняются процессы (pipelines) сборки и тестирования, растет количество сборщиков (build agents), и всё это приносит дополнительные накладные расходы. Сегодня мы рассмотрим конкретную проблему - чистку/освобождение ресурсов на самих сборочных серверах. Мы используем Gitlab и несколько <a href="https://docs.gitlab.com/runner/">Gitlab runners</a> с докером (<a href="https://docs.gitlab.com/runner/executors/docker.html">docker executor</a>) под капотом. Вот с проблем и начнём.</p><h2 id="проблемы">Проблемы</h2><p>Вся терминология будет опираться на <a href="https://docs.gitlab.com/">Gitlab</a>, но всё это применимо и к другим решениям, опирающимся на docker.</p><p>Типовой процесс сборки и доставки состоит из 4х больших этапов (stages), по несколько задач (jobs) в каждом:</p><ul>  <li>сборка docker-образа (обычно одного на проект, но бывает и больше).</li>  <li>тестирование. Тут мы используем подход, когда тестируем не сам код, а весь контейнер. Конечно, есть юнит-тесты, интеграционные, графические и прочие, но запускаем мы их непосредственно внутри боевого контейнера. А что? У нас ruby - можем себе позволить 💎 :)</li>  <li>тэгирование (image promoting). Этап в зависимости от результатов тестов и QA перетегируется во что-то вроде <code class="language-plaintext highlighter-rouge">stable</code> или <code class="language-plaintext highlighter-rouge">release</code>, или как-то еще, в зависимости от конкретного проекта, команды и принятых процессов.</li>  <li>доставка (deploy). Тут всё как обычно - дев/тест стенды, QA-стенды, динамические стенды для <a href="https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html">MR</a> и всякое разное вроде документации.</li></ul><section style="background-color:hsl(hsl(0, 0%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">    <p id="kswC">Давно хочу написать отдельно про процесс image promotiong, про то, как мы таскаем образы между стадиями, но руки не доходят. Если кого сильно интересует - пишите в комментариях, и мне придётся найти в себе силы :)</p>  </section><p>В результате описанных выше процессов (особенно тестирования) на сборщиках остаётся много мусора, именно это является проблемой. Далее по порядку.</p><h3 id="подвисшие-контейнеры-dangling-containers">Подвисшие контейнеры (dangling containers)</h3><p>Тут речь, конечно, идёт не о повисании ПО внутри контейнера, а о самих “ненужных” контейнерах. Например, для графических тестов поднимается целый стек: Postgres, Chrome, Selenium, само приложение, контейнер, из которого запускаются тесты, Redis и бог еще знает что. Если весь процесс (pipeline) или непосредственно задача (job) будут прерваны, то никто не погасит за нас эти контейнеры, и они продолжат висеть и потреблять ресурсы. Проблема!</p><h3 id="ненужные-образы">Ненужные образы</h3><p>В процессе сборки имя образа претерпевает примерно следующие изменения: image:$SHA -&gt; image:stable -&gt; image:release. Конечно, это примерно, шагов может больше или именования другие, но сути это не меняет. При последовательных комитах <del>сратые</del> старые образы остаются лежать мёртвым грузом.</p><section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">    <p id="LCgm">В день мы генерим примерно 75GB образов. ~25GB на каждом из трех сборщиков. И это летом, когда многие в отпуске :)</p>  </section><p>Ситуация усугубляется тем, что один и тот же образ находится (с максимальной вероятностью) сразу на всех сборщиках, поскольку разные типы тестов запускаются параллельно и попадают на все сборщики и занимают x3 места. Проблема! Дважды проблема, поскольку с образами всё не так просто и очень интересно - об этом ниже.</p><h3 id="безымянные-и-кеширующие-тома-unnamed-and-cache-volumes">Безымянные и кеширующие тома (unnamed and cache volumes)</h3><p>Многие контейнеры при создании аллоцируют в докере временные анонимные тома/диски (мы же всегда про докер говорим, верно?) и оставляют их после себя. Также сам Gitlab, если вы не используете распределённые кеш (shared cache) <a href="https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnerscaches3-section">на базе S3</a>, всё равно создаёт кеш-тома, <a href="https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersdocker-section">если ему явно не запретить это делать</a> через <code class="language-plaintext highlighter-rouge">disable_cache</code>. Проблема!</p><hr /><p>Конечно, эти проблемы не простой “мусор” - старые образы могут ускорить последующую сборку, кеш-тома также теоретически могут ускорять сборку, так что они не совсем бесполезны. Однако, проблемы ускорения и кеширования мы решаем немного иначе - с помощью определённых схем именования образов, используя buildkit, который умеет подтягивать кешированные слои прямо из реестра образов (в нашем случае harbor) и пр.</p><h2 id="что-же-делать">Что же делать?</h2><p>Проблемы появляются не сразу. Когда-то у нас был один сборщик, там вообще не было проблем с перетягиванием образов между этапами, а образы чистились через <code class="language-plaintext highlighter-rouge">docker rmi</code> на последнем шаге процесса. Когда сборщиков стало больше, и на них докинули ресурсы, пришлось немного усложнить схему - сначала были bash-скрипты, которые по расписанию удаляли образы по шаблону имени. Затем периодически стали использовать <code class="language-plaintext highlighter-rouge">docker system prune</code>. Но эти решения очень негибкие, плохо поддерживаются и масштабируются и обладают фундаментальной проблемой - частыми кеш-промахами (cache miss), что периодически сильно тормозило процессы (pipelines). И вот однажды терпеть это стало невозможно 😜 :)))</p><p>Нам нужно хорошее решение, желательно с баристой и массажисткой! Встречаем - <a href="https://github.com/RND-SOFT/gitlab-janitor">gitlab-janitor</a>!</p><h2 id="gitlab-janitor">Gitlab-janitor</h2><p>Основными задачами были:</p><ul>  <li>консолидация всех чисток сборщика в одном месте;</li>  <li>уменьшение кеш-промахов, насколько это возможно;</li>  <li>простота работы.</li></ul><p>Для чистки места на сборщиках <a href="https://gitlab.com/gitlab-org/gitlab-runner-docker-cleanup">уже есть проект gitlab-runner-docker-cleanup</a>, он и явился идейным вдохновителем нашей утилиты, но, к сожалению, не выполнял всех необходимых нам функций.</p><p>Написав своё решение, мы решили опубликовать его в открытом виде - и <a href="https://github.com/RND-SOFT/gitlab-janitor">зеркало на github</a>, и <a href="https://hub.docker.com/r/rnds/gitlab-janitor">образы на docker-hub</a>, и даже документация с примерами. Ну и не мог я удержаться от разнообразных бейджиков :)</p><p><img src="https://img3.teletype.in/files/62/47/6247a285-8416-4c43-be16-c2317598a279.png" alt="" /></p><p>Проще всего запускать его в докере на каждом сборщике (для этого мы используем <a href="https://www.nomadproject.io/">nomad</a> 🥂 ):</p><div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">--rm</span> <span class="se">\</span>  <span class="nt">-v</span> /var/run/docker.sock:/var/run/docker.sock <span class="se">\</span>  <span class="nt">-v</span> /persistent/janitor:/store <span class="se">\</span>  <span class="nt">-e</span> <span class="nv">REMOVE</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>  <span class="nt">-e</span> <span class="nv">INCLUDE</span><span class="o">=</span><span class="s2">"*integr*, *units*"</span> <span class="se">\</span>  <span class="nt">-e</span> <span class="nv">EXCLUDE</span><span class="o">=</span><span class="s2">"*gitlab*"</span> <span class="se">\</span>  <span class="nt">-e</span> <span class="nv">CONTAINER_DEADLINE</span><span class="o">=</span><span class="s2">"1h10m"</span> <span class="se">\</span>  <span class="nt">-e</span> <span class="nv">VOLUME_DEADLINE</span><span class="o">=</span><span class="s2">"4d"</span> <span class="se">\</span>  <span class="nt">-e</span> <span class="nv">IMAGE_DEADLINE</span><span class="o">=</span><span class="s2">"4d"</span> <span class="se">\</span>  <span class="nt">-e</span> <span class="nv">CACHE_SIZE</span><span class="o">=</span><span class="s2">"10G"</span> <span class="se">\</span>  <span class="nt">-e</span> <span class="nv">IMAGE_STORE</span><span class="o">=</span><span class="s2">"/store/images.txt"</span> <span class="se">\</span>  rnds/gitlab-janitor:latest</code></pre></div></div><p>Заметили <code class="language-plaintext highlighter-rouge">IMAGE_STORE</code>? С остальными параметрами всё просто - шаблоны для имён образов, томов, сроки хранения, а вот для образов всё гораздо интереснее!</p><p>Докер не сохраняет в своём локальном хранилище никакой информации о времени загрузки образа, а только обычную дату создания образа. В результате нет возможности определить, какой образ очень нужен и часто используется (хоть и создан давно), а какой свеженький, но больше никогда и никому не будет нужен.</p><p>Для решения этой проблемы в gitlab-janitor сделан простенький механизм - когда образ встречается первый раз, он сохраняется в файл с временной меткой - это считается датой появления. Когда подходит срок - образ удаляется. Однако, если gitlab-janitor увидит созданный контейнер из этого образа - то временная метка в этом файле сбрасывается. Именно для этого и используется параметр <code class="language-plaintext highlighter-rouge">IMAGE_STORE</code>. Данный файл можно монтировать с хост-машины, а можно и нет - в этом случае история образов будет теряться при пересоздании/обновлении контейнера-чистильщика:</p><div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>selenium/standalone-chrome:latest sha256:c01aea5eb0bf279df5f745e3c277e30d7d9c81f15b9d1d4e829f1075c31ed5b1 1660205645postgres:10-alpine sha256:d7023df56cb7cbe961f0c402888f7170397c98c099fb76fbe16b5442a236ad51 1660205040redis:latest sha256:3edbb69f9a493835e66a0f0138bed01075d8f4c2697baedd29111d667e1992b4 1660205645</code></pre></div></div><p>С остальными параметрами, документацией и примерами можно ознакомиться на гитхабе.</p><h2 id="результаты">Результаты</h2><p>Вот и подъехали результаты объективного контроля за работой gitlab-janitor на трех наших сборщиках:</p><p><img src="https://img2.teletype.in/files/55/ae/55aecb6b-d59b-4bdd-b6e1-b079fcfb4805.png" alt="" /></p><p>Все чистки с помощью <code class="language-plaintext highlighter-rouge">cron+bash</code>, <code class="language-plaintext highlighter-rouge">docker prune</code> и пр. выключены. Параметры, оптимально подходящие под наши процессы, мы еще подбираем (и будем подбирать), но уже видно, что процесс вошел в стабильную фазу, и наш чистильщик выполняет свою работу!</p> ]]></description>
        <content:encoded><![CDATA[ <p><img src="https://img4.teletype.in/files/37/38/37383f40-cc8b-4523-b616-a1705311242e.jpeg" alt="" /></p><p><em>Оригинал: <a href="https://blog.rnds.pro/030-gitlab-janitor?utm_source=devto&amp;utm_medium=feed_rss&amp;utm_campaign=rnds">https://blog.rnds.pro/030-gitlab-janitor</a></em></p><p>Спешим поделиться с вами нашим инструментом для поддержания чистоты и порядка на наших (RNDSOFT) сборочных серверах <a href="https://github.com/RND-SOFT/gitlab-janitor">gitlab-janitor</a>. О том, как мы к нему пришли, и каков первый опыт - далее по тексту.</p><p>С каждым годом инфраструктура RNDSOFT, обеспечивающая процессы CI/CD, растёт. Появляются новые проекты, усложняются процессы (pipelines) сборки и тестирования, растет количество сборщиков (build agents), и всё это приносит дополнительные накладные расходы. Сегодня мы рассмотрим конкретную проблему - чистку/освобождение ресурсов на самих сборочных серверах. Мы используем Gitlab и несколько <a href="https://docs.gitlab.com/runner/">Gitlab runners</a> с докером (<a href="https://docs.gitlab.com/runner/executors/docker.html">docker executor</a>) под капотом. Вот с проблем и начнём.</p><h2 id="проблемы">Проблемы</h2><p>Вся терминология будет опираться на <a href="https://docs.gitlab.com/">Gitlab</a>, но всё это применимо и к другим решениям, опирающимся на docker.</p><p>Типовой процесс сборки и доставки состоит из 4х больших этапов (stages), по несколько задач (jobs) в каждом:</p><ul>  <li>сборка docker-образа (обычно одного на проект, но бывает и больше).</li>  <li>тестирование. Тут мы используем подход, когда тестируем не сам код, а весь контейнер. Конечно, есть юнит-тесты, интеграционные, графические и прочие, но запускаем мы их непосредственно внутри боевого контейнера. А что? У нас ruby - можем себе позволить 💎 :)</li>  <li>тэгирование (image promoting). Этап в зависимости от результатов тестов и QA перетегируется во что-то вроде <code class="language-plaintext highlighter-rouge">stable</code> или <code class="language-plaintext highlighter-rouge">release</code>, или как-то еще, в зависимости от конкретного проекта, команды и принятых процессов.</li>  <li>доставка (deploy). Тут всё как обычно - дев/тест стенды, QA-стенды, динамические стенды для <a href="https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html">MR</a> и всякое разное вроде документации.</li></ul><section style="background-color:hsl(hsl(0, 0%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">    <p id="kswC">Давно хочу написать отдельно про процесс image promotiong, про то, как мы таскаем образы между стадиями, но руки не доходят. Если кого сильно интересует - пишите в комментариях, и мне придётся найти в себе силы :)</p>  </section><p>В результате описанных выше процессов (особенно тестирования) на сборщиках остаётся много мусора, именно это является проблемой. Далее по порядку.</p><h3 id="подвисшие-контейнеры-dangling-containers">Подвисшие контейнеры (dangling containers)</h3><p>Тут речь, конечно, идёт не о повисании ПО внутри контейнера, а о самих “ненужных” контейнерах. Например, для графических тестов поднимается целый стек: Postgres, Chrome, Selenium, само приложение, контейнер, из которого запускаются тесты, Redis и бог еще знает что. Если весь процесс (pipeline) или непосредственно задача (job) будут прерваны, то никто не погасит за нас эти контейнеры, и они продолжат висеть и потреблять ресурсы. Проблема!</p><h3 id="ненужные-образы">Ненужные образы</h3><p>В процессе сборки имя образа претерпевает примерно следующие изменения: image:$SHA -&gt; image:stable -&gt; image:release. Конечно, это примерно, шагов может больше или именования другие, но сути это не меняет. При последовательных комитах <del>сратые</del> старые образы остаются лежать мёртвым грузом.</p><section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">    <p id="LCgm">В день мы генерим примерно 75GB образов. ~25GB на каждом из трех сборщиков. И это летом, когда многие в отпуске :)</p>  </section><p>Ситуация усугубляется тем, что один и тот же образ находится (с максимальной вероятностью) сразу на всех сборщиках, поскольку разные типы тестов запускаются параллельно и попадают на все сборщики и занимают x3 места. Проблема! Дважды проблема, поскольку с образами всё не так просто и очень интересно - об этом ниже.</p><h3 id="безымянные-и-кеширующие-тома-unnamed-and-cache-volumes">Безымянные и кеширующие тома (unnamed and cache volumes)</h3><p>Многие контейнеры при создании аллоцируют в докере временные анонимные тома/диски (мы же всегда про докер говорим, верно?) и оставляют их после себя. Также сам Gitlab, если вы не используете распределённые кеш (shared cache) <a href="https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnerscaches3-section">на базе S3</a>, всё равно создаёт кеш-тома, <a href="https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersdocker-section">если ему явно не запретить это делать</a> через <code class="language-plaintext highlighter-rouge">disable_cache</code>. Проблема!</p><hr /><p>Конечно, эти проблемы не простой “мусор” - старые образы могут ускорить последующую сборку, кеш-тома также теоретически могут ускорять сборку, так что они не совсем бесполезны. Однако, проблемы ускорения и кеширования мы решаем немного иначе - с помощью определённых схем именования образов, используя buildkit, который умеет подтягивать кешированные слои прямо из реестра образов (в нашем случае harbor) и пр.</p><h2 id="что-же-делать">Что же делать?</h2><p>Проблемы появляются не сразу. Когда-то у нас был один сборщик, там вообще не было проблем с перетягиванием образов между этапами, а образы чистились через <code class="language-plaintext highlighter-rouge">docker rmi</code> на последнем шаге процесса. Когда сборщиков стало больше, и на них докинули ресурсы, пришлось немного усложнить схему - сначала были bash-скрипты, которые по расписанию удаляли образы по шаблону имени. Затем периодически стали использовать <code class="language-plaintext highlighter-rouge">docker system prune</code>. Но эти решения очень негибкие, плохо поддерживаются и масштабируются и обладают фундаментальной проблемой - частыми кеш-промахами (cache miss), что периодически сильно тормозило процессы (pipelines). И вот однажды терпеть это стало невозможно 😜 :)))</p><p>Нам нужно хорошее решение, желательно с баристой и массажисткой! Встречаем - <a href="https://github.com/RND-SOFT/gitlab-janitor">gitlab-janitor</a>!</p><h2 id="gitlab-janitor">Gitlab-janitor</h2><p>Основными задачами были:</p><ul>  <li>консолидация всех чисток сборщика в одном месте;</li>  <li>уменьшение кеш-промахов, насколько это возможно;</li>  <li>простота работы.</li></ul><p>Для чистки места на сборщиках <a href="https://gitlab.com/gitlab-org/gitlab-runner-docker-cleanup">уже есть проект gitlab-runner-docker-cleanup</a>, он и явился идейным вдохновителем нашей утилиты, но, к сожалению, не выполнял всех необходимых нам функций.</p><p>Написав своё решение, мы решили опубликовать его в открытом виде - и <a href="https://github.com/RND-SOFT/gitlab-janitor">зеркало на github</a>, и <a href="https://hub.docker.com/r/rnds/gitlab-janitor">образы на docker-hub</a>, и даже документация с примерами. Ну и не мог я удержаться от разнообразных бейджиков :)</p><p><img src="https://img3.teletype.in/files/62/47/6247a285-8416-4c43-be16-c2317598a279.png" alt="" /></p><p>Проще всего запускать его в докере на каждом сборщике (для этого мы используем <a href="https://www.nomadproject.io/">nomad</a> 🥂 ):</p><div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">--rm</span> <span class="se">\</span>  <span class="nt">-v</span> /var/run/docker.sock:/var/run/docker.sock <span class="se">\</span>  <span class="nt">-v</span> /persistent/janitor:/store <span class="se">\</span>  <span class="nt">-e</span> <span class="nv">REMOVE</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>  <span class="nt">-e</span> <span class="nv">INCLUDE</span><span class="o">=</span><span class="s2">"*integr*, *units*"</span> <span class="se">\</span>  <span class="nt">-e</span> <span class="nv">EXCLUDE</span><span class="o">=</span><span class="s2">"*gitlab*"</span> <span class="se">\</span>  <span class="nt">-e</span> <span class="nv">CONTAINER_DEADLINE</span><span class="o">=</span><span class="s2">"1h10m"</span> <span class="se">\</span>  <span class="nt">-e</span> <span class="nv">VOLUME_DEADLINE</span><span class="o">=</span><span class="s2">"4d"</span> <span class="se">\</span>  <span class="nt">-e</span> <span class="nv">IMAGE_DEADLINE</span><span class="o">=</span><span class="s2">"4d"</span> <span class="se">\</span>  <span class="nt">-e</span> <span class="nv">CACHE_SIZE</span><span class="o">=</span><span class="s2">"10G"</span> <span class="se">\</span>  <span class="nt">-e</span> <span class="nv">IMAGE_STORE</span><span class="o">=</span><span class="s2">"/store/images.txt"</span> <span class="se">\</span>  rnds/gitlab-janitor:latest</code></pre></div></div><p>Заметили <code class="language-plaintext highlighter-rouge">IMAGE_STORE</code>? С остальными параметрами всё просто - шаблоны для имён образов, томов, сроки хранения, а вот для образов всё гораздо интереснее!</p><p>Докер не сохраняет в своём локальном хранилище никакой информации о времени загрузки образа, а только обычную дату создания образа. В результате нет возможности определить, какой образ очень нужен и часто используется (хоть и создан давно), а какой свеженький, но больше никогда и никому не будет нужен.</p><p>Для решения этой проблемы в gitlab-janitor сделан простенький механизм - когда образ встречается первый раз, он сохраняется в файл с временной меткой - это считается датой появления. Когда подходит срок - образ удаляется. Однако, если gitlab-janitor увидит созданный контейнер из этого образа - то временная метка в этом файле сбрасывается. Именно для этого и используется параметр <code class="language-plaintext highlighter-rouge">IMAGE_STORE</code>. Данный файл можно монтировать с хост-машины, а можно и нет - в этом случае история образов будет теряться при пересоздании/обновлении контейнера-чистильщика:</p><div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>selenium/standalone-chrome:latest sha256:c01aea5eb0bf279df5f745e3c277e30d7d9c81f15b9d1d4e829f1075c31ed5b1 1660205645postgres:10-alpine sha256:d7023df56cb7cbe961f0c402888f7170397c98c099fb76fbe16b5442a236ad51 1660205040redis:latest sha256:3edbb69f9a493835e66a0f0138bed01075d8f4c2697baedd29111d667e1992b4 1660205645</code></pre></div></div><p>С остальными параметрами, документацией и примерами можно ознакомиться на гитхабе.</p><h2 id="результаты">Результаты</h2><p>Вот и подъехали результаты объективного контроля за работой gitlab-janitor на трех наших сборщиках:</p><p><img src="https://img2.teletype.in/files/55/ae/55aecb6b-d59b-4bdd-b6e1-b079fcfb4805.png" alt="" /></p><p>Все чистки с помощью <code class="language-plaintext highlighter-rouge">cron+bash</code>, <code class="language-plaintext highlighter-rouge">docker prune</code> и пр. выключены. Параметры, оптимально подходящие под наши процессы, мы еще подбираем (и будем подбирать), но уже видно, что процесс вошел в стабильную фазу, и наш чистильщик выполняет свою работу!</p> ]]></content:encoded>
        <yandex:full-text>Оригинал: https://blog.rnds.pro/030-gitlab-janitorСпешим поделиться с вами нашим инструментом для поддержания чистоты и порядка на наших (RNDSOFT) сборочных серверах gitlab-janitor. О том, как мы к нему пришли, и каков первый опыт - далее по тексту.С каждым годом инфраструктура RNDSOFT, обеспечивающая процессы CI/CD, растёт. Появляются новые проекты, усложняются процессы (pipelines) сборки и тестирования, растет количество сборщиков (build agents), и всё это приносит дополнительные накладные расходы. Сегодня мы рассмотрим конкретную проблему - чистку/освобождение ресурсов на самих сборочных серверах. Мы используем Gitlab и несколько Gitlab runners с докером (docker executor) под капотом. Вот с проблем и начнём.ПроблемыВся терминология будет опираться на Gitlab, но всё это применимо и к другим решениям, опирающимся на docker.Типовой процесс сборки и доставки состоит из 4х больших этапов (stages), по несколько задач (jobs) в каждом:  сборка docker-образа (обычно одного на проект, но бывает и больше).  тестирование. Тут мы используем подход, когда тестируем не сам код, а весь контейнер. Конечно, есть юнит-тесты, интеграционные, графические и прочие, но запускаем мы их непосредственно внутри боевого контейнера. А что? У нас ruby - можем себе позволить 💎 :)  тэгирование (image promoting). Этап в зависимости от результатов тестов и QA перетегируется во что-то вроде stable или release, или как-то еще, в зависимости от конкретного проекта, команды и принятых процессов.  доставка (deploy). Тут всё как обычно - дев/тест стенды, QA-стенды, динамические стенды для MR и всякое разное вроде документации.    Давно хочу написать отдельно про процесс image promotiong, про то, как мы таскаем образы между стадиями, но руки не доходят. Если кого сильно интересует - пишите в комментариях, и мне придётся найти в себе силы :)  В результате описанных выше процессов (особенно тестирования) на сборщиках остаётся много мусора, именно это является проблемой. Далее по порядку.Подвисшие контейнеры (dangling containers)Тут речь, конечно, идёт не о повисании ПО внутри контейнера, а о самих “ненужных” контейнерах. Например, для графических тестов поднимается целый стек: Postgres, Chrome, Selenium, само приложение, контейнер, из которого запускаются тесты, Redis и бог еще знает что. Если весь процесс (pipeline) или непосредственно задача (job) будут прерваны, то никто не погасит за нас эти контейнеры, и они продолжат висеть и потреблять ресурсы. Проблема!Ненужные образыВ процессе сборки имя образа претерпевает примерно следующие изменения: image:$SHA -&gt; image:stable -&gt; image:release. Конечно, это примерно, шагов может больше или именования другие, но сути это не меняет. При последовательных комитах сратые старые образы остаются лежать мёртвым грузом.    В день мы генерим примерно 75GB образов. ~25GB на каждом из трех сборщиков. И это летом, когда многие в отпуске :)  Ситуация усугубляется тем, что один и тот же образ находится (с максимальной вероятностью) сразу на всех сборщиках, поскольку разные типы тестов запускаются параллельно и попадают на все сборщики и занимают x3 места. Проблема! Дважды проблема, поскольку с образами всё не так просто и очень интересно - об этом ниже.Безымянные и кеширующие тома (unnamed and cache volumes)Многие контейнеры при создании аллоцируют в докере временные анонимные тома/диски (мы же всегда про докер говорим, верно?) и оставляют их после себя. Также сам Gitlab, если вы не используете распределённые кеш (shared cache) на базе S3, всё равно создаёт кеш-тома, если ему явно не запретить это делать через disable_cache. Проблема!Конечно, эти проблемы не простой “мусор” - старые образы могут ускорить последующую сборку, кеш-тома также теоретически могут ускорять сборку, так что они не совсем бесполезны. Однако, проблемы ускорения и кеширования мы решаем немного иначе - с помощью определённых схем именования образов, используя buildkit, который умеет подтягивать кешированные слои прямо из реестра образов (в нашем случае harbor) и пр.Что же делать?Проблемы появляются не сразу. Когда-то у нас был один сборщик, там вообще не было проблем с перетягиванием образов между этапами, а образы чистились через docker rmi на последнем шаге процесса. Когда сборщиков стало больше, и на них докинули ресурсы, пришлось немного усложнить схему - сначала были bash-скрипты, которые по расписанию удаляли образы по шаблону имени. Затем периодически стали использовать docker system prune. Но эти решения очень негибкие, плохо поддерживаются и масштабируются и обладают фундаментальной проблемой - частыми кеш-промахами (cache miss), что периодически сильно тормозило процессы (pipelines). И вот однажды терпеть это стало невозможно 😜 :)))Нам нужно хорошее решение, желательно с баристой и массажисткой! Встречаем - gitlab-janitor!Gitlab-janitorОсновными задачами были:  консолидация всех чисток сборщика в одном месте;  уменьшение кеш-промахов, насколько это возможно;  простота работы.Для чистки места на сборщиках уже есть проект gitlab-runner-docker-cleanup, он и явился идейным вдохновителем нашей утилиты, но, к сожалению, не выполнял всех необходимых нам функций.Написав своё решение, мы решили опубликовать его в открытом виде - и зеркало на github, и образы на docker-hub, и даже документация с примерами. Ну и не мог я удержаться от разнообразных бейджиков :)Проще всего запускать его в докере на каждом сборщике (для этого мы используем nomad 🥂 ):docker run --rm \  -v /var/run/docker.sock:/var/run/docker.sock \  -v /persistent/janitor:/store \  -e REMOVE=true \  -e INCLUDE="*integr*, *units*" \  -e EXCLUDE="*gitlab*" \  -e CONTAINER_DEADLINE="1h10m" \  -e VOLUME_DEADLINE="4d" \  -e IMAGE_DEADLINE="4d" \  -e CACHE_SIZE="10G" \  -e IMAGE_STORE="/store/images.txt" \  rnds/gitlab-janitor:latestЗаметили IMAGE_STORE? С остальными параметрами всё просто - шаблоны для имён образов, томов, сроки хранения, а вот для образов всё гораздо интереснее!Докер не сохраняет в своём локальном хранилище никакой информации о времени загрузки образа, а только обычную дату создания образа. В результате нет возможности определить, какой образ очень нужен и часто используется (хоть и создан давно), а какой свеженький, но больше никогда и никому не будет нужен.Для решения этой проблемы в gitlab-janitor сделан простенький механизм - когда образ встречается первый раз, он сохраняется в файл с временной меткой - это считается датой появления. Когда подходит срок - образ удаляется. Однако, если gitlab-janitor увидит созданный контейнер из этого образа - то временная метка в этом файле сбрасывается. Именно для этого и используется параметр IMAGE_STORE. Данный файл можно монтировать с хост-машины, а можно и нет - в этом случае история образов будет теряться при пересоздании/обновлении контейнера-чистильщика:selenium/standalone-chrome:latest sha256:c01aea5eb0bf279df5f745e3c277e30d7d9c81f15b9d1d4e829f1075c31ed5b1 1660205645postgres:10-alpine sha256:d7023df56cb7cbe961f0c402888f7170397c98c099fb76fbe16b5442a236ad51 1660205040redis:latest sha256:3edbb69f9a493835e66a0f0138bed01075d8f4c2697baedd29111d667e1992b4 1660205645С остальными параметрами, документацией и примерами можно ознакомиться на гитхабе.РезультатыВот и подъехали результаты объективного контроля за работой gitlab-janitor на трех наших сборщиках:Все чистки с помощью cron+bash, docker prune и пр. выключены. Параметры, оптимально подходящие под наши процессы, мы еще подбираем (и будем подбирать), но уже видно, что процесс вошел в стабильную фазу, и наш чистильщик выполняет свою работу!</yandex:full-text>

        
        <category>infra</category>
        
        <category>gitlab</category>
        
        <category>cleaner</category>
        
        <category>devops</category>
        
      </item>
    
  </channel>
</rss>