В прошлом месяце мы, наконец, переехали со своей старой CI/CD (continuous integration and delivery) системы домашней выделки на GitLab Community Edition, и этот факт делает моё лицо счастливым до сих пор. Всё-таки так приятно, когда и репозитории, и коммиты, и компиляция, и тесты, и результаты всего этого, и даже так кнопка «Одобрям-с», которая отправляет релиз-кандидата в репозиторий одобренных релизов — когда всё это лежит в одном месте и прекрасно интегрируется друг с другом.
От чего я действительно фанатею в Гитлабе, так это насколько легко в нём всё это настраивать. Настолько просто, что сегодня мы настроим полнофункциональную CI/CD систему от начала и до конца. От установки GitLab и до выкатывания релиза после успешных тестов.
Ну что, будем начинать?
Шаг 0. Демо-проект
Маленький дисклаймер: я буду делать всё на Маке и попутно злоупотреблять Докером, но это только потому, что мне так проще. В реальности же развернуть GitLab CI/CD можно хоть на бумаге, так что мой выбор инструментов ни к чему не обязывает.
Итак, есть простенький пример веб-приложения на TypeScript, который просто меняет текст на веб-странице сразу после её открытия. Простой-то он простой, но тут есть и компиляция, и тест-другой можно придумать, да и сервер с приложением хорошо бы обновить, если компиляция и тесты удались. И на всё это можно настроить CI/CD конвейер с build, test и deploy стадиями внутри. Только займёмся мы этим потом, а сейчас посмотрим, как выглядит наша морская свинка изнутри:
1 2 3 4 5 6 7 |
<!doctype html> <html> <head> <script src="index.js"></script> </head> <body>Waiting for JS to fire</body> </html> |
1 2 3 4 |
window.addEventListener('load', e => document.querySelector('BODY').textContent = 'JS worked', false ); |
Ничего особенного. Кроме, собственно, контента проекту понадобится .gitignore
файл, чтобы не комитить результаты компиляции в репозиторий (index.ts -> index.js), и README.md
, чтобы проект выглядел мало-мальски солидным.
1 |
*.js |
1 2 |
GitLab-CI demo project ====================== |
Вот и всё. Если где-нибудь в системе завалялся TypeScript компилятор (установленный, например, через sudo npm install -g typescript
), то tsc index.ts
превратит TS в JS, а запущенный с текущей директории абсолютно любой веб-сервер откроет ничем не примечательную страницу:
Теперь можно создать вокруг проекта git репозиторий и двинуться к первому интересному шагу — установке GitLab.
1 2 3 |
git init . git add . git commit -m "Initial commit" |
Шаг 1: Установка GitLab
Кроме прочих достоинств у GitLab есть готовый Docker образ, так что устанавливать его элементарно. Кстати, у меня на домашнем сервере Гитлаб работает именно в контейнере, и за всё время после установки проблем с ним было — ноль. А как его удобно обновлять…
В нашем CI/CD будет больше, чем один контейнер, так что вместо привычного docker run ...
я запущу Гитлаб через docker-compose и добавлять последующие контейнеры буду уже туда.
Начальная версия docker-compose.yml
будет такая:
1 2 3 4 5 6 7 |
version: '2' services: gitlab: image: 'gitlab/gitlab-ce:latest' ports: - '80:80' |
docker-compose up -d gitlab
сделает всё магию, и через минуту докерных размышлений можно идти и настраивать пароль для root
аккаунта.
Итак, пароль есть, и мы внутри. Создаём новый проект:
Как только мы его создали, можно воспользоваться заботливо предложенными командами git remote add
и git push -u origin master
чтобы импортировать наш демо-проект в GitLab.
Ну что, проект внутри, можно переходить к первой стадии CI — компиляции.
Шаг 2: Настраиваем стадию «Build»
Чтобы убедить GitLab делать различные CI штуки сразу после того, как кто-нибудь в него сделал git push
, нужно дать ему две вещи: .gitlab-ci.yml
файл с инструкциями, что и в каком порядке делать, и какой-нибудь хост (реальный, виртуальный, контейнер — без разницы), который в терминологии Гитлаба будет называться «runner», и который, собственно, все эти инструкции будет выполнять. Начнём мы с файла.
.gitlab-ci.yml
Наша компиляции заключается в запуске TypeScript компилятора и натравливании его на index.ts. И желательно это сделать на хосте, на котором TypeScript компилятор установлен. Звучит просто, и так же просто описывается в .gitlab-ci.yml
:
1 2 3 4 5 6 7 8 9 |
stages: - build Compile: stage: build tags: - typescript script: - tsc index.ts |
Секция stages
описывает, какие стадии будут в нашем CI, , строка stage: build
говорит, что задача под названием Compile принадлежит стадии build, а tags
показывает, какие тэги должны быть на runner хосте, чтобы ему можно было эту задачу доверить.
Вообще, выдумывание имён тэгов (и стадий, и задач) весит целиком на нас. Я, например, в качестве тегов выбираю фичи, которые runner поддерживает, и потом уже в конкретных задачах расставляю теги-фичи, которые им нужны. Ну вроде msbuild14, dotnetcoresdk1.0.4, docfx, и т.п.
Если закоммитить .gitlab-ci.yml вместе с остальным демо-проектом в репозиторий, то в гитлабе произойдёт что-то отдалённо интересное. На странице «Pipelines» появится вот такая штука:
Гитлаб распознал, что ему скормили билд-задачу, постарался её запустить, но так как не было ни одного знакомого раннера (именно так я буду иногда называть runner-хосты) с тегом typescript, задача подвисла до лучших времён. Ну что же, лучшие времена определённо сейчас наступят.
Настраиваем gitlab-runner сервис
Знаете, что отличает обычный хост или контейнер от runner-хоста? Установленный сервис, который называется gitlab-runner, и больше ничего. Нам просто нужно скачать gitlab-runner с официального сайта (apt-get install
тоже есть), зарегистрировать его в нашем GitLab, повесить тег-другой, запустить, и всё, раннер готов.
Для простоты, я установлю runner в Docker контейнере и зарегистрирую как докеровский сервис в docker-compose.yml
. Таким образом он очутится в одной сети с GitLab и они смогут спокойно общаться. Вот как будет выглядеть Dockerfile, создающий образ с runner:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
FROM node RUN wget -O /usr/local/bin/gitlab-runner https://gitlab-ci-multi-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-ci-multi-runner-linux-amd64 && \ chmod +x /usr/local/bin/gitlab-runner && \ useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash && \ npm install -g typescript CMD gitlab-runner register \ -u http://gitlab/ci \ -r rSyUTfHxLL_qP7nYSfvA \ -n \ --executor shell \ --tag-list "typescript"\ --name "TypeScript Runner" && \ gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner && \ gitlab-runner start && \ tail -f /var/log/dpkg.log |
В качестве базового образа я взял nodejs, ведь нам ещё устанавливать TypeScript через npm. Почти весь код регистрации раннера я скопипастил с официального сайта. В секции RUN
лежит установка раннера и TypeScript, а уже регистрация и запуск пошли в CMD секцию. Немного уродливо, но работать будет. По крайней мере один раз.
В команде регистрации — gitlab-runner register
— есть четыре очень важных параметра:
-u
— туда передаётся адрес, на котором живёт GitLab сервер. Так как у нас docker-compose, и потому общая сеть, в качестве хоста можно передавать имя compose сервиса.-r
— токен регистрации, который знакомит раннер и гитлаб друг с другом. Для каждого экземпляра Гитлаба токен свой (он вроде и от проекта к проекту разный), и узнать его можно на странице «GitLab -> Settings -> CI/CD settings».--executor
объясняет каким образом интерпретировать и запускать команды из.gitlab-ci.yml
. В нашем случае там обычные шелл команды, так что и значение передалиshell
. Кстати, там ещё мог бы быть PowerShell для Windows, или вообще Docker..--tags
— в нашем случае — какие фичи поддерживает раннер. У нас установлен только TypeScript, вот его-то я в теги и передал.
Теперь, если добавить этот Dockerfile в docker-compose.yml
в качестве сервиса под названием runner-ts (и заодно положить его в одноимённую папку), и запустить его через docker-compose up -d runner-ts
, то та наша подвешенная задача отвиснет и таки выполнится.
1 2 3 4 5 6 7 8 |
# ... gitlab: image: 'gitlab/gitlab-ce:latest' ports: - '80:80' runner-ts: build: runner-ts/. # Dockerfile in runner-te folder |
Ломаем билд
Ради интереса, что будет если сломать билд, написав какую-нибудь ерунду в index.ts? Он же точно сломается, так ведь? Добавим-ка лишний пробел в ненужное место:
1 |
e => doc ument.querySelector('BODY').textContent = 'JS worked', |
Коммитим, ну и натурально следующий билд падает с живописным красным цветом:
На любые красные (и зелёные, да куда угодно) иконки можно кликнуть и узнать больше деталей.
Ну и конечно, заполучив исправленный index.ts, билд станет празднично зелёным:
Вот он какой, CI.
Шаг 3. Настраиваем стадию «Test»
Вот я абсолютно не в настроении настраивать в полночь какой-нибудь фреймуорк для тестирования, но мы вполне можем протестировать стиль кода — tslint никто ведь не отменял.
Стадия тестирования — ещё одна секция в .gitlab-ci.yml
и ещё один раннер. Мы могли бы просто доустановить tslint в существующий раннер, но я не люблю модифицировать существующие сервера. Лучше уж заменить, или новый добавить.
Сначала добавим ещё одну задачу в .gitlab-ci.yml
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
stages: - build - test Compile: stage: build #.... Test: stage: test tags: - typescript - tslint script: - tslint -c tslint.json index.ts |
Она идёт в отдельной test стадии, которая не начнётся, пока успешно не завершится build.
Теперь используем силу копипасты и создаём новый Dockerfile из предыдущего, но в этот раз c установленным tslint, и регистрируем его в качестве runner-tslint сервиса в существующий docker-compose.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
FROM node RUN wget -O /usr/local/bin/gitlab-runner https://gitlab-ci-multi-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-ci-multi-runner-linux-amd64 && \ chmod +x /usr/local/bin/gitlab-runner && \ useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash CMD npm install -g typescript && \ npm install -g tslint && \ gitlab-runner register \ -u http://gitlab/ci \ -r rSyUTfHxLL_qP7nYSfvA \ -n \ --executor shell \ --tag-list "typescript,tslint"\ --name "TSLint Runner" && \ gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner && \ gitlab-runner start && \ tail -f /var/log/dpkg.log |
Так как в наследство от предыдущего раннера нам достался typescript
, то его можно добавить в теги наравне с tslint, и в результате получится раннер, который может делать и то, и то.
Ну что, добавляем runner-tslint в docker-compose.yml
, запускаем через docker-compose up -d runner-tslint
и после коммита обновлённого .gitlab-ci.yml
любуемся на целых две стадии в нашей CI: Build и Test:
Как оказалось, tslint
оказался невысокого мнения о моих способностях в TypeScript и разродился целой кучей ошибок.
Пришлось делать ещё один коммит и исправлять.
Шаг 4. Добавляем стадию развёртывания
В CI/CD аббревиатуре последняя буква «D» подразумевает, что после успешной сборки и тестирования продукт можно где-нибудь и развернуть. Этим и займёмся.
Обычно релизы разворачивают как минимум в двух средах: staging — для тестирования, и production — для денег. На первую устанавливать новый билд вполне безопасно, она для того и создавалась. Выкатывать же новый релиз в продакшен стоит более осторожно. Памятуя это, в наш CI/CD можно добавить ещё два шага: автоматическую установку на staging, если билд удался, и ручную установку в production, если так решило начальство.
Раннер для развёртывания
Мне сразу было немного непонятно, как лучше сэмулировать два сервера и дать возможность их обновлять. Но потом решил сделать два nginx
контейнера, у которых папки с html
будут подключены к третьему контейнеру — раннеру развёртывания. Это будет просто ещё один gitlab-runner, на который даже компилятор устанавливать не надо:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
FROM ubuntu RUN apt-get update && apt-get install -y wget && \ apt-get install -y git && \ wget -O /usr/local/bin/gitlab-runner https://gitlab-ci-multi-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-ci-multi-runner-linux-amd64 && \ chmod +x /usr/local/bin/gitlab-runner CMD gitlab-runner register \ -u http://gitlab/ci \ -r rSyUTfHxLL_qP7nYSfvA \ -n \ --executor shell \ --tag-list "deploy,staging,production"\ --name "Deploy Runner" && \ gitlab-runner install --user=root --working-directory=/root && \ gitlab-runner start && \ tail -f /var/log/dpkg.log |
В этот раз я взял базовый образ с Убунтой и собрался запускать раннер-сервис под root аккаунтом. Если взять обычного пользователя, то у него будут проблемы с правом на запись.
Расшарить папки между несколькими контейнерами при помощи Docker volumes оказалось совсем не тяжело:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# ... runner-deploy: build: runner-deploy/. volumes: - staging:/www-staging - production:/www-production staging: image: 'nginx' ports: - '8081:80' volumes: - staging:/usr/share/nginx/html production: image: 'nginx' ports: - '8082:80' volumes: - production:/usr/share/nginx/html volumes: staging: production: |
Запустились новые контейнера тоже легко, так что можно перейти к .gitlab-ci.yml
.
1 2 3 |
docker-compose up -d runner-deploy docker-compose up -d staging docker-compose up -d production |
Задачи развёртывания в .gitlab-ci.yml
Есть один момент: распространять ведь мы будет не index.ts, а JavaScript файл, который из него получается. Так как каждая задача получает на вход чистую рабочую копию, то получить готовый index.js из стадии build просто так не выйдет. Тут можно либо ещё раз скомпилировать TS уже на стадии deploy, либо сделать результат компиляции в build артифактом, и тогда Гитлаб-таки начнёт копировать его между стадиями:
1 2 3 4 5 6 7 8 9 10 11 12 |
#... Compile: stage: build artifacts: name: "CompiledJS" paths: - ./*.js tags: - typescript script: - tsc index.ts #... |
Теперь вернёмся к развёртыванию. Я покажу сразу финальную версию .gitlab-ci.yml
, и там уже пройдусь по тому, что в неё добавилось:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
stages: - build - test - deploy Compile: stage: build artifacts: name: "CompiledTS" paths: - ./*.js tags: - typescript script: - tsc index.ts Test: stage: test tags: - typescript - tslint script: - tslint -c tslint.json index.ts Deploy-Staging: stage: deploy tags: - deploy - staging environment: name: Staging url: http://127.0.0.1:8081 script: - cp -f ./{index.js,index.html} /www-staging/ Deploy-Production: stage: deploy tags: - deploy - production environment: name: Production url: http://127.0.0.1:8082 script: - cp -f ./{index.js,index.html} /www-production/ when: manual |
Итак, появилась ещё одна стадия — deploy
, это понятно. Во-вторых, добавились две задачи — Deploy-Staging
и Deploy-Production
. Задачи развёртывания отличаются от обычных задач только секцией environment
, но даже и она не обязательна.
Имя для environment можно задавать любое и даже вычислять динамически. Например, на моём рабочем проекте это имена релизных веток. В этой же демке — просто слова «Staging» и «Production».
Опция url
— ссылка на то, где потом можно будет увидеть развёрнутый релиз.
Самая последняя строка — when: manual
— предсказуемо превращает Deploy-Production задачу из автоматической в ручную.
Коммитим изменённый .gitlab-ci.yml
ещё раз, делаем git push origin master
(а я ещё и пару ошибок исправил), и вот оно, CI/CD счастье:
Три зелёных чекбокса. Когда такое случается на работе — это праздник.
Можно кликнуть на детали билда и выяснить, что на самом деле только «Deploy-Staging» задача была выполнена. «Deploy-Production» нужно запускать вручную:
Что и где сейчас установлено можно узнать на странице «Environments». Те url
которые задавались в .gitlab-ci.yml, будут как раз здесь. Можно кликнуть и посмотреть релиз вживую на сервере:
Мораль
Просто задумайтесь, что мы только что сделали. Мы собрали с нуля настоящую CI/CD систему, которая на любой push в репозиторий реагирует целой серией шагов от компиляции до развёртывания, и всё это абсолютно автоматически. Если что-то пойдёт не так, то GitLab может прислать «письмо счастья». Если всё пройдёт хорошо, то он может послать код в релиз. Это просто прекрасно.
Конечно, можно заявить, что сделать CI/CD для hello-world это одно дело, а для реального проекта — совсем другое. Ну как тут сказать. У меня есть относительно большой живой проект, у которого только в master ветке полтора десятка задач (количество всё растёт) раскиданных по трём стадиям, семь Windows/Linux runner машин и три сценария развёртывания в Google Storage. А ещё есть полгода legacy веток, у которых CI правила хоть и похожие, но с нюансами. И не скажу, что настраивать это было сильно сложнее. Принципы ведь те же.
Отличная статья!познаю с Вами мир Docker!очень интересно читать ваши статьи!спасибо
Пожалуйста!
Спасибо за обзоры. Познавательно.
В секции «Задачи развёртывания в .gitlab-ci.yml», видимо, описка: пример кода из .gitlab-ci.yml. а не из docker-compose.yml.
Пожалуйста и спасибо. В следующей куске кода ещё и подсветка неправильная стояла. Всё подправил.
Какая-то странная штука..
У клиента билды не билд(ятся)…
Гугл опять привёл к тебе…
Интересно…
Это он в ответ на запрос «кто сломал мне билд?» привёл? Чесслово, меня в тот день даже в интернете не было.
В CentOS 7 возникает проблема на этапе «Настраиваем gitlab-runner сервис».
Контейнер с runner-ts не стартует.
docker-compose up runner-ts возвращает ошибку:
«runner-ts_1 | /bin/sh: 1: gitlab-runner: Exec format error
gitlab_runner-ts_1 exited with code 2»
Подскажите, куда копать? Файлы Docker-compose.yml и Dockerfile ровно по инструкции оформлены.
Со времён поста процесс установки раннера слегка изменился. Этот докерфайл должен сработать:
FROM centos:centos7
RUN curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh | bash
RUN GITLAB_RUNNER_DISABLE_SKEL=true yum install gitlab-runner -y
CMD gitlab-runner register \
-u http://gitlab/ci \
-r **************** \
-n \
—executor shell \
—tag-list «deploy,staging,production»\
—name «Deploy Runner» && \
tail -f /var/log/yum.log
Павел, здравствуйте!
очень пригодилась ваша статья! спасибо огромное:)
хотела только узнать, актуальны ли Dockerfile для последних версий linux?
Пожалуйста, Мария. Вполне может быть, что кусок кода, который скачивает гитлаб-раннер и регистрирует его слегка устарел. Я смутно помню, что кто-то комментил английскую версию моего поста, и мы нашли какие-то проблемы. Кажется, раннер можно теперь не скачивать как файл, а просто устанавливать через нормальный package manager. И регистрация вроде стала проще. Не помню, давно это было. Но принципы все те же, просто параметры чуть поменялись.