<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: It Caat</title>
    <description>The latest articles on DEV Community by It Caat (@itcaat).</description>
    <link>https://dev.to/itcaat</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2738014%2Fcba38a5b-02b3-459d-b290-b81365228c56.jpg</url>
      <title>DEV Community: It Caat</title>
      <link>https://dev.to/itcaat</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/itcaat"/>
    <language>en</language>
    <item>
      <title>How to use dynamic matrix in Github Actions in practice</title>
      <dc:creator>It Caat</dc:creator>
      <pubDate>Wed, 26 Nov 2025 15:57:25 +0000</pubDate>
      <link>https://dev.to/itcaat/krasota-dinamichieskikh-matrits-github-actions-2ka5</link>
      <guid>https://dev.to/itcaat/krasota-dinamichieskikh-matrits-github-actions-2ka5</guid>
      <description>&lt;p&gt;Сегодня мы с вами на практике разберем что такое динамические матрицы в Github Actions и как с их помощью экономить время и ресурсы.&lt;/p&gt;

&lt;p&gt;Я подготовил монорепозиторий с несколькими микросервисами &lt;a href="https://github.com/itcaat/url-shortener-demo" rel="noopener noreferrer"&gt;url-shortener-demo&lt;/a&gt; с очень коротким флоу: &lt;code&gt;feature_branch(через PR) →  main&lt;/code&gt;. Как понятно из названия это проект позволяющий генерировать короткие ссылки.&lt;/p&gt;

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

&lt;p&gt;А для упрощения локального запуска подготовлен &lt;code&gt;docker-compose.yml&lt;/code&gt;, состоящий из сервисов:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;api-gateway (Go) - API Gateway, единая точка входа&lt;/li&gt;
&lt;li&gt;shortener-service (Go + Redis) - Создание коротких URL&lt;/li&gt;
&lt;li&gt;redirect-service (Go + Redis + Kafka) - Перенаправление + события перехода для аналитики&lt;/li&gt;
&lt;li&gt;analytics-service (Go + MongoDB + Kafka) - Аналитика &lt;/li&gt;
&lt;li&gt;frontend (HTML + Nginx) - Веб-интерфейс&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Теперь про магию - на самом деле вы, наверняка, видели множество примеров со статичными матрицами сборки (например, когда сборка приложения делается на нескольких OS). Но что если пойти дальше и самому сгенерировать матрицу в зависимости от того что поменялось?&lt;/p&gt;

&lt;p&gt;К счастью Github Actions позволяет нам это сделать. При создании PR мы автоматически можем определить какой сервис поменялся, собрать его и выложить. А в случае, если поменялось что-то в pkg - собрать все сервисы.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/build-pr.yml&lt;/span&gt;

&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build Pull Request&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;REGISTRY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io&lt;/span&gt;
  &lt;span class="na"&gt;IMAGE_PREFIX&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.repository }}&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;changed-services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Detect changed services&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.set-matrix.outputs.matrix }}&lt;/span&gt;
      &lt;span class="na"&gt;any_changed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.changed-files.outputs.any_changed }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get changed services&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;changed-files&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tj-actions/changed-files@v45&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;dir_names&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;dir_names_max_depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
          &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;**/*&lt;/span&gt;
          &lt;span class="na"&gt;files_ignore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;**/*.md&lt;/span&gt;
            &lt;span class="s"&gt;.github/**&lt;/span&gt;
            &lt;span class="s"&gt;scripts/**&lt;/span&gt;
            &lt;span class="s"&gt;*.md&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;List all changed files&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "Changed files: ${{ steps.changed-files.outputs.all_changed_files }}"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set matrix&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;set-matrix&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# Находим все директории с Dockerfile&lt;/span&gt;
          &lt;span class="s"&gt;ALL_SERVICES=$(find . -maxdepth 2 -name "Dockerfile" -type f | sed 's|^\./||' | sed 's|/Dockerfile$||' | jq -R -s 'split("\n") | map(select(length &amp;gt; 0))' | jq -c .)&lt;/span&gt;
          &lt;span class="s"&gt;echo "All services with Dockerfile: $ALL_SERVICES"&lt;/span&gt;

          &lt;span class="s"&gt;# Получаем измененные файлы и убираем экранирование&lt;/span&gt;
          &lt;span class="s"&gt;CHANGED_DIRS_RAW='${{ steps.changed-files.outputs.all_changed_files }}'&lt;/span&gt;
          &lt;span class="s"&gt;CHANGED_DIRS=$(echo "$CHANGED_DIRS_RAW" | sed 's/\\"/"/g')&lt;/span&gt;
          &lt;span class="s"&gt;echo "Changed directories: $CHANGED_DIRS"&lt;/span&gt;

          &lt;span class="s"&gt;# Если изменился pkg/, пересобираем все Go сервисы (с go.mod)&lt;/span&gt;
          &lt;span class="s"&gt;if echo "$CHANGED_DIRS" | jq -e 'index("pkg")' &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; then&lt;/span&gt;
            &lt;span class="s"&gt;SERVICES=$(find . -maxdepth 2 -name "go.mod" -type f | sed 's|^\./||' | sed 's|/go.mod$||' | jq -R -s 'split("\n") | map(select(length &amp;gt; 0))' | jq -c .)&lt;/span&gt;
            &lt;span class="s"&gt;echo "pkg/ changed, rebuilding all Go services: $SERVICES"&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;# Фильтруем: оставляем только измененные директории с Dockerfile&lt;/span&gt;
            &lt;span class="s"&gt;SERVICES=$(jq -nc --argjson all "$ALL_SERVICES" --argjson changed "$CHANGED_DIRS" \&lt;/span&gt;
              &lt;span class="s"&gt;'$changed | map(select(. as $dir | $all | index($dir)))' | jq -c .)&lt;/span&gt;
            &lt;span class="s"&gt;echo "Changed services: $SERVICES"&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

          &lt;span class="s"&gt;# Если нет сервисов для сборки, создаем пустой массив&lt;/span&gt;
          &lt;span class="s"&gt;if [ "$SERVICES" = "[]" ] || [ -z "$SERVICES" ]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "No services to build"&lt;/span&gt;
            &lt;span class="s"&gt;SERVICES="[]"&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

          &lt;span class="s"&gt;echo "matrix={\"service\":$SERVICES}" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;

  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build ${{ matrix.service }}&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;changed-services&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.changed-services.outputs.any_changed == 'true' }}&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ fromJSON(needs.changed-services.outputs.matrix) }}&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Docker Buildx&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/setup-buildx-action@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Log in to GitHub Container Registry&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.REGISTRY }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.actor }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Extract metadata&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;meta&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/metadata-action@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.service }}&lt;/span&gt;
          &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;type=ref,event=pr&lt;/span&gt;
            &lt;span class="s"&gt;type=sha,prefix=pr-${{ github.event.pull_request.number }}-&lt;/span&gt;
            &lt;span class="s"&gt;type=raw,value=pr-${{ github.event.pull_request.number }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and push&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
          &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.service }}/Dockerfile&lt;/span&gt;
          &lt;span class="na"&gt;platforms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux/amd64,linux/arm64&lt;/span&gt;
          &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.meta.outputs.tags }}&lt;/span&gt;
          &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.meta.outputs.labels }}&lt;/span&gt;
          &lt;span class="na"&gt;cache-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha,scope=${{ matrix.service }}&lt;/span&gt;
          &lt;span class="na"&gt;cache-to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha,mode=max,scope=${{ matrix.service }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Add PR comment&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mshick/add-pr-comment@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;✅ **${{ matrix.service }}** successfully built!&lt;/span&gt;

            &lt;span class="s"&gt;**Images:**&lt;/span&gt;
            &lt;span class="s"&gt;```&lt;/span&gt;
&lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="nv"&gt;endraw %&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;

            &lt;span class="s"&gt;${{ steps.meta.outputs.tags }}&lt;/span&gt;
&lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="nv"&gt;raw %&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;

            &lt;span class="err"&gt;```&lt;/span&gt;

            &lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;*Pull&lt;/span&gt; &lt;span class="s"&gt;command:**&lt;/span&gt;  
            &lt;span class="s"&gt;docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:pr-${{ github.event.pull_request.number }}&lt;/span&gt;
          &lt;span class="s"&gt;message-id&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build-${{ matrix.service }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Что мы получили по итогу создав такой workflow, который треггерится при создание/обновление Pull Request в main:&lt;/p&gt;

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

&lt;p&gt;Собирает только измененные сервисы - использует динамическую матрицу. Если изменился &lt;code&gt;pkg/&lt;/code&gt; - пересобираются все Go сервисы. Никаких лишних сборок.&lt;/p&gt;

&lt;p&gt;Пушит образы с тегами PR - например, &lt;code&gt;pr-123&lt;/code&gt;, &lt;code&gt;pr-123-sha123abc&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Добавляет комментарий в PR - с информацией о собранных образах&lt;/p&gt;

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

&lt;p&gt;А в качестве тренировке можете форкнуть реп &lt;a href="https://github.com/itcaat/url-shortener-demo" rel="noopener noreferrer"&gt;https://github.com/itcaat/url-shortener-demo&lt;/a&gt; (все примеры workflow вы найдете там же) и сделать так, чтобы собирались не все сервисы при изменении в pkg, а только те что реально зависят от измененного пакета.&lt;/p&gt;

&lt;p&gt;Другие авторские статьи по теме DevOps, SRE и администрирования вы можете найти в моем Telegram-канале &lt;a href="https://t.me/devopsbrain" rel="noopener noreferrer"&gt;DevOps Brain&lt;/a&gt; ↩&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>github</category>
    </item>
    <item>
      <title>Тулзы для работы с сотнями серверов</title>
      <dc:creator>It Caat</dc:creator>
      <pubDate>Wed, 26 Nov 2025 15:50:11 +0000</pubDate>
      <link>https://dev.to/itcaat/siekrietnoie-oruzhiie-dlia-raboty-s-sotniami-siervierov-e4b</link>
      <guid>https://dev.to/itcaat/siekrietnoie-oruzhiie-dlia-raboty-s-sotniami-siervierov-e4b</guid>
      <description>&lt;p&gt;Сегодня хочу показать вам инструменты, которые входят в мой личный топ полезных утилит для работы с множеством серверов. Их главная суперсила - параллельное выполнение задач. &lt;/p&gt;

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

&lt;p&gt;pdsh -  инструмент, который позволяет выполнять команды на множестве хостов параллельно.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Проверим uptime на хостах начиная с 1 по 5, исключив 3&lt;/span&gt;
pdsh &lt;span class="nt"&gt;-w&lt;/span&gt; node[1-5] &lt;span class="nt"&gt;-x&lt;/span&gt; node3 &lt;span class="nb"&gt;uptime&lt;/span&gt;

&lt;span class="c"&gt;# Перезагрузим все хосты, кроме node3 и node7&lt;/span&gt;
pdsh &lt;span class="nt"&gt;-w&lt;/span&gt; node[1-10] &lt;span class="nt"&gt;-x&lt;/span&gt; node3,node7 &lt;span class="s1"&gt;'sudo reboot'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Если вы достаточно организованы, чтобы вести список хостов - можно даже использовать файл с заранее определенными хостами&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# File: my_hosts.txt&lt;/span&gt;
&lt;span class="c"&gt;# node1&lt;/span&gt;
&lt;span class="c"&gt;# node2&lt;/span&gt;
&lt;span class="c"&gt;# db[01-05]&lt;/span&gt;

pdsh &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"^my_hosts.txt"&lt;/span&gt; &lt;span class="s1"&gt;'uptime'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Вы скажете: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;так тебе же ничего не мешает использовать для этих целей Ansible!&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'node1,node2,node3'&lt;/span&gt; all &lt;span class="nt"&gt;-m&lt;/span&gt; shell &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s1"&gt;'uptime'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;&lt;strong&gt;Когда одного pdsh мало - берите PSSH.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pssh&lt;/code&gt; - отличная альтернатива pdsh&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Одновременное обновление пакетов на всех хостах&lt;/span&gt;
pssh &lt;span class="nt"&gt;-h&lt;/span&gt; production_servers.txt &lt;span class="nt"&gt;-l&lt;/span&gt; admin &lt;span class="nt"&gt;-t&lt;/span&gt; 300 &lt;span class="s2"&gt;"sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pscp&lt;/code&gt; - массовое копирование на сервера&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Залить скрипт мониторинга на все хосты&lt;/span&gt;
pscp &lt;span class="nt"&gt;-h&lt;/span&gt; all_hosts.txt monitor_script.sh /usr/local/bin/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;prsync&lt;/code&gt; - параллельное копирование через rsync&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Обновить статические файлы только если они изменились&lt;/span&gt;
prsync &lt;span class="nt"&gt;-h&lt;/span&gt; cdn_nodes.txt &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s2"&gt;"-avz"&lt;/span&gt; static/ /var/www/static/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pslurp&lt;/code&gt; - собрать файлы с серверов на локальную тачку&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Собрать логи со всех серверов в отдельные папки&lt;/span&gt;
pslurp &lt;span class="nt"&gt;-h&lt;/span&gt; servers.txt &lt;span class="nt"&gt;-l&lt;/span&gt; user &lt;span class="nt"&gt;-L&lt;/span&gt; ./collected_logs /var/log/app/error.log app_error.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Эти инструменты не заменят полноценные системы управления конфигурациями вроде Ansible или chef, но для быстрых задач, срочных исправлений или массового сбора информации - они незаменимы.&lt;/p&gt;

&lt;p&gt;Другие авторские статьи по теме DevOps, SRE и администрирования вы можете найти в моем Telegram-канале &lt;a href="https://t.me/devopsbrain" rel="noopener noreferrer"&gt;DevOps Brain&lt;/a&gt; ↩&lt;/p&gt;

</description>
      <category>cli</category>
      <category>devops</category>
      <category>sre</category>
      <category>sysadmin</category>
    </item>
  </channel>
</rss>
