Организация распределенного CI/CD с помощью werf

xlbplc9y3qygzics6y0djfpwn3i.png

werf — наша Open Source-утилита для сборки и деплоя приложений. Сегодня мы с радостью сообщаем, что werf научилась работать в распределенном режиме, начиная с версии v1.1.10 (доступна в каналах v1.1 alpha, beta, ea и stable). Для его подключения требуется минимум усилий.

Вот некоторые из примечательных особенностей нового режима:

  • хранение сборочных кэш-слоев (стадий) в реестре Docker-образов (stages storage);
  • продвинутое распределенное кэширование сборочных кэш-слоев (стадий) для сборщика stapel;
  • возможность использования произвольного количества runner’ов (постоянных или временных) для запуска werf;
  • эффективный и оптимизированный алгоритм выбора стадий и сборки;
  • подключение к любому экземпляру Kubernetes — единственная внешняя зависимость для этой функции (она необходима для синхронизации процессов werf, работающих в распределенном режиме, и для хранения внутренних кэшей).


Заметим, что сборщик Dockerfile также можно использовать в распределенном режиме, однако он пока не поддерживает продвинутое распределенное кэширование слоев.

Однако начнем с рассказа о том, что такое распределенный режим и какие компоненты необходимы для его реализации. Затем покажем, как включить этот режим и как перевести на него (с локального режима) существующие проекты, использующие werf. Наконец, рассмотрим демо-проект, который в полной мере задействует механизм распределенной сборки werf.

Общая информация


В werf имеется несколько ключевых концепций, связанных с процессом сборки: стадии, образы, хранилище стадий. В документации стадии и образы определяются следующим образом:

Мы предлагаем разделить сборочный процесс на этапы, каждый с четкими функциями и своим назначением. Каждый этап соответствует промежуточному образу, подобно слоям в Docker. В werf такой этап называется стадией, и конечный образ в итоге состоит из набора собранных стадий. Все стадии хранятся в хранилище стадий (stages storage).


Что за хранилище стадий? До версии v1.1.10 на этот вопрос можно было ответить так: «Это просто локальный сервер Docker». Однако начиная с текущего момента, werf позволяет хранить стадии в реестре Docker-образов. Более того, он поддерживает большинство реализаций Docker Registry, доступных сегодня.

Использование Docker Registry в качестве хранилища стадий позволяет проводить распределенную сборку образов на нескольких хостах. Для сборщика stapel, представляющего собой альтернативу Dockerfile со множеством полезных функций, доступно продвинутое кэширование слоев. Для него реализован эффективный и оптимизированный алгоритм выбора стадий и сборки:

  • Уже собранные стадии, имеющиеся в хранилище стадий, будут использованы при сборке новой стадии.
  • Стадия извлекается из хранилища стадий, когда это необходимо для сборки следующей стадии.
  • Стадия, извлеченная из хранилища стадий, останется в локальном кэше Docker-образов (автоматический сборщик мусора удалит наименее востребованные образы).
  • Публикация свежесобранных образов осуществляется гораздо быстрее, поскольку в момент, когда она происходит, Docker Registry (по совместительству хранящий и стадии) уже содержит все стадии образа. Они являются базовыми слоями для публикуемого образа.
  • Для сохранения свежесобранных стадий в хранилище алгоритм сборки использует оптимистическую блокировку: тем самым гарантируется, что только один сборщик сможет сохранить новую стадию, после чего она станет доступной для других процессов сборщика.


Для сборки образов на нескольких хостах можно также использовать и сборщик Dockerfile, однако на данный момент для него не реализовано продвинутое распределенное кэширование слоев (впрочем, это будет сделано).

С появлением распределенного режима werf предлагает два уровня кэширования Docker-образов:

  1. Стадии, хранящиеся в хранилище стадий (Docker-образы в Docker registry).
  2. Локальные стадии (Docker-образы), лежащие на локальных Docker-серверах каждого узла сборки.


Кроме того, имеются образы, опубликованные в репозитории образов, — назовем их разновидность (3).

Кэши нуждаются в периодической очистке. В werf для этого встроена команда werf cleanup, удаляющая Docker-образы (2)-го и (3)-го типов.

Локальные Docker-образы (1) пока приходится убирать вручную (только при использовании распределенного режима). Эти образы можно удалять с помощью любых инструментов (например, docker rmi). Будущие версии werf смогут автоматически удалять эти образы при выполнении связанных со сборкой команд, используя алгоритм LRU, что позволит поддерживать заполненность хранилища на сборочном узле на уровне 80%.

Также следует отметить, что распределенный режим werf использует стадии из хранилища стадий (Docker Registry) и извлекает только те образы, которые необходимы для сборки нового слоя (скачивает только базовый образ). При этом во время холостых сборок (когда образы в действительности не собираются) образы вообще не извлекаются.

Более подробная информация об алгоритме работы сборщика и архитектуре доступна в документации:


Требования к узлам


Для работы в распределенном режиме werf’у требуется подключение к какому-либо кластеру Kubernetes. Kubernetes будет использоваться для координации множества процессов werf при:

  • выборе и записи стадий в хранилище стадий;
  • публикации образов в репозиторий;
  • одновременном развертывании приложений с нескольких хостов.


Не имеет значения, используется ли данный кластер Kubernetes для деплоя приложения. Единственное требование — один и тот же экземпляр K8s должен использоваться для всего проекта.

Как именно он используется? werf создает ConfigMap cm/werf-PROJECT_NAME в пространстве имен werf-synchronization для каждого проекта. Этот ConfigMap используется для хранения так называемого кэша хранилища стадий и для распределенных блокировок. Для реализации распределенной блокировки в кластере Kubernetes используется Open Source-библиотека lockgate, созданная нами специально для werf.

Различные процессы werf, работающие с одним и тем же проектом, должны использовать единое хранилище стадий и один и тот же экземпляр кластера Kubernetes.

Дополнительную информацию о синхронизации можно найти в документации.

Новые команды для работы со стадиями


В werf реализованы новые команды для работы со стадиями:

  1. werf stages sync — копирует стадии между хранилищами.
  2. werf stages switch-from-local — помогает перевести существующий проект в распределенный режим (подробнее об этом процессе см. ниже).


Как включить распределенный режим


Для использования распределенного режима достаточно указать параметр --stages-storage=DOCKER_REPO_ADDRESS для всех команд werf.

Обратите внимание, что DOCKER_REPO_ADDRESS должен ссылаться на уникальный Docker-репозиторий для данного проекта. Этот репозиторий не может использоваться одновременно для нескольких проектов (хотя один и тот же Docker Registry, конечно, может использоваться несколькими проектами).

Команда werf ci-env, интегрирующая werf в процесс CI/CD, экспортирует переменную WERF_STAGES_STORAGE. Она содержит адрес Docker-репозитория, предназначенного для хранения стадий, и это хранилище по умолчанию будет использоваться при всех последующих вызовах werf. Вот пример этой переменной для GitLab CI/CD: WERF_STAGES_STORAGE=CI_REGISTRY_IMAGE/stages.

Если --stages-storage определён не :local, а как адрес Docker Registry (DOCKER_REPO_ADDRESS), werf автоматически задействует пространство имен Kubernetes werf-synchronization и текущий контекст из kubeconfig’а по умолчанию для подключения к кластеру. При этом пользователь может явно указать произвольное пространство имен с помощью опции --synchronization=kubernetes://NAMESPACE. Дополнительная информация доступна в документации.

Миграция существующего проекта


Проект можно переключить в распределенный режим, если он уже использует werf с локальным хранилищем стадий. Новая версия werf поставляется со специальными инструментами, упрощающими этот процесс. Подробная информация об этом доступна в специальном руководстве по миграции.

Демо-проект


Мы также подготовили демо-проект, чтобы продемонстрировать, как с помощью распределенного режима werf можно собрать приложение в публичном GitLab: symfony-demo.

Ниже приведены шаги по использованию распределенного режима werf:

1. Подготовьте werf.yaml:

Его содержимое
project: symfony-demo
configVersion: 1
---

image: ~
from: ubuntu:16.04
docker:
  WORKDIR: /app
  # Non-root user
  USER: app
  EXPOSE: "80"
  ENV:
    LC_ALL: en_US.UTF-8
