Публикую перевод моей статьи из блога ГитЛаба про то как начать использовать CI. Остальные переводы гитлабовских постов можно найти в блоге компании Softmart.


Представим на секунду, что вы не знаете ничего о концепции непрерывной интеграции (Continuous Integration — CI) и для чего она нужна. Или вы всё это забыли. В любом случае, начнем с основ.

Представьте, что вы работаете над проектом, в котором вся кодовая база состоит из двух текстовых файлов. Более того, очень важно, чтобы при конкатенации этих файлов в результате всегда получалась фраза “Hello world.” Если это условие не выполняется, вся команда лишается месячной зарплаты. Да, все настолько серьезно.

Hello wolrd

Один ответственный разработчик написал небольшой скрипт, который нужно запускать перед каждой отправкой кода заказчикам. Скрипт нетривиален:

cat file1.txt file2.txt | grep -q "Hello world"

Проблема в том, что в команде десять разработчиков, а человеческий фактор еще никто не отменял.

Неделю назад один новичок забыл запустить скрипт перед отправкой кода, в результате чего трое заказчиков получили поломанные сборки. Хотелось бы в дальнейшем избежать подобного, так что вы решаете положить конец этой проблеме раз и навсегда. К счастью, ваш код уже находится на GitLab, а вы помните про встроенную CI-систему. К тому же, на конференции вы слышали, что CI используется для тестирования…

Запуск первого теста в CI

После пары минут, потраченных на поиск и чтение документации, оказывается, что все что нужно сделать — это добавить две строчки кода в файл .gitlab-ci.yml:

test:
  script: cat file1.txt file2.txt | grep -q 'Hello world'

Добавляем, коммитим — и ура! Сборка успешна! Build succeeded

Поменяем во втором файле “world” на “Africa” и посмотрим, что получится: Build failed

Сборка неудачна, как и ожидалось.

Итак, у нас теперь есть автоматизированные тесты. GitLab CI будет запускать наш тестовый скрипт при каждом пуше нового кода в репозиторий.

Возможность загрузки результатов сборки

Следующим бизнес-требованием является архивация кода перед отправкой заказчикам. Почему бы не автоматизировать и его?

Все, что для этого нужно сделать — определить еще одну задачу для CI. Назовем ее “package”:

test:
  script: cat file1.txt file2.txt | grep -q 'Hello world'

package:
  script: cat file1.txt file2.txt | gzip > package.gz

В результате появляется вторая вкладка Two tabs - generated from two jobs

Однако мы забыли уточнить, что новый файл является артефактом сборки, что позволит его скачивать. Это легко поправить, добавив раздел artifacts:

test:
  script: cat file1.txt file2.txt | grep -q 'Hello world'

package:
  script: cat file1.txt file2.txt | gzip > packaged.gz
  artifacts:
	paths:
	- packaged.gz

Проверяем… Все на месте: Checking the download buttons

Отлично! Однако, осталась одна проблема: задачи выполняются параллельно, а нам не нужно архивировать наше приложение в случаях, когда тест не пройден.

Последовательное выполнение задач

Задача ‘package’ должна выполняться только при успешном прохождении тестов. Определим порядок выполнения задач путем введения стадий (stages):

stages:
  - test
  - package

test:
  stage: test
  script: cat file1.txt file2.txt | grep -q 'Hello world'

package:
  stage: package
  script: cat file1.txt file2.txt | gzip > packaged.gz
  artifacts:
	paths:
	- packaged.gz

Должно сработать.

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

stages:
  - compile
  - test
  - package

compile:
  stage: compile
  script: cat file1.txt file2.txt > compiled.txt
  artifacts:
	paths:
	- compiled.txt

test:
  stage: test
  script: cat compiled.txt | grep -q 'Hello world'

package:
  stage: package
  script: cat compiled.txt | gzip > packaged.gz
  artifacts:
	paths:
	- packaged.gz

Посмотрим на получившиеся артефакты:

Unnecessary artifact

Скачивание файла “compile” нам ни к чему, поэтому ограничим длительность жизни временных артефактов 20 минутами:

compile:
  stage: compile
  script: cat file1.txt file2.txt > compiled.txt
  artifacts:
	paths:
	- compiled.txt
	expire_in: 20 minutes

Итоговая функциональность конфига впечатляет:

  • Есть три последовательных стадии: компиляция, тестирование и архивация приложения.
  • Результат стадии компиляции передается на последующие стадии, то есть приложение компилируется только однажды (что ускоряет рабочий процесс).
  • Архивированная версия приложения хранится в артефактах сборки для дальнейшего использования.

Какие образы Docker лучше использовать

Прогресс налицо. Однако, несмотря на наши усилия, сборка до сих пор проходит медленно. Взглянем на логи:

Ruby 2.1 is the logs

Что, простите? Ruby 2.1?

Зачем тут вообще Ruby? А затем, что GitLab.com использует образы Docker для запуска сборок, а по умолчанию для этого используется образ ruby:2.1. Само собой, в этом образе содержится множество пакетов, которые нам ни к чему. Спросив помощи у гугла, узнаем, что существует образ alpine, который представляет собой практически «голый» образ Linux.

Для того, чтобы использовать этот образ, добавим image: alpine в .gitlab-ci.yml. Благодаря этому время сборки сокращается почти на три минуты:

Build speed improved

А вообще, в свободном доступе находится довольно много разных образов, так что можно без проблем подобрать один для нашего стека. Главное — помнить о том, что лучше подходят образы, не содержащие дополнительной функциональности — такой подход минимизирует время скачивания.

Работа со сложными сценариями

Теперь представим, что у нас появился новый заказчик, который хочет, чтобы вместо .gz архива наше приложение поставлялось в виде образа .iso. Поскольку весь процесс сборки реализован через CI, все, что нам нужно сделать — добавить еще одну задачу. Образы ISO создаются с помощью команды mkisofs. В итоге конфигурационный файл должен выглядеть следующим образом:

image: alpine

stages:
  - compile
  - test
  - package

# ... задания "compile" и "test" в данном примере пропущены ради краткости

pack-gz:
  stage: package
  script: cat compiled.txt | gzip > packaged.gz
  artifacts:
	paths:
	- packaged.gz

pack-iso:
  stage: package
  script:
  - mkisofs -o ./packaged.iso ./compiled.txt
  artifacts:
	paths:
	- packaged.iso

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

А тем временем сборка не удалась: Failed build because of missing mkisofs

Проблема в том, что конманда mkisofs не входит в состав образа alpine, так что нужно установить ее отдельно.

Установка дполнительного ПО

На сайте Alpine Linux указано, что mkisofs входит в состав пакетов xorriso и cdrkit. Для установки пакета нужно выполнить следующие команды:

echo "ipv6" >> /etc/modules  # включить поддержку сети
apk update               	# обновить список пакетов
apk add xorriso          	# установить пакет

Все это — тоже валидные команды CI. Полный список команд в разделе script должен выглядеть следующим образом:

script:
- echo "ipv6" >> /etc/modules
- apk update
- apk add xorriso
- mkisofs -o ./packaged.iso ./compiled.txt

С другой стороны, семантически более корректно выполнять команды, ответственные за установку пакетов до раздела script, а именно в разделе before_script. При размещении этого раздела в верхнем уровне файла конфигурации, его команды будут выполнены раньше всех задач. Однако в нашем случае достаточно выполнить before_script раньше одной определенной задачи.

Итоговая версия .gitlab-ci.yml:

image: alpine

stages:
  - compile
  - test
  - package

compile:
  stage: compile
  script: cat file1.txt file2.txt > compiled.txt
  artifacts:
	paths:
	- compiled.txt
	expire_in: 20 minutes

test:
  stage: test
  script: cat compiled.txt | grep -q 'Hello world'

pack-gz:
  stage: package
  script: cat compiled.txt | gzip > packaged.gz
  artifacts:
	paths:
	- packaged.gz

pack-iso:
  stage: package
  before_script:
  - echo "ipv6" >> /etc/modules
  - apk update
  - apk add xorriso
  script:
  - mkisofs -o ./packaged.iso ./compiled.txt
  artifacts:
	paths:
	- packaged.iso

А ведь мы только что создали конвейер! У нас есть три последовательные стадии, при этом задачи pack-gz и pack-iso стадии package выполняются параллельно:

Pipelines illustration

Подводя итоги

В этой статье приведены далеко не все возможности GitLab CI, однако пока что остановимся на этом. Надеемся вам понравился этот небольшой рассказ. Приведенные в нем примеры были намеренно тривиальными — это было сделано для того, чтобы наглядно показать принципы работы CI не отвлекаясь на незнакомые технологии. Давайте подытожим изученное:

  1. Для того, чтобы передать выполнение определенной работы в GitLab CI, нужно определить одну или более задач в .gitlab-ci.yml.
  2. Задачам должны быть присвоены названия, советуем делать их осмысленными, чтобы потом самим не запутаться.
  3. В каждой задаче содержится набор правил и инструкций для GitLab CI, определяющийся ключевыми словами.
  4. Задачи могут выполняться последовательно, параллельно, либо вы можете задать свой собственный порядок выполнения, создав конвейер.
  5. Существует возможность передавать файлы между заданиями и сохранять их как артефакты сборки для последующего скачивания через интерфейс.

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

Описания ключевых слов и ссылки на документацию

Ключевое слово/термин Описание
.gitlab-ci.yml Конфигурационный файл, в котором содержатся все определения сборки проекта
script Определяет исполняемый shell-скрипт
before_script Определяет команды, которые выполняются перед всеми заданиями
image Определяет используемый Docker-образ
stage Определяет стадию конвейера (test по умолчанию)
artifacts Определяет список артефактов сборки
artifacts:expire_in Используется для удаления загруженных артефактов по истечению определенного промежутка времени
pipeline Конвейер — набор сборок, которые выполняются стадиями

Также обратите внимание на другие примеры работы с GitLab CI:

(Автор перевода — @sgnl_05)