company_banner

GitLab CI для непрерывной интеграции и доставки в production. Часть 2: преодолевая трудности


    Эта статья продолжает первую часть, содержащую подробное описание нашего пайплайна:

    image

    … и рассказывает о проблемах, с которыми мы столкнулись для его реализации, и их решении.

    Итак, я остановился на том, что созданный .gitlab-ci.yml не позволяет реализовать пайплайн в полной мере, поскольку GitLab CI не предоставляет директив для разделения задач по пользователям и для описания зависимостей выполнения задач от статуса выполнения других задач, а также не позволяет разрешить модификацию .gitlab-ci.yml только для отдельных пользователей.

    1. Защита .gitlab-ci.yml от изменений


    Любой пользователь, который имеет разрешение на git push, может изменить .gitlab-ci.yml и сломать production. Эту проблему обсуждают в тикетах GitLab: как минимум — в #24794 и #20826 с подачи нашего коллеги.

    Пока сложно сказать, будет ли когда-либо реализована защита из коробки, но на данный момент мы реализовали её упрощённый вариант с помощью небольшого патча: push’ить коммиты с изменениями в .gitlab-ci.yml могут только некоторые пользователи — обычно это команда DevOps, т.к. сборка и развёртывание в их зоне ответственности.

    Помимо применения патча потребуется добавить boolean-столбец ci_admin в таблицу с пользователями. Кому в столбце установлено true, тот может делать git push с изменениями в .gitlab-ci.yml.

    2. Переменные для скрипта задачи


    Вторая проблема, которую получилось решить довольно легко, — переменные среды GITLAB_USER_ID и GITLAB_USER_EMAIL для скрипта задачи с идентификатором пользователя и его почтой. Эти переменные можно использовать, чтобы определить, может ли пользователь запустить задачу. Реализовано как решение тикета #21825, принято в основную ветку (upstream) и доступно в GitLab CI начиная с версии 8.12:



    3. Зависимости между стадиями


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

    То есть, во-первых, автоматические задачи запускаются только в момент создания пайплайна, а во-вторых, нельзя сделать такой процесс, когда успешное выполнение ручной задачи запустило бы автоматические задачи, расположенные дальше в пайплайне. Документация по сути описывает поведение автоматических задач. Ручные же задачи живут «сами по себе» и могут быть запущены в любой момент, независимо от статуса выполнения задач на предыдущих стадиях.

    На этот счёт есть несколько тикетов, где предлагается изменить поведение ручных и автоматических задач:


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

    Артефакт задачи — это файлы, указанные в директиве artifact, которые будут доступны (после успешного завершения задачи) всем остальным задачам на последующих стадиях. Тут, правда, есть свои подводные камни: файлы со всех задач стадии будут доступны на дальнейших стадиях и удалить что-то из этого набора нельзя. В то же время файлы артефакта задачи недоступны в других задачах той же стадии.

    Рассмотрим подробнее на двух примерах. Сначала на примере стадий testing и staging:



    По описанию пайплайна, задачи развёртывания на окружения тестировщиков (deploy to qa-*) можно запускать только после выполнения всех тестов, а остальные задачи такой зависимости не имеют. Для реализации этой логики в конце успешного выполнения тестов делается touch файла с именем задачи, а в начале выполнения задач deploy to qa-*, на стадии staging, проверяется наличие этих файлов.

    Вот для примера листинги задач test integration и deploy to qa-1:

    test integration:
      stage: testing
      tags: [deploy]
      script:
        - mkdir -p .ci_status
        - echo "test integration"
        - touch .ci_status/test_integration
      artifacts:
        paths:
          - .ci_status/
    
    deploy to qa-1:
      tags: [deploy]
      stage: staging
      when: manual
      script:
        - if [ ! -e .ci_status/test_unit -o ! -e .ci_status/test_integration -o ! -e .ci_status/test_selenium ]; then echo "Нужно успешное выполнение всех тестов"; exit 1; fi
        - echo "execute job ${CI_BUILD_NAME}"
        - touch .ci_status/deploy_to_qa_1
      artifacts:
        paths:
          - .ci_status/

    Добавилась директива artifact, которая определяет пути в репозитории, сохраняемые GitLab CI в архив после выполнения задачи и разархивируемые перед выполнением следующей задачи. Чтобы не перечислять все файлы, указывается директория .ci_status, которую не помешает создать во время выполнения задачи (mkdir -p).

    Исходник: файл .gitlab-ci.yml с зависимостью стадии staging от testing доступен здесь.

    Второй пример немного сложнее — это зависимость стадии production от стадии approve:



    Задачи approve и not approve создают файлы, которые проверяет задача production. Это можно сделать так же, как и в предыдущем примере, но хочется, чтобы задачи NOT approve и approve работали как переключатель. Такой работе мешает факт, что нельзя удалить файлы артефактов от другой задачи. Поэтому задачи не просто создают файл, а пишут в него timestamp. В начале выполнения задачи deploy to production выполняется проверка: если в файле от задачи approve timestamp больше, то можно продолжать, если нет — задача завершается с ошибкой.

    approve:
      script:
        - mkdir -p .ci_status
        - echo $(date +%s) > .ci_status/approved
      artifacts:
        paths:
          - .ci_status/
    
    NOT approve:
      script:
        - mkdir -p .ci_status
        - echo $(date +%s) > .ci_status/not_approved
      artifacts:
        paths:
          - .ci_status/
    
    deploy to production:
      script:
        - if [[ $(cat .ci_status/not_approved) > $(cat .ci_status/approved) ]]; then echo "Нужно разрешение от релиз-инженера"; exit 1; fi
        - echo "deploy to production!"

    После выполнения задачи appove успешно выполняется deploy to production:



    После выполнения задачи NOT approve следующая за ней задача deploy to production завершается с ошибкой:



    Исходники:


    Что дальше?


    Осталось не озвученным требование разрешать отдельные задачи только некоторым пользователям. На данном этапе стало понятно, как это можно реализовать: нужен REST API, который можно будет запросить через curl с передачей переменных GITLAB_USER_ID и GITLAB_USER_EMAIL. Создание такого REST API выходит за рамки данной статьи.

    В приведённых примерах скрипт, проверяющий зависимости, хранится в .gitlab-ci.yml. Это очень неудобно, если проектов много и нужно что-то поправить, например, если появится новое окружение для qa или окружений для pre-production станет больше. Мы это решили с помощью вынесения скриптов в один внешний скрипт, который не хранится в каждом репозитории, а устанавливается на машины с раннерами.

    Такому скрипту доступно несколько переменных среды. На основе этих переменных скрипт принимает решение, какой вид задачи запущен, по файлам от предыдущих стадий проверяет, можно ли запускать эту задачу. Если нужно, проверяет доступ для пользователя через внешний REST-сервис. Скрипт содержит в себе инструкции, которые нужно выполнить для задачи и после их успешного выполнения создаёт файлы, на которые будет реагировать следующая задача.

    Обычно в пайплайне не так много вариаций задач, наш скрипт знает о трёх:

    1. инструкции для сборки,
    2. инструкции для разворачивания,
    3. инструкции approve и not approve.

    Инструкции тоже получают переменные среды и могут подстраиваться под конкретную задачу. Так как вариантов сборки и развёртывания в окружениях великое множество, да и количество проектов в GitLab тоже у всех отличается, то приводить реализацию такого скрипта считаю излишним. Впрочем, если есть вопросы — давайте обсудим в комментариях.

    Вместо заключения


    Надеюсь, что данная статья раскроет для вас новые интересные возможности GitLab CI и даст отправную точку для реализации собственных крутых пайплайнов.



    P.S. Не забывайте про проверку .gitlab-ci.yml по адресу https://мой-гитлаб/ci/lint. Поможет сэкономить время!
    Флант
    739,30
    Специалисты по DevOps и Kubernetes
    Поделиться публикацией

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

      +1
      А для чего REST-API? Можно же получить имя и email того кто сделал коммит прямо в .gitlab-ci.yml:
      USER_NAME=$(git log -1 —pretty=%an)
      USER_EMAIL=$(git log -1 —pretty=%ae)
      COMMIT_MESSAGE=$(git log -1 —pretty=%B)
      

      Форматы для pretty тут
        +1

        Согласен, про REST API невнятно вышло. Зачем вообще он упомянут? Вот есть такой тикет https://gitlab.com/gitlab-org/gitlab-ce/issues/21911 — он про права пользователей на запуск задач, на запуск задач в определённых environment, на доступ к секретным переменным, на доступ к раннерам. Когда это будет в gitlab, то права пользователям будем выдавать в интерфейса gitlab-а и не надо будет никаких "самописных с боку систем с REST API".


        Переменная GITLAB_USER_EMAIL это email того, кто запускает задачу в пайплайне, а не email последнего коммитера. Ваш вариант пригодился бы, как решение проблемы с правкой .gitlab-ci.yml, однако email последнего коммитера в общем случае не обязан совпадать с email того, кто пушит.

          0
          гита может не быть
            0
            С трудом представляю. Гита нет в Гитлабе?
              0

              Если используется директива image: то команды будут запускаться через указанный там образ. И вот в этом образе может не быть команды git.

          0
          Используется ли кеширование для зависимостей, если да то как происходит их обновление?

          Есть ли билды в контейнерах, используете чистый докер имидж или свои?

          При монтировании .gradle папки как volume билд валится с ошибкой
          Failed to load native library 'libnative-platform.so' for Linux amd64
          что то похожее высказывалось здесь, но в последней версии gradle все так же
          (используется docker in docker и официальный gradle image https://hub.docker.com/_/gradle/)
          Можете что-нибудь посоветовать?
            +1

            По вашей ссылке сказано, что проблема из-за версий glibc и скорее всего в официальном gradle image старая версия glibc, либо проблема из-за использования alpine с его musl-libc — мы тоже на эти грабли наступили, правда в другом случае https://github.com/flant/dapp/issues/164.


            Я так понял, вам было бы интересно узнать, как не перекачивать лишний раз java-артефакты или вообще чтобы скачивать только те, у которых версия изменилась? Немножко за рамками темы статьи на самом деле, но попробую ответить.


            Мы в основном не используем возможности gitlab-а по сборке через docker, а используем свою утилиту dapp. В ней реализована поддержка сборки отдельных образов и копирование файлов из этих образов в финальный. На поверхности это похоже на multi stages в последних версиях docker, но есть и существенные отличия, как минимум dapp знает про git и dapp собирает образы по стадиям, что даёт возможность пересобирать только те стадии, для которых изменились указанные файлы.


            Для примера в вашем случае в Dappfile будет описан отдельный образ в котором живёт gradle (можно указать, что он на основе официального gradle), куда при сборке подключится .gradle и где выполнится gradle build по исходникам.
            После из этого образа скопируются указанные war, jar, ear или вся папка target в указанное место в финальном образе.
            Т.к. dapp знает про git, то можно на одной стадии попросить gradle скачать свежие версии зависимостей, если изменился build.gradle (времязатратная стадия, но изменения редки, поэтому будет пересобираться не часто), а на следующей стадии gradle будет собирать проект, если изменились исходники в src.

            0
            совсем недавно пришлось работать с Gitlab и возник, казалось бы, простой вопрос.
            Есть несколько веток, который нудно сливать в мастер автоматически после коммита.
            Но как студент, нутром чувствую, что просто, а понять пока не могу.
            подскажите?
              0
              Вам ответили ниже, но коллега промахнулся веткой, так что вы могли не заметить :-)
              0

              Если сливать именно после коммита (git commit), то достаточно git хуков в локальном репозитории. Если же после push или после принятия MR, то хуки в gitlab. Ещё есть кнопка merge when build succeeds — но это наверно другая история.

                +1
                Может кто-то подсказать начинающему, что значит эта деректива, что она делает?
                deploy to dev-1:
                  <<: *staging-deploy-common
                
                  +1
                  что значит эта деректива, что она делает?

                  Во-первых, откуда вы её взяли? В статье поиском не нашёл.
                  Во-вторых, если всё же открыть документацию, то там очень понятно с примерами написано, что это ссылка на якорь. Это значит, что в это место подставится содержимое staging-deploy-common (если прям по простому объяснить).
                  Ну и вопросы лучше задавать на Тостер'е.

                    0
                    Благодарю, теперь понятно. Взял из их примера. Честно говоря, не зная что это концептуально, сложно было бы найти это в мануале.

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

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