ansible:
  beforeInstall:
  - name: "Install additional packages"
    apt:
      state: present
      update_cache: yes
      pkg:
      - locales
      - ca-certificates
  - name: "Generate en_US.UTF-8 default locale"
    locale_gen:
      name: en_US.UTF-8
      state: present
  - name: "Create non-root group for the main application"
    group:
      name: app
      state: present
      gid: 242
  - name: "Create non-root user for the main application"
    user:
      name: app
      comment: "Create non-root user for the main application"
      uid: 242
      group: app
      shell: /bin/bash
      home: /app
  - name: Add repository key
    apt_key:
      keyserver: keyserver.ubuntu.com
      id: E5267A6C
  - name: "Add PHP apt repository"
    apt_repository:
      repo: 'deb http://ppa.launchpad.net/ondrej/php/ubuntu xenial main'
      update_cache: yes
  - name: "Install PHP and modules"
    apt:
      name: "{{`{{packages}}`}}"
      state: present
      update_cache: yes
    vars:
      packages:
      - php7.2
      - php7.2-sqlite3
      - php7.2-xml
      - php7.2-zip
      - php7.2-mbstring
      - php7.2-intl
  - name: Install composer
    get_url:
      url: https://getcomposer.org/download/1.6.5/composer.phar
      dest: /usr/local/bin/composer
      mode: a+x
  install:
  - name: "Install app deps"
    # NOTICE: Always use `composer install` command in real world environment!
    shell: composer update
    become: yes
    become_user: app
    args:
      creates: /app/vendor/
      chdir: /app/
  setup:
  - name: "Create start script"
    copy:
      content: |
        #!/bin/bash
        php -S 0.0.0.0:8000 -t public/
      dest: /app/start.sh
      owner: app
      group: app
      mode: 0755
  - raw: echo `date` > /app/version.txt
  - raw: chown app:app /app/version.txt
git:
- add: /
  to: /app
  owner: app
  group: app

(файл в репозитории)


2. Для распределенного режима werf требуется экземпляр кластера Kubernetes — мы воспользуемся GKE. Подготовьте kube-config для кластера и установите секретную переменную BASE64_KUBECONFIG:

cat .kube/config | base64 -w0 > /tmp/base64_kubeconfig
# copy /tmp/base64_kubeconfig content and set BASE64_KUBECONFIG variable in CI/CD


k4m3dtxrg9nd8arckgclxbftuew.png

3. Подготовьте стадию сборки (build) в .gitlab-ci.yml:

stages:
  - build

Build:
  stage: build
  script:
  - export KUBECONFIG=$(mktemp -d)/kubeconfig
  - echo $BASE64_KUBECONFIG | base64 -d -w0 > $KUBECONFIG
  - type multiwerf && source $(multiwerf use 1.1 ea --as-file)
  - type werf && source $(werf ci-env gitlab --as-file)
  - werf build-and-publish
  tags:
  - werf-demo-runner


(файл в репозитории)

В этом примере мы реализовали только стадию сборки. Для полноценного CI/CD дополнительно понадобятся стадии развертывания (deploy), очистки (cleanup) и удаления (dismiss), однако их реализацию оставим за рамками статьи. С полным примером можно ознакомиться в руководстве по использованию GitLab.

4. Убедитесь, что в вашем проекте явно не задан параметр --stages-storage и не прописана переменная окружения WERF_STAGES_STORAGE. Команда werf ci-env установит WERF_STAGES_STORAGE=CI_REGISTRY_IMAGE/stages (в нашем примере это registry.gitlab.com/distorhead/symfony-demo/stages).

5. Проверьте реестр контейнеров на соответствующей странице проекта: symfony-demo/container_registry. Здесь показаны собранные стадии проекта в хранилище стадий Docker Registry:

tvts2ving3drloyeswek_ictv-i.png

6. Теперь можно попробовать внести изменения в исходники своего приложений (src/Kernel.php с помощью merge_requests/2) и пересобрать его. Сборочный процесс (build job) берет существующие стадии из Docker Registry (хранилища стадий) и пересобирает только стадию gitLatestPatch:

spae3jdxpkjtohza0zfho2bq4is.png

Подобный вывод означает, что все работает правильно!

Заключение


Распределенный режим — очередная важная веха в развитии проекта werf. Он приносит масштабируемость, требуя при этом минимальных усилий от пользователя.

Распределенный режим доступен с версии werf v1.1.10. В этой статье мы описали, как его включить и как на него перевести существующий проект (уже использующий локальный режим werf). Распределенный режим рекомендуется для систем CI/CD и включается для них по умолчанию.

Попробуйте werf, если еще не сделали это! И следите за новостями: скоро мы опубликуем руководство по полной интеграции werf c GitHub Actions.

P.S.


Читайте также в нашем блоге:

© Habrahabr.ru