company_banner

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



    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



    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:



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



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

    Заключение


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

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

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

    P.S.


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

    Флант
    DevOps-as-a-Service, Kubernetes, обслуживание 24×7

    Комментарии 9

      0
      Наверное стоит упомянуть, что werf'у, по прежнему нужен демон докера для сборки, и поды внутри которых запускается сборка образа должны быть запущены с демоном докера, то есть по сути нужен DIND контейнер, и его нужно запускать со специальными привилегиями. Или я неправильно всё понял?
        0

        В планах у них есть добавление чего-то типа buildah для сборки, что позволит собирать без запущенного докер-демона, но пока нет.

          0
          Если не хочется использовать DIND, то начиная с Gitlab 12.10 есть возможность сборки образов с помощью kaniko в AWS Fargate. Возможность недокументированная, есть куча особенностей, но стоит это раз в почти в 10 раз дешевле, чем AWS CodeBuild, если Fargate запускать на спотовых мощностях. И при этом масштабируется практически неограниченно.
          Если интересно, как это настроить, могу сделать пост.
            0
            Про kaniko, buildkit, buildah, makisu я знаю. Меня интересует сборка именно в werf. Просто в статье это не совсем очевидно, мне кажется об этом нужно было упомянуть.

            то начиная с Gitlab 12.10 есть возможность сборки образов с помощью kaniko в AWS Fargate


            Gitlab не использую, но странно что это как-то ограниченно его версией. В Jenkins запускаю любой сборщик образов, без каких-либо проблем.

            Если интересно, как это настроить, могу сделать пост

            Думаю всем будет интересно почитать. Я для билдов AWS не использую, довольно дорого. Но нужной масштабируемости достигал связкой на hetzner cloud. k8s + cluster autosclaer + jenkins + kaniko. Правда kaniko багует частенько, да и хотелось использовать werf deploy, а не werf helm deploy-chart, по этому пока от этой связки отказался.
            0

            Так-то оно так, но ни kaniko, ни buildah не умеют собирать по-настоящему распределённый кеш. Чтобы уже собранные слои были доступны во всех сборках.


            В werf теперь возможно такое: werf собирает коммит номер 1 на одном билдере. Пользователь делает новый коммит в git, werf запускается на другом билдере, накладывает патч на уже существующий кеш из предыдущей сборки на другом раннере и публикует новый образ.


            Это похоже на то, что предоставляет --cache-from в docker builder, но быстрее и эффективнее и с поддержкой ansible и инкрементальных пересборок по истории git и т.д.

              0

              Интересно. Конечно. Пишите!!! Очень ждем

            0

            Да, сборщик werf требует доступа к локальному docker-server на хостах, где запускается.

              +2

              Небольшое дополнение к статье. В werf сейчас реализован принципиально новый подход к сборке и публикации образов в docker registry, отличный от того, что используется при docker build:


              • Werf делает push каждого собранного слоя (стадии) в registry сразу после успешной сборки, что позволяет сразу переиспользовать его в других процессах сборки на произвольных машинах. Это возможно благодаря новому алгоритму публикации и выборки стадий с оптимистичными блокировками.
              • На момент публикации финального образа по определённому имени все необходимые слои, из которых состоит этот образ, уже есть в registry. Werf по факту делает push лишь для небольшого слоя с некоторой мета-информацией и ставит на этот слой тег. Таким образом этот push происходит моментально, т.к. registry переиспользует существующие слои.

              К тому же werf по умолчанию предлагает content-based-тегирование, что позволяет ещё лучше переиспользовать уже существующие слои и избавится от лишних ребилдов и редеплоев приложения.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое