Публикую перевод моей статьи из блога ГитЛаба про то как начать использовать CI. Остальные переводы гитлабовских постов можно найти в блоге компании Softmart.
Представим на секунду, что вы не знаете ничего о концепции непрерывной интеграции (Continuous Integration — CI) и для чего она нужна. Или вы всё это забыли. В любом случае, начнем с основ.
Представьте, что вы работаете над проектом, в котором вся кодовая база состоит из двух текстовых файлов. Более того, очень важно, чтобы при конкатенации этих файлов в результате всегда получалась фраза “Hello world.” Если это условие не выполняется, вся команда лишается месячной зарплаты. Да, все настолько серьезно.
Один ответственный разработчик написал небольшой скрипт, который нужно запускать перед каждой отправкой кода заказчикам. Скрипт нетривиален:
cat file1.txt file2.txt | grep -q "Hello world"
Проблема в том, что в команде десять разработчиков, а человеческий фактор еще никто не отменял.
Неделю назад один новичок забыл запустить скрипт перед отправкой кода, в результате чего трое заказчиков получили поломанные сборки. Хотелось бы в дальнейшем избежать подобного, так что вы решаете положить конец этой проблеме раз и навсегда. К счастью, ваш код уже находится на GitLab, а вы помните про встроенную CI-систему. К тому же, на конференции вы слышали, что CI используется для тестирования…
После пары минут, потраченных на поиск и чтение документации, оказывается, что все что нужно сделать — это добавить две строчки кода в файл .gitlab-ci.yml
:
test:
script: cat file1.txt file2.txt | grep -q 'Hello world'
Добавляем, коммитим — и ура! Сборка успешна!
Поменяем во втором файле “world” на “Africa” и посмотрим, что получится:
Сборка неудачна, как и ожидалось.
Итак, у нас теперь есть автоматизированные тесты. 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
В результате появляется вторая вкладка
Однако мы забыли уточнить, что новый файл является артефактом сборки, что позволит его скачивать. Это легко поправить, добавив раздел 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
Проверяем… Все на месте:
Отлично! Однако, осталась одна проблема: задачи выполняются параллельно, а нам не нужно архивировать наше приложение в случаях, когда тест не пройден.
Задача ‘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
Посмотрим на получившиеся артефакты:
Скачивание файла “compile” нам ни к чему, поэтому ограничим длительность жизни временных артефактов 20 минутами:
compile:
stage: compile
script: cat file1.txt file2.txt > compiled.txt
artifacts:
paths:
- compiled.txt
expire_in: 20 minutes
Итоговая функциональность конфига впечатляет:
Прогресс налицо. Однако, несмотря на наши усилия, сборка до сих пор проходит медленно. Взглянем на логи:
Что, простите? Ruby 2.1?
Зачем тут вообще Ruby? А затем, что GitLab.com использует образы Docker для запуска сборок, а по умолчанию для этого используется образ ruby:2.1
. Само собой, в этом образе содержится множество пакетов, которые нам ни к чему. Спросив помощи у гугла, узнаем, что существует образ alpine
, который представляет собой практически «голый» образ Linux.
Для того, чтобы использовать этот образ, добавим image: alpine
в .gitlab-ci.yml
.
Благодаря этому время сборки сокращается почти на три минуты:
А вообще, в свободном доступе находится довольно много разных образов, так что можно без проблем подобрать один для нашего стека. Главное — помнить о том, что лучше подходят образы, не содержащие дополнительной функциональности — такой подход минимизирует время скачивания.
Теперь представим, что у нас появился новый заказчик, который хочет, чтобы вместо .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
Обратите внимание на то, что названия задач не обязательно должны быть одинаковыми. Более того, в таком случае параллельное выполнение задач на одной стадии было бы невозможным. Так что относитесь к одинаковым названиям задач и стадий как к совпадению.
А тем временем сборка не удалась:
Проблема в том, что конманда 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
выполняются параллельно:
В этой статье приведены далеко не все возможности GitLab CI, однако пока что остановимся на этом. Надеемся вам понравился этот небольшой рассказ. Приведенные в нем примеры были намеренно тривиальными — это было сделано для того, чтобы наглядно показать принципы работы CI не отвлекаясь на незнакомые технологии. Давайте подытожим изученное:
.gitlab-ci.yml
.В последнем разделе этой статьи приведен более формализованный список терминов и ключевых слов, использованных в данном примере, а также ссылки на подробные описания функциональности GitLab CI.
Ключевое слово/термин | Описание |
---|---|
.gitlab-ci.yml | Конфигурационный файл, в котором содержатся все определения сборки проекта |
script | Определяет исполняемый shell-скрипт |
before_script | Определяет команды, которые выполняются перед всеми заданиями |
image | Определяет используемый Docker-образ |
stage | Определяет стадию конвейера (test по умолчанию) |
artifacts | Определяет список артефактов сборки |
artifacts:expire_in | Используется для удаления загруженных артефактов по истечению определенного промежутка времени |
pipeline | Конвейер — набор сборок, которые выполняются стадиями |
Также обратите внимание на другие примеры работы с GitLab CI:
(Автор перевода — @sgnl_05)