Stapel-builder глубоко

Уровень: intermediate ~60 мин Практика: Vikunja (AGPL-3.0)

Что нужно знать перед уроком

  • M3.02 — werf.yaml и два билдера — нужно понимать общую структуру werf.yaml и выбор билдера

Что нужно знать перед уроком

M3 урок 2 — общая структура werf.yaml, беглое знакомство со Stapel. Здесь разбираем Stapel-builder полностью: 4 стадии, git.stageDependencies, ansible, cacheVersion.

Теория

4 user-стадии в строгом порядке: beforeInstall → install → beforeSetup → setup

Stapel-builder делит сборку на четыре именованные пользовательские стадии, выполняемые строго последовательно в этом порядке. Официальная конвенция того, что класть на какую стадию:

СтадияЧто кладёмПример
beforeInstallсистемные пакеты, редко меняющиесяapt update -q && apt install -y libmysqlclient-dev g++
installзависимости приложения (по манифестам зависимостей)bundle install, npm install, go mod download
beforeSetupконфигурация системы, сборка ассетовbundle exec rails assets:precompile, webpack build
setupконфигурация приложения, финальная генерацияrake generate:configs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
image: backend
from: ubuntu:24.04
git:
  - add: /
    to: /app
    stageDependencies:
      install:
        - package-lock.json
        - Gemfile.lock
      beforeSetup:
        - app/webpack/
        - app/assets/
      setup:
        - config/templates/
shell:
  beforeInstall:
    - apt update -q
    - apt install -y libmysqlclient-dev mysql-client g++
  install:
    - bundle install
    - npm install
  beforeSetup:
    - bundle exec rails assets:precompile
  setup:
    - rake generate:configs

Порядок неслучаен: он воспроизводит логику зависимостей «от общего к специфичному». Системные пакеты (beforeInstall) меняются реже всего — обычно только при обновлении базового образа. Зависимости приложения (install) меняются при обновлении манифеста зависимостей. Конфигурация системы/ассеты (beforeSetup) — при изменении статических ресурсов. Конфигурация приложения (setup) — самая часто меняющаяся часть. Разделение на 4 стадии с независимым кэшем каждой означает: если поменялся только код в config/templates/, пересобирается только setup, всё до неё берётся из кэша.

git.stageDependencies — точный контроль инвалидации кэша

Ключевое отличие от обычного COPY в Dockerfile: stageDependencies привязывает маски файлов (не весь git.add) к конкретной стадии. Если изменился только package-lock.json — пересобирается стадия install (и всё после неё), но не beforeInstall. Если изменился файл в app/webpack/ — пересобирается beforeSetupsetup), но install берётся из кэша нетронутым. Это невозможно выразить чистым Dockerfile без явного разнесения COPY по нескольким инструкциям с ручным контролем порядка — Stapel делает это декларативно и заточено именно под git-историю проекта.

shell vs ansible — два взаимоисключающих способа описать инструкции

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# вариант shell
shell:
  install:
    - apt-get update
    - apt-get install -y curl

# вариант ansible — тот же результат, декларативнее
ansible:
  install:
    - apt:
        name: curl
        state: present
        update_cache: true

shell и ansible — взаимоисключающие top-level директивы одного image:-блока (нельзя использовать оба одновременно в одном образе). ansible-инструкции (debug, file, copy, apk, apt, yum, template и другие Ansible-модули) предпочтительны, когда команда уже владеет Ansible-экосистемой и хочет идемпотентные, декларативные шаги вместо императивных shell-команд (например, apt: state=present не переустановит пакет, если он уже там, в отличие от повторного apt-get install, который просто отработает без ошибки, но не даёт декларативной гарантии).

cacheVersion — принудительный сброс кэша стадии

1
2
3
4
5
shell:
  install:
    - bundle install
  installCacheVersion: "v2" # изменение этого значения форсирует пересборку install, даже если файлы не менялись
cacheVersion: "v1" # сброс кэша всех стадий сразу

Иногда нужно форсировать пересборку стадии без изменения фактических файлов зависимостей — например, при обновлении версии системного пакета в базовом образе, о котором Git не «знает». cacheVersion (для всех стадий) и <stage>CacheVersion (beforeInstallCacheVersion/installCacheVersion/beforeSetupCacheVersion/setupCacheVersion, для конкретной) — произвольная строка, изменение которой инвалидирует кэш соответствующей стадии независимо от изменений в файлах.

Частые ошибки и подводные камни

  • Класть установку зависимостей приложения в beforeInstall вместо install. Формально работать будет, но вы теряете смысл разделения стадий — git.stageDependencies для install не сработает так, как задумано, если реальная установка идёт на другой стадии.
  • Пытаться использовать и shell, и ansible в одном image:-блоке. Это взаимоисключающие директивы; нужно выбрать одну для конкретного образа (но разные образы одного werf.yaml могут использовать разные подходы).
  • Забыть про git.stageDependencies и получить пересборку всего образа при любом изменении любого файла. Без явных масок по стадиям Stapel по умолчанию привязывает изменения Git к максимально ранней стадии, где встречается git.add — можно потерять весь смысл тонкого кэша.

Практика в кластере

Практики в кластере в этом уроке нет — фокус на механике сборки, деплой Vikunja начнётся с M4.

Практика разработки

🧩 Практика урока: Vikunja · лицензия AGPL-3.0
  1. Перепишите backend-образ Vikunja из M3.02 через Stapel вместо Dockerfile-builder, с явным разделением стадий:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
image: backend
from: golang:1.26
git:
  - add: /api
    to: /app
    stageDependencies:
      install:
        - go.mod
        - go.sum
      setup:
        - "**/*.go"
shell:
  beforeInstall:
    - apt-get update -q && apt-get install -y gcc
  install:
    - cd /app && go mod download
  setup:
    - cd /app && go build -o /app/bin/vikunja .
  1. Соберите и замерьте время первой сборки, затем измените только один .go-файл (например, добавьте комментарий) и соберите снова:
1
2
3
werf build backend
# измените любой .go файл в api/
werf build backend    # обратите внимание — install (go mod download) должен взяться из кэша
  1. Затем измените go.mod (например, добавьте пустую строку) и соберите ещё раз — теперь должна пересобраться и стадия install, подтверждая, что stageDependencies работает именно так, как задумано.

Шпаргалка команд урока

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
shell:
  beforeInstall: [...]
  install: [...]
  beforeSetup: [...]
  setup: [...]
  cacheVersion: "<value>"
  installCacheVersion: "<value>"
git:
  - add: /path
    to: /path
    stageDependencies:
      install: ["<mask>"]

Шпаргалка команд: 📋 werf .

Вопросы для самопроверки

В каком порядке выполняются 4 user-стадии Stapel?

Строгий порядок: beforeInstall (системные пакеты) → install (зависимости приложения) → beforeSetup (конфигурация системы/ассеты) → setup (конфигурация приложения).

Источник: Werf — документация

Что делает git.stageDependencies?

stageDependencies даёт точечный контроль инвалидации кэша по маскам файлов для install/beforeSetup/setup.

Источник: Werf — документация

Можно ли использовать shell и ansible одновременно в одном image:-блоке?

shell и ansible взаимоисключающие директивы одного image:-блока; выбор делается per-образ, а не глобально для файла.

Источник: Werf — документация

Зачем нужен cacheVersion/<stage>CacheVersion?

Изменение значения cacheVersion/<stage>CacheVersion инвалидирует кэш соответствующей стадии независимо от изменений в файлах Git.

Источник: Werf — документация

На какую стадию по конвенции кладётся установка зависимостей приложения (bundle install/npm install/go mod download)?

install — стадия для зависимостей приложения по манифестам зависимостей; beforeInstall — для системных пакетов.

Источник: Werf — документация — пример Stapel shell instructions

Рекомендуемая литература

Официальная документация

Статьи и блоги

Книги

  • Nigel Poulton. Docker Deep Dive. 2023. ISBN 978-1916585206.

Связанные материалы