Непрерывная интеграция и развёртывание (CI/CD) в GitLab

GitLab logo

В прошлом месяце мы, наконец, переехали со своей старой CI/CD (continuous integration and delivery) системы домашней выделки на GitLab Community Edition, и этот факт делает моё лицо счастливым до сих пор. Всё-таки так приятно, когда и репозитории, и коммиты, и компиляция, и тесты, и результаты всего этого, и даже так кнопка «Одобрям-с», которая отправляет релиз-кандидата в репозиторий одобренных релизов — когда всё это лежит в одном месте и прекрасно интегрируется друг с другом.

От чего я действительно фанатею в Гитлабе, так это насколько легко в нём всё это настраивать. Настолько просто, что сегодня мы настроим полнофункциональную CI/CD систему от начала и до конца. От установки GitLab и до выкатывания релиза после успешных тестов.

Ну что, будем начинать?

Шаг 0. Демо-проект

Маленький дисклаймер: я буду делать всё на Маке и попутно злоупотреблять Докером, но это только потому, что мне так проще. В реальности же развернуть GitLab CI/CD можно хоть на бумаге, так что мой выбор инструментов ни к чему не обязывает.

Итак, есть простенький пример веб-приложения на TypeScript, который просто меняет текст на веб-странице сразу после её открытия. Простой-то он простой, но тут есть и компиляция, и тест-другой можно придумать, да и сервер с приложением хорошо бы обновить, если компиляция и тесты удались. И на всё это можно настроить CI/CD конвейер с build, test и deploy стадиями внутри. Только займёмся мы этим потом, а сейчас посмотрим, как выглядит наша морская свинка изнутри:

Ничего особенного. Кроме, собственно, контента проекту понадобится  .gitignore файл, чтобы не комитить результаты компиляции в репозиторий (index.ts -> index.js), и README.md, чтобы проект выглядел мало-мальски солидным.

Вот и всё. Если где-нибудь в системе завалялся TypeScript компилятор (установленный, например, через sudo npm install -g typescript), то tsc index.ts превратит TS в JS, а запущенный с текущей директории абсолютно любой веб-сервер откроет ничем не примечательную страницу:

demo application

Теперь можно создать вокруг проекта git репозиторий и двинуться к первому интересному шагу — установке GitLab.

Шаг 1: Установка GitLab

Кроме прочих достоинств у GitLab есть готовый Docker образ, так что устанавливать его элементарно. Кстати, у меня на домашнем сервере Гитлаб работает именно в контейнере, и за всё время после установки проблем с ним было — ноль. А как его удобно обновлять…

В нашем CI/CD будет больше, чем один контейнер, так что вместо привычного docker run ... я запущу Гитлаб через docker-compose и добавлять последующие контейнеры буду уже туда.

Начальная версия docker-compose.yml будет такая:

docker-compose up -d gitlab сделает всё магию, и через минуту докерных размышлений можно идти и настраивать пароль для root аккаунта.

Create root password

Sign in as root

Итак, пароль есть, и мы внутри. Создаём новый проект:

Create project

Edit project

Как только мы его создали, можно воспользоваться заботливо предложенными командами git remote add и git push -u origin master чтобы импортировать наш демо-проект в GitLab.

Import project

Demo project in GitLab

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

Шаг 2: Настраиваем стадию «Build»

Чтобы убедить GitLab делать различные CI штуки сразу после того, как кто-нибудь в него сделал git push, нужно дать ему две вещи:  .gitlab-ci.yml файл с инструкциями, что и в каком порядке делать, и какой-нибудь хост (реальный, виртуальный, контейнер — без разницы), который в терминологии Гитлаба будет называться «runner», и который, собственно, все эти инструкции будет выполнять. Начнём мы с файла.

.gitlab-ci.yml

Наша компиляции заключается в запуске TypeScript компилятора и натравливании его на index.ts. И желательно это сделать на хосте, на котором TypeScript компилятор установлен. Звучит просто, и так же просто описывается в .gitlab-ci.yml:

Секция stages описывает, какие стадии будут в нашем CI, , строка stage: build говорит, что задача под названием Compile принадлежит стадии build, а tags показывает, какие тэги должны быть на runner хосте, чтобы ему можно было эту задачу доверить.

Вообще, выдумывание имён тэгов (и стадий, и задач) весит целиком на нас. Я,  например, в качестве тегов выбираю фичи, которые runner поддерживает, и потом уже в конкретных задачах расставляю теги-фичи, которые им нужны. Ну вроде msbuild14, dotnetcoresdk1.0.4, docfx, и т.п.

Если закоммитить .gitlab-ci.yml вместе с остальным демо-проектом в репозиторий, то в гитлабе произойдёт что-то отдалённо интересное. На странице «Pipelines» появится вот такая штука:

Pending pipeline

Гитлаб распознал, что ему скормили билд-задачу, постарался её запустить, но так как не было ни одного знакомого раннера (именно так я буду иногда называть runner-хосты) с тегом typescript, задача подвисла до лучших времён. Ну что же, лучшие времена определённо сейчас наступят.

Настраиваем gitlab-runner сервис

Знаете, что отличает обычный хост или контейнер от runner-хоста? Установленный сервис, который называется gitlab-runner, и больше ничего. Нам просто нужно скачать gitlab-runner с официального сайта (apt-get install тоже есть), зарегистрировать его в нашем GitLab, повесить тег-другой, запустить, и всё, раннер готов.

Для простоты, я установлю runner в Docker контейнере и зарегистрирую как докеровский сервис в docker-compose.yml. Таким образом он очутится в одной сети с GitLab и они смогут спокойно общаться. Вот как будет выглядеть Dockerfile, создающий образ с runner:

В качестве базового образа я взял nodejs, ведь нам ещё устанавливать TypeScript через npm. Почти весь код регистрации раннера я скопипастил с официального сайта. В секции RUN лежит установка раннера и TypeScript, а уже регистрация и запуск пошли в CMD секцию. Немного уродливо, но работать будет. По крайней мере один раз.

В команде регистрации — gitlab-runner register — есть четыре очень важных параметра:

  • -u — туда передаётся адрес, на котором живёт GitLab сервер. Так как у нас docker-compose, и потому общая сеть, в качестве хоста можно передавать имя compose сервиса.
  • -r — токен регистрации, который знакомит раннер и гитлаб друг с другом. Для каждого экземпляра Гитлаба токен свой (он вроде и от проекта к проекту разный), и узнать его можно на странице «GitLab -> Settings -> CI/CD settings». CI/CD settings menuCI/CD settings
  • --executor объясняет каким образом интерпретировать и запускать команды из .gitlab-ci.yml. В нашем случае там обычные шелл команды, так что и значение передали shell. Кстати, там ещё мог бы быть PowerShell для Windows, или вообще Docker..
  • --tags — в нашем случае — какие фичи поддерживает раннер. У нас установлен только TypeScript, вот его-то я в теги и передал.

Теперь, если добавить этот Dockerfile в docker-compose.yml в качестве сервиса под названием runner-ts (и заодно положить его в одноимённую папку), и запустить его через docker-compose up -d runner-ts, то та наша подвешенная задача отвиснет и таки выполнится.

Pipeline succeeds

Ломаем билд

Ради интереса, что будет если сломать билд, написав какую-нибудь ерунду в index.ts? Он же точно сломается, так ведь? Добавим-ка лишний пробел в ненужное место:

Коммитим, ну и натурально следующий билд падает с живописным красным цветом:

Broken build

На любые красные (и зелёные, да куда угодно) иконки можно кликнуть и узнать больше деталей.

pipeline-broken-commit-details

Ну и конечно, заполучив исправленный index.ts, билд станет празднично зелёным:

pipeline-fix-the-build

Вот он какой, CI.

Шаг 3. Настраиваем стадию «Test»

Вот я абсолютно не в настроении настраивать в полночь какой-нибудь фреймуорк для тестирования, но мы вполне можем протестировать стиль кода — tslint никто ведь не отменял.

Стадия тестирования — ещё одна секция в .gitlab-ci.yml и ещё один раннер. Мы могли бы просто доустановить tslint в существующий раннер, но я не люблю модифицировать существующие сервера. Лучше уж заменить, или новый добавить.

Сначала добавим ещё одну задачу в .gitlab-ci.yml:

Она идёт в отдельной test стадии, которая не начнётся, пока успешно не завершится build.

Теперь используем силу копипасты и создаём новый Dockerfile из предыдущего, но в этот раз c установленным tslint, и регистрируем его в качестве runner-tslint сервиса в существующий docker-compose.yml:

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

Ну что, добавляем runner-tslint в docker-compose.yml, запускаем через docker-compose up -d runner-tslint и после коммита обновлённого .gitlab-ci.yml любуемся на целых две стадии в нашей CI: Build и Test:

pipeline-test-failure

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

pipeline-test-failure-details

Пришлось делать ещё один коммит и исправлять.

Шаг 4. Добавляем стадию развёртывания

В CI/CD аббревиатуре последняя буква «D» подразумевает, что после успешной сборки и тестирования продукт можно где-нибудь и развернуть. Этим и займёмся.

Обычно релизы разворачивают как минимум в двух средах: staging — для тестирования, и production — для денег. На первую устанавливать новый билд вполне безопасно, она для того и создавалась. Выкатывать же новый релиз в продакшен стоит более осторожно. Памятуя это, в наш CI/CD можно добавить ещё два шага: автоматическую установку на staging, если билд удался, и ручную установку в production, если так решило начальство.

Раннер для развёртывания

Мне сразу было немного непонятно, как лучше сэмулировать два сервера и дать возможность их обновлять. Но потом решил сделать два nginx контейнера, у которых папки с html будут подключены к третьему контейнеру — раннеру развёртывания. Это будет просто ещё один gitlab-runner, на который даже компилятор устанавливать не надо:

В этот раз я взял базовый образ с Убунтой и собрался запускать раннер-сервис под root аккаунтом. Если взять обычного пользователя, то у него будут проблемы с правом на запись.

Расшарить папки между несколькими контейнерами при помощи Docker volumes оказалось совсем не тяжело:

Запустились новые контейнера тоже легко, так что можно перейти к .gitlab-ci.yml.

Задачи развёртывания в .gitlab-ci.yml

Есть один момент: распространять ведь мы будет не index.ts, а JavaScript файл, который из него получается. Так как каждая задача получает на вход чистую рабочую копию, то получить готовый index.js из стадии build просто так не выйдет. Тут можно либо ещё раз скомпилировать TS уже на стадии deploy, либо сделать результат компиляции в build артифактом, и тогда Гитлаб-таки начнёт копировать его между стадиями:

Теперь вернёмся к развёртыванию. Я покажу сразу финальную версию .gitlab-ci.yml, и там уже пройдусь по тому, что в неё добавилось:

Итак, появилась ещё одна стадия — deploy, это понятно. Во-вторых, добавились две задачи — Deploy-Staging и Deploy-Production. Задачи развёртывания отличаются от обычных задач только секцией environment , но даже и она не обязательна.

Имя для environment можно задавать любое и даже вычислять динамически. Например, на моём рабочем проекте это имена релизных веток. В этой же демке — просто слова «Staging» и «Production».

Опция url — ссылка на то, где потом можно будет увидеть развёрнутый релиз.

Самая последняя строка — when: manual — предсказуемо превращает Deploy-Production задачу из автоматической в ручную.

Коммитим изменённый .gitlab-ci.yml ещё раз, делаем git push origin master (а я ещё и пару ошибок исправил), и вот оно, CI/CD счастье:

Three successful stages

Три зелёных чекбокса. Когда такое случается на работе — это праздник.

Можно кликнуть на детали билда и выяснить, что на самом деле только «Deploy-Staging» задача была выполнена. «Deploy-Production» нужно запускать вручную:

Неприрывная интеграция: от начала и до конца

Что и где сейчас установлено можно узнать на странице «Environments». Те url которые задавались в .gitlab-ci.yml, будут как раз здесь. Можно кликнуть и посмотреть релиз вживую на сервере:

Environments page

Мораль

Просто задумайтесь, что мы только что сделали. Мы собрали с нуля настоящую CI/CD систему, которая на любой push в репозиторий реагирует целой серией шагов от компиляции до развёртывания, и всё это абсолютно автоматически. Если что-то пойдёт не так, то GitLab может прислать «письмо счастья». Если всё пройдёт хорошо, то он может послать код в релиз. Это просто прекрасно.

Конечно, можно заявить, что сделать CI/CD для hello-world это одно дело, а для реального проекта — совсем другое. Ну как тут сказать. У меня есть относительно большой живой проект, у которого только в master ветке полтора десятка задач (количество всё растёт) раскиданных по трём стадиям, семь Windows/Linux runner машин и три сценария развёртывания в Google Storage. А ещё есть полгода legacy веток, у которых CI правила хоть и похожие, но с нюансами. И не скажу, что настраивать это было сильно сложнее. Принципы ведь те же.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *