Русский
Русский
English
Статистика
Реклама

Gitlab-ci

Я сделал свой PyPI-репозитарий с авторизацией и S3. На Nginx

07.09.2020 16:04:06 | Автор: admin

В данной статье хочу поделится опытом работы с NJS, интерпретатора JavaScript для Nginx разрабатываемого в компании Nginx inc, описав на реальном примере его основные возможности. NJS это подмножество ЯП JavaScript, которое позволяет расширить функциональность Nginx. На вопрос зачем свой интерпретатор??? подробно ответил Дмитрий Волынцев. Если вкратце: NJS это nginx-way, а JavaScript более прогрессивный, родной и без GC в отличии от Lua.

A long time ago...

На прошлой работе в наследство мне достался gitlab с некоторым количеством разношерстых CI/CD-пайплайнов с docker-compose, dind и прочими прелестями, которые были переведены на рельсы kaniko. Образы же, которые использовались ранее в CI, переехали в первозданном виде. Работали они исправно до того дня, пока у нашего gitlab не сменился IP и CI превратился в тыкву. Проблема была в том, что в одном из docker-образов, участвовавшего в CI, был git, который по ssh тянул Python-модули. Для ssh нужен приватный ключ и ... он был в образе вместе с known_hosts. И любой CI завершался ошибкой проверки ключа из-за несовпадения реального IP и указанного в known_hosts. Из имеющихся Dockfile-ов был быстро собран новый образ и добавлена опция StrictHostKeyChecking no. Но неприятный привкус остался и появилось желание перенести либы в приватный PyPI-репозиторий. Дополнительным бонусом, после перехода на приватный PyPI, становился более простой пайплайн и нормальное описание requirements.txt

Выбор сделан, Господа!

Мы всё крутим в облаках и Kubernetes и в итоге хотелось получить небольшой сервис который представлял из себя stateless-контейнером с внешнем хранилищем. Ну а так как мы используем S3, то и приоритет был за ним. И по возможности с аутентификацией в gitlab (можно и самому дописать по необходимости).

Беглый поиск дал несколько результатов s3pypi, pypicloud и вариант с ручным созданием html-файлов для репы. Последний вариант отпал сам-собой.

s3pypi:Это cli для использования хостинга на S3. Выкладываем файлы, генерим html и заливаем в тот же бакет. Для домашнего использования подойдет.

pypicloud: Казался интересным проектом, но после прочтения доки пришло разочарование. Не смотря на хорошую документацию и возможности расширения под свои задачи, на деле оказался избыточен и сложный в настройке. Поправить код под свои задачи, по тогдашним прикидкам, заняло бы 3-5 дней. Так же сервису необходима БД. Оставили его на случай, если более ничего не найдем.

Более углубленный поиск дал модуль для Nginx, ngx_aws_auth. Результатом его тестирования стал XML отображаемый в браузере, по которому было видно содержимое бакета S3. Последний коммит, на момент поиска, был год назад. Репозиторий выглядел заброшенным.

Обратившись к первоисточнику и прочитав PEP-503 понял, что XML можно конвертировать в HTML налету и отдавать его pip. Ещё немного погуглив по словам Nginx и S3 наткнулся на пример аутентификации в S3 написанный на JS для Nginx. Так я познакомился с NJS.

Взяв за основу этотпример,через час наблюдал в своем браузере тот же XML, что и при использовании модуляngx_aws_auth, но написано уже все было на JS.

Решение на nginx мне очень нравилось. Во-первых хорошая документация и множество примеров, во-вторых мы получаем все плюшки Nginx по работе с файлами (из коробки), в-третьих любой человек умеющий писать конфиги для Nginx, сможет разобраться что к чему. Так же плюсом для меня является минимализм, по сравнению с Python или Go(если писать с нуля), не говоря уж об nexus.

TL;DR Через 2 дня тестовая версия PyPi уже была использована в CI.

Как это работает?

В Nginx подгружается модуль ngx_http_js_module, включен в официальный docker-образ. Импортируем наш скрипт c помощью директивы js_importв конфигурацию Nginx. Вызов функции осуществляется директивойjs_content. Для установки переменных используется директива js_set, которая аргументом принимает только функцию описанную в скрипте. А вот выполнять подзапросы в NJS мы можем только с помощью Nginx, ни каких Вам тамXMLHttpRequest. Для этого в конфигурации Nginx должен быть добавлен соответствующий локейшн. А в скрипте должен быть описан подзапрос (subrequest) к этому локейшену.Чтобы иметь возможность обратиться к функции из конфига Nginx, в самом скрипте имя функции необходимо экспортировать export default.

nginx.conf

load_module modules/ngx_http_js_module.so;http {  js_import   imported_name  from script.js;server {  listen 8080;  ...  location = /sub-query {    internal;    proxy_pass http://upstream;  }  location / {    js_contentimported_name.request;  }}

script.js

function request(r) {  function call_back(resp) {    // handler's code    r.return(resp.status, resp.responseBody);  }  r.subrequest('/sub-query', { method: r.method }, call_back);}export default {request}

При запросе в браузере http://localhost:8080/ мы попадаем в location /в котором директива js_content вызывает функцию request описанную в нашем скрипте script.js. В свою очередь в функции request осуществляется подзапрос к location = /sub-query, с методом (в текущем примере GET) полученным из аргумента (r), неявно передаваемым при вызове этой функции. Обработка ответа подзапроса будет осуществлена в функции call_back.

Пробуем S3

Чтобы сделать запрос к приватному S3-хранилищу, нам необходимы:

ACCESS_KEY

SECRET_KEY

S3_BUCKET

Из используемого http-метода, текущая дата/время,S3_NAME и URI генерируется определенного вида строка, которая подписывается (HMAC_SHA1) с помощьюSECRET_KEY. Далее строку, вида AWS $ACCESS_KEY:$HASH, можно использовать в заголовке авторизации. Та же дата/время, что была использована для генерации строки на предыдущем шаге, должна быть добавлена взаголовок X-amz-date.В коде это выглядит так:

nginx.conf

load_module modules/ngx_http_js_module.so;http {  js_import   s3      from     s3.js;  js_set      $s3_datetime     s3.date_now;  js_set      $s3_auth         s3.s3_sign;server {  listen 8080;  ...  location~* /s3-query/(?<s3_path>.*) {    internal;    proxy_set_header    X-amz-date     $s3_datetime;    proxy_set_header    Authorization  $s3_auth;    proxy_pass          $s3_endpoint/$s3_path;}  location~ "^/(?<prefix>[\w-]*)[/]?(?<postfix>[\w-\.]*)$" {    js_content s3.request;  }}

s3.js(пример авторизации AWS Sign v2, переведена в статус deprecated)

var crypt = require('crypto');var s3_bucket = process.env.S3_BUCKET;var s3_access_key = process.env.S3_ACCESS_KEY;var s3_secret_key = process.env.S3_SECRET_KEY;var _datetime = new Date().toISOString().replace(/[:\-]|\.\d{3}/g, '');function date_now() {  return _datetime}function s3_sign(r) {  var s2s = r.method + '\n\n\n\n';  s2s += `x-amz-date:${date_now()}\n`;  s2s += '/' + s3_bucket;  s2s += r.uri.endsWith('/') ? '/' : r.variables.s3_path;  return `AWS ${s3_access_key}:${crypt.createHmac('sha1', s3_secret_key).update(s2s).digest('base64')}`;}function request(r) {  var v = r.variables;  function call_back(resp) {    r.return(resp.status, resp.responseBody);  }  var _subrequest_uri =r.uri;  if (r.uri ==='/') {    // root    _subrequest_uri = '/?delimiter=/';  } else if (v.prefix !== '' && v.postfix === '') {    // directory    var slash = v.prefix.endsWith('/') ? '' : '/';    _subrequest_uri = '/?prefix=' + v.prefix + slash;}  r.subrequest(`/s3-query${_subrequest_uri}`, { method: r.method }, call_back);}export default {request,s3_sign,date_now}

Немного пояснения про _subrequest_uri: это переменная которая в зависимости от изначального uri формирует запрос к S3. Если нужно получить содержимое корня, в таком случае необходимо сформировать uri-запрос с указанием разделителя delimiter, который вернет список всех xml-элементов CommonPrefixes, что соответствует директориям (в случае с PyPI, список всех пакетов). Если нужно получить список содержимого в определенной директории (список всех версий пакетов), тогда uri-запрос должен содержатьполе prefix с именем директории (пакета) обязательно заканчивающийся на слэш /. В противном случае возможны коллизии при запросе содержимого директории, например. Есть директории aiohttp-request иaiohttp-requests и если в запросе будет указано /?prefix=aiohttp-request, тогда в ответе будет содержимое обеих директорий. Если же на конце будет слэш,/?prefix=aiohttp-request/, то в ответе будет только нужная директория. И если мы запрашиваем файл, то результирующий uri не должен отличать от изначального.

Сохраняем, перезапускаем Nginx. В браузере вводим адрес нашего Nginx, результатом работы запроса будет XML, например:

Список директорий
<?xml version="1.0" encoding="UTF-8"?><ListBucketResult xmlns="http://personeltest.ru/away/s3.amazonaws.com/doc/2006-03-01/">  <Name>myback-space</Name>  <Prefix></Prefix>  <Marker></Marker>  <MaxKeys>10000</MaxKeys>  <Delimiter>/</Delimiter>  <IsTruncated>false</IsTruncated>  <CommonPrefixes>    <Prefix>new/</Prefix>  </CommonPrefixes>  <CommonPrefixes>    <Prefix>old/</Prefix>  </CommonPrefixes></ListBucketResult>

Из списка директорий понадобятся только элементыCommonPrefixes.

Добавив, в браузере, к нашему адресу нужную нам директорию, получим ее содержимое так же в виде XML:

Список файлов в директории
<?xml version="1.0" encoding="UTF-8"?><ListBucketResult xmlns="http://personeltest.ru/away/s3.amazonaws.com/doc/2006-03-01/">  <Name>myback-space</Name>  <Prefix>old/</Prefix>  <Marker></Marker>  <MaxKeys>10000</MaxKeys>  <Delimiter></Delimiter>  <IsTruncated>false</IsTruncated>  <Contents>    <Key>old/giphy.mp4</Key>    <LastModified>2020-08-21T20:27:46.000Z</LastModified>    <ETag>&#34;00000000000000000000000000000000-1&#34;</ETag>    <Size>1350084</Size>    <Owner>      <ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID>      <DisplayName></DisplayName>    </Owner>    <StorageClass>STANDARD</StorageClass>  </Contents>  <Contents>    <Key>old/hsd-k8s.jpg</Key>    <LastModified>2020-08-31T16:40:01.000Z</LastModified>    <ETag>&#34;b2d76df4aeb4493c5456366748218093&#34;</ETag>    <Size>93183</Size>    <Owner>      <ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID>      <DisplayName></DisplayName>    </Owner>    <StorageClass>STANDARD</StorageClass>  </Contents></ListBucketResult>

Из списка файлов возьмем только элементыKey.

Остается полученный XML распарсить и отдать в виде HTML, предварительно заменив заголовок Content-Type на text/html.

function request(r) {  var v = r.variables;  function call_back(resp) {    var body = resp.responseBody;    if (r.method !== 'PUT' && resp.status < 400 && v.postfix === '') {      r.headersOut['Content-Type'] = "text/html; charset=utf-8";      body = toHTML(body);    }    r.return(resp.status, body);  }    var _subrequest_uri =r.uri;  ...}function toHTML(xml_str) {  var keysMap = {    'CommonPrefixes': 'Prefix',    'Contents': 'Key',  };  var pattern = `<k>(?<v>.*?)<\/k>`;  var out = [];  for(var group_key in keysMap) {    var reS;    var reGroup = new RegExp(pattern.replace(/k/g, group_key), 'g');    while(reS = reGroup.exec(xml_str)) {      var data = new RegExp(pattern.replace(/k/g, keysMap[group_key]), 'g');      var reValue = data.exec(reS);      var a_text = '';      if (group_key === 'CommonPrefixes') {        a_text = reValue.groups.v.replace(/\//g, '');      } else {        a_text = reValue.groups.v.split('/').slice(-1);      }      out.push(`<a href="http://personeltest.ru/aways/habr.com/${reValue.groups.v}">${a_text}</a>`);    }  }  return '<html><body>\n' + out.join('</br>\n') + '\n</html></body>'}

Пробуем PyPI

Проверяем, что ни где и ни чего не ломается на заведомо рабочих пакетах.

# Создаем для тестов новое окружениеpython3 -m venv venv. ./venv/bin/activate# Скачиваем рабочие пакеты.pip download aiohttp# Загружаем в приватную репуfor wheel in *.whl; do curl -T $wheel http://localhost:8080/${wheel%%-*}/$wheel; donerm -f *.whl# Устанавливаем из приватной репыpip install aiohttp -i http://localhost:8080

Повторяем с нашими либами.

# Создаем для тестов новое окружениеpython3 -m venv venv. ./venv/bin/activatepip install setuptools wheelpython setup.py bdist_wheelfor wheel in dist/*.whl; do curl -T $wheel http://localhost:8080/${wheel%%-*}/$wheel; donepip install our_pkg--extra-index-url http://localhost:8080

В CI, создание и загрузка пакета выглядит так:

pip install setuptools wheelpython setup.py bdist_wheelcurl -sSfT dist/*.whl -u "gitlab-ci-token:${CI_JOB_TOKEN}" "https://pypi.our-domain.com/${CI_PROJECT_NAME}"

Аутентификация

В Gitlab возможно использовать JWT для аутентификации/авторизации внешних сервисов. Воспользовавшись директивой auth_request в Nginx, перенаправим аутентификационные данные в подзапрос содержащий вызов функции в скрипте. В скрипте будет сделан ещё один подзапрос на url Gitlab-а и если аутентификационные данные указаны были верно, то Gitlab вернет код 200 и будет разрешена загрузка/скачивание пакета. Почему не воспользоваться одним подзапросом и сразу не отправить данные в Gitlab? Потому, что придется тогда править файл конфигурации Nginx каждый раз, как у нас будут какие-то изменения в авторизации, а это достаточно муторное занятие. Так же, если в Kubernetes используется политика read-only root filesystem, то это ещё больше добавляет сложностей при подмене nginx.conf через configmap. И становится абсолютно невозможна конфигурация Nginx через configmap при одновременном использованииполитикзапрещающих подключение томов (pvc) иread-only root filesystem (такое тоже бывает).

Используя промежуточным звеном NJS, мы получаем возможность менять указанные параметры в конфиге nginx с помощью переменных окружения и делать какие-нибудь проверки в скрипте (например, неверно указанного URL).

nginx.conf

location = /auth-provider {  internal;  proxy_pass $auth_url;}location = /auth {  internal;  proxy_set_header Content-Length "";  proxy_pass_request_body off;  js_content auth.auth;}location ~ "^/(?<prefix>[\w-]*)[/]?(?<postfix>[\w-\.]*)$" {  auth_request /auth;  js_content s3.request;}

s3.js

var env = process.env;var env_bool = new RegExp(/[Tt]rue|[Yy]es|[Oo]n|[TtYy]|1/);var auth_disabled  = env_bool.test(env.DISABLE_AUTH);var gitlab_url = env.AUTH_URL;function url() {  return `${gitlab_url}/jwt/auth?service=container_registry`}function auth(r) {  if (auth_disabled) {    r.return(202, '{"auth": "disabled"}');    return null}  r.subrequest('/auth-provider',                {method: 'GET', body: ''},                function(res) {                  r.return(res.status, "");                }  );}export default {auth, url}

Скорее всего назревает вопрос: -А почему бы не использовать готовые модули? Там ведь всё уже сделано! Например, var AWS = require('aws-sdk')и не надо писать"велосипед" с S3-аутентификацией!

Перейдем к минусам

Для меня, невозможность импортирование внешние JS-модулей, стало неприятной, но ожидаемой особенностью. Описанный в примере выше require('crypto'), это build-in-модули и require работает только для них. Так же нет возможности переиспользовать код из скриптов и приходится копи-пастить его по разным файлам. Надеюсь, что когда-нибудь этот функционал будет реализован.

Так же для текущего проекта в Nginx должно быть отключено сжатие gzip off;

Потому, что нет gzip-модуля в NJS и подключить его невозможно, соотвественно нет возможности работать с сжатыми данными. Правда, не особо это и минус для данного кейса. Текста не много, а передаваемые файлы уже сжатые и дополнительное сжатие им не особо поможет. Так же это не на столько нагруженный или критичный сервис, чтобы заморачиваться с отдачей контента на несколько миллисекунд быстрее.

Отладка скрипта долгая и возможна только через принты в error.log. В зависимости от выставленного уровня логирования info, warn или error возможно использовать 3 метода r.log, r.warn, r.error соответственно. Некоторые скрипты пытаюсь отлаживать в Chrome (v8) или консольной тулзе njs, но не все возможно там проверить. При отладке кода, ака функциональное тестирование, history выглядит примерно так:

docker-compose restart nginxcurl localhost:8080/docker-compose logs --tail 10 nginx

и таких последовательностей может быть сотни.

Написание кода с использование подзапросов и переменных для них, превращается в запутанный клубок. Иногда начинаешь метаться по разным окнам IDE пытаясь разобраться в последовательности действий твоего кода. Это не сложно, но иногда сильно напрягает.

Нет полноценной поддержки ES6.

Может есть и ещё какие-то недостатки, но ни с чем я более не сталкивался. Поделитесь инфой если у Вас есть отрицательный опыт эксплуатации NJS.

Заключение

NJS - легковесный open-source интерпретатор, позволяющий реализовать в Nginx разные сценарии на ЯП JavaScript. При его разработке было уделено большое внимание производительности. Конечно много чего в нём ещё не хватает, но проект развивается силами небольшой команды и они активно добавляют новые фичи и фиксят баги. Я же надеюсь, что когда-нибудь NJS позволит подключать внешние модули, что сделает функционал Nginx практически неограниченным. Но есть NGINX Plus и каких-то фич скорее всего не будет!

Репозиторий с полным кодом к статье

njs-pypi с поддержкой AWS Sign v4

Описание директив модуля ngx_http_js_module

Официальный репозиторий NJS и документация

Примеры использования NJS от Дмитрия Волынцева

njs - родной JavaScript-скриптинг в nginx / Выступление Дмитрия Волныева на Saint HighLoad++ 2019

NJS в production / Выступление Василия Сошникова на HighLoad++ 2019

Подпись и аутентификация REST-запросов в AWS

Подробнее..
Категории: Javascript , Development , Devops , Nginx , Pip , Gitlab-ci , Scripting , Pypi , Aws s3

GitOps очередной модный термин или прорыв в автоматизации?

06.10.2020 12:23:44 | Автор: admin

Большинство из нас, подмечая очередной новый термин в IT блогосфере или конференции, рано или поздно задается подобным вопросом: Что это? Очередное модное слово, buzzword или действительно что-то стоящее пристального внимания, изучения и обещающее новые горизонты? Точно также вышло у меня и с термином GitOps некоторое время назад. Вооружившись множеством уже существующих статей, а также знанием коллег из компании GitLab, я попытался разобраться, что же это за зверь, и как его применение может выглядеть на практике.

Кстати, о новизне термина GitOps также говорит недавно проведенный нами опрос: более половины опрошенных еще не начинали работы с его принципами.

Итак, проблема управления инфраструктурой не нова. Множество облачных провайдеров уже добрый десяток лет доступны широкой публике и, казалось бы, должны были сделать работу отвечающих за инфраструктуру команд простой и незатейливой. Однако при сравнении с процессом разработки приложений (где уровень автоматизации достигает все новых и новых горизонтов), инфраструктурные проекты все еще зачастую включают множество выполняемых вручную задач и требуют особых знаний и специалистов, особенно с учетом современных требований к отказоустойчивости, гибкости, масштабируемости и эластичности.

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

Так в чем собственно отличие GitOps от IaC? Именно с этого вопроса я и начал свое расследование. Пообщавшись с коллегами, у меня получилось выработать следующее сравнение:

GitOps

IaC

Весь код хранится в git репозитории

Версионность кода необязательна

Декларативное описание кода / Идемпотентность

Допустимо как декларативное, так и императивное описание

Изменения вступают в силу с использованием механизмов Merge Request / Pull Request

Согласование, утверждение и коллаборация необязательны

Процесс выката обновлений автоматизирован

Процесс выката обновлений не нормирован (автоматический, ручной, копирование файлов, с использованием командной строки и т.д.)

Иными словами GitOps появился на свет именно благодаря применению принципов IaC. Во-первых, инфраструктуру и конфигурации теперь можно было хранить точно так же как и приложения. Код легко хранить, им легко делиться, сравнивать, пользоваться возможностями версионности. Версии, ветки, история. И все это в общедоступном для всей команды месте. Поэтому вполне закономерным развитием стало использования систем контроля версий. В частности, git, как наиболее популярного.

С другой стороны, появилась возможность автоматизировать процессы управления инфраструктурой. Теперь это можно сделать быстрее, надежнее и дешевле. Тем более принципы CI / CD уже были известны и популярны среди разработчиков программного обеспечения. Необходимо было только перенести и применить уже известные знания и навыки в новую область. Эти практики однако выходили за рамки стандартного определения Инфраструктуры как кода, отсюда и родилось понятие GitOps.

Любопытность GitOps, конечно, еще и в том, что это не продукт, плагин или платформа, связанная с каким бы то ни было вендором. Это скорее парадигма и набор принципов, аналогично с другим знакомым нам термином: DevOps.

В компании GitLab мы выработали два определения этого нового термина: теоретический и практически. Начнем с теоретического:

GitOps - это методология, которая использует передовые принципы DevOps, используемые для разработки приложений, такие как контроль версий, взаимодействие, согласование, CI/CD, и применяет их для решения задач по автоматизации управления инфраструктурой.

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

С практической же точки зрения мы описываем GitOps следующим образом:

GitOps = IaC + MRs + CI/CD

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

Merge Request (альтернативное название Pull Request). В плане процесса MR - это запрос на применение изменений кода и последующее слияние веток. Но в плане инструментов, которыми мы пользуемся - это скорее возможность для получения полной картины всех вносимых изменений: не только code diff, собранный из какого-то количества коммитов, но и контекст, результаты тестов, конечный ожидаемый результат. Если мы говорим про инфраструктурный код, то нам интересно, как именно изменится инфраструктура, сколько новых ресурсов будет добавлено или удалено, изменено. Желательно в каком-то более удобном и легко читаемом формате. В случае с облачными провайдерами неплохо было бы знать, какие финансовые последствия понесет это изменение.

Но MR - это еще и средство совместной работы, взаимодействия, общения. То место, где вступает в действие система сдержек и противовесов. От простых комментариев до формальных одобрений и утверждений.

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

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

А если вам вдруг станет интересно, как это все выглядит на практике, то приглашаю посмотреть наш мастер-класс, в котором я пошагово рассказываю, как при помощи GitLab:

  • Реализовать основные принципы GitOps

  • Создавать и вносить изменения в облачную инфраструктуру (на примере Yandex Cloud)

  • Автоматизировать обнаружение дрейфа системы от желаемого состояния при помощи активного мониторинга

http://personeltest.ru/aways/bit.ly/34tRpwZhttps://bit.ly/34tRpwZ
Подробнее..

Стенды разработки без очередей и простоев

23.03.2021 08:12:20 | Автор: admin

Цель статьи - показать один из возможных подходов для организации гибкого развёртывания dev/test стендов. Показать какие преимущества предоставляет нам IaC подход в сочетании с современными инструментами.


Предыстория

Имеется несколько стендов для разработчиков - devs, tests, production. Новые версии компонентов продукта появляются несколько раз в день.

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

Создание дополнительных статичных стендов решит проблему, но приведёт к их переизбытку во время снижения активности разработчиков и, как следствие, повысит затраты компании на инфраструктуру.

Задача

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

Стек

Gitlab CI, Terraform, Bash, любое приватное/публичное облако.

Технические сложности:

  1. Terraform state file - из коробки у нас нет возможности использовать переменную в значенииимени state файла. Нужно что-то придумывать или использовать другой продукт.

  2. Subnets - каждое новое окружение должно быть создано в изолированной подсети. Нужно контролировать свободные/занятые подсети, некий аналог DHCP, но для подсетей.

Алгоритм

  1. Gitlab CI выполняет pipeline. Связывает все остальные компоненты воедино.

  2. Terraform создаёт и удаляет экземпляры виртуальных машин.

  3. Configuration manager(CM) - разворачивает сервисы.

  4. Bash сценарии подготавливают конфигурационные файлы специфичные для каждого стенда.

Структура репозитория

development-infrastructure/  deploy/    env1/      main.tf      backend.tf     ansible-vars.json       subnets.txt     env2/    ...  cm/    ...  modules/    azure/      main.tf      variables.tf  scripts/    env.sh    subnets.txt   .gitlab-ci.yml
  • deploy - содержит специфичные для каждого окружения файлы - файл с переменными для terraform и CM, файл содержащий адрес подсети стенда.

  • cm - в моём случае, тут хранятся Ansible плейбуки для настройки ОС и разворачивания сервисов.

  • modules - модули terraform которые будут получать в качестве параметров имя окружения и адрес подсети

  • scripts - bash сценарии для создания и удаления стендов и их конфигураций

.gitlab-ci.yml:

stages:  - create environment  - terraform apply  - cm  - destroy environment .template: variables: ENV: $NAME_ENV  when: manual tags: [cloudRunner01]  only:   refs:    - triggers Create environment:  stage:create environment  extends: .template script:   - ./scripts/create_env.sh -e $ENV -a create  artifacts:   paths:    - deploy/${ENV}/backend.tf    - deploy/${ENV}/main.tf    - deploy/${ENV}/vars.json Create instances:  stage: terraform apply extends: .template  script:   - cd ./deploy/$ENV   - terraform init -input=false   - terraform validate   - terraform plan -input=false -out=tf_plan_$ENV   - terraform apply -input=false tf_plan_$ENV Deploy applications:  stage: cm  extends: .template  script:   - # мы можем передать имя окружения в качестве параметра нашей CM   - # в моём случае, на основе переменной $ENV генерируются сертификаты,   - # конфигурируется обратный прокси сервер и т.п.   - # также мы можем использовать данные из terraform Destroy instances and environment:  stage:destroy environment  extends: .template  script:   - cd ./deploy/$ENV   - terraform init -input=false   - terraform destroy -auto-approve   - ./scripts/delete_env.sh -e $ENV -a delete 

Остановимся подробнее на каждом шаге нашего пайплайна:

  • Create environment - на основе имени окружения, полученного из переменно NAME_ENV, создаём уникальные для окружения файлы, после чего помещаем их в наш git репозиторий.

  • Create instances - создаём инстансы(виртуальные машины) и подсети, которые будут использоваться окружением.

  • Deploy applications - разворачиваем наши сервисы с помощью любимого Configuration Manager.

  • Destroy instances and environment - с помощью bash сценария данный шаг удалит наши инстансы, после чего удалит из репозитория каталог с файлами окружения. Освободившаяся подсеть будет возвращена в файл scripts/subnets.txt.

Запуск пайплайна происходит с объявлением переменной NAME_ENV, содержащей имя нового стенда:

Разработчики не имеют доступа к Git репозиторию и могут вносить в него изменения только через запуск pipeline.

modules/base/main.tf:

# лучшие практики и личный опыт настоятельно рекомендуют фиксировать версию провайдера provider "azurerm" { version = "=1.39.0" }  # создаём новую группу ресурсов, это особенность Azure. Имя группы уникально, в этом случае будет удобно использовать имя окружения resource "azurerm_resource_group" "product_group" { name= "${var.env_name}" location = "East Europe" } # создаем сеть resource "azurerm_virtual_network" "vnet" { name= "product-vnet" resource_group_name = azurerm_resource_group.product_group.name location= azurerm_resource_group.product_group.location address_space= [var.vnet_address] } #используем подсеть полученную с помощью bash сценария resource "azurerm_subnet" "subnet" { name= "product-subnet" resource_group_name= azurerm_resource_group.product_group.name virtual_network_name = azurerm_virtual_network.vnet.name address_prefix= var.subnet_address } # теперь можем создать виртуальную машинуresource "azurerm_virtual_machine" "product_vm" { name= "main-instance" location= azurerm_resource_group.product_group.location resource_group_name= azurerm_resource_group.product_group.name network_interface_ids = [azurerm_network_interface.main_nic.id]   } # прочие ресурсы и виртуальные машины... 

Чтобы сократить листинг я убрал большую часть обязательных, но, в нашем случае, неважных ресурсов.

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

scripts/env.sh:

#!/bin/bash set -eu CIDR="24" DEPLOY_DIR="./deploy" SCRIPT_DIR=$(dirname "$0") usage() { echo "Usage: $0 -e [ENV_NAME] -a [create/delete]" echo "-e: Environment name" echo "-a: Create or delete" echo "-h: Help message" echo "Examples:" echo "$0 -e dev-stand-1 -a create" echo "$0 -e issue-1533 -a delete" } while getopts 'he:a:' opt; do case "${opt}" in e) ENV_NAME=$OPTARG ;; a) ACTION=$OPTARG ;; h) usage; exit 0 ;; *) echo "Unknown parameter"; usage; exit 1;; esac done if [ -z "${ENV_NAME:-}" ] && [ -z "${ACTION:-}" ]; then usage exit 1 fi # приводим имя окружения к нижнему регистру ENV_NAME="${ENV_NAME,,}" git_push() { git add ../"${ENV_NAME}" case ${1:-} in create) git commit -am "${ENV_NAME} environment was created" git push origin HEAD:"$CI_COMMIT_REF_NAME" -o ci.skip echo "Environment ${ENV_NAME} was created.";; delete) git commit -am "${ENV_NAME} environment was deleted" git push origin HEAD:"$CI_COMMIT_REF_NAME" -o ci.skip echo "Environment ${ENV_NAME} was deleted.";; esac } create_env() { # создаём каталог для нового окружения if [ -d "${DEPLOY_DIR}/${ENV_NAME}" ]; then echo "Environment ${ENV_NAME} exists..." exit 0 else mkdir -p ${DEPLOY_DIR}/"${ENV_NAME}" fi # получаем адрес подсети NET=$(sed -e 'a$!d' "${SCRIPT_DIR}"/subnets.txt) sed -i /"$NET"/d "${SCRIPT_DIR}"/subnets.txt echo "$NET" > ${DEPLOY_DIR}/"${ENV_NAME}"/subnets.txt if [ -n "$NET" ] && [ "$NET" != "" ]; then echo "Subnet: $NET" SUBNET="${NET}/${CIDR}" else echo "There are no free subnets..." rm -r "./${DEPLOY_DIR}/${ENV_NAME}" exit 1 fi   pushd "${DEPLOY_DIR}/${ENV_NAME}" || exit 1 # Создаем main.tf terraform файл с нужными нам переменными нового окружения cat > main.tf << END module "base" { source = "../../modules/azure" env_name = "${ENV_NAME}" vnet_address = "${SUBNET}" subnet_address = "${SUBNET}" } END # Cоздаём backend.tf terraform файл , в котором указываем имя нового state файла cat > backend.tf << END terraform { backend "azurerm" { storage_account_name = "terraform-user" container_name = "environments" key = "${ENV_NAME}.tfstate" } } END } delete_env() { # удаляем каталог окружения и высвобождаем подсеть if [ -d "${DEPLOY_DIR}/${ENV_NAME}" ]; then NET=$(sed -e '$!d' ./${DEPLOY_DIR}/"${ENV_NAME}"/subnets.txt) echo "Release subnet: ${NET}" echo "$NET" >> ./"${SCRIPT_DIR}"/subnets.txt pushd ./${DEPLOY_DIR}/"${ENV_NAME}" || exit 1 popd || exit 1 rm -r ./${DEPLOY_DIR}/"${ENV_NAME}" else echo "Environment ${ENV_NAME} does not exist..." exit 1 fi } case "${ACTION}" in create) create_env git_push "${ACTION}" ;; delete) delete_env git_push "${ACTION}" ;; *) usage; exit 1;; esac 
  1. Сценарий env.shпринимает два параметра - имя окружения и действие(создание\удаление).

  2. При создании нового окружения:

  3. В каталоге DEPLOY_DIRсоздаётся директория с именем окружения.

  4. Из файла scripts/subnets.txt мы извлекаем одну свободную подсеть.

  5. На основе полученных данных генерируем конфигурационные файлы для Terraform.

  6. Для фиксации результата отправляем каталог с файлами окружения в git репозиторий.

  7. При удалении -сценарий удаляет каталог с файлами окружения и возвращает освободившуюся подсеть в файл scripts/subnets.txt

scripts/subnets.txt:

172.28.50.0172.28.51.0172.28.52.0...

В данном файле мы храним адреса наши подсетей. Размер подсети определяется переменной CIDR в файлеscripts/create_env.sh

Результат

  1. Мы получили фундамент который позволяет нам развернуть новый стенд путём запуска piplinea в Gitlab CI.

  2. Снизили затраты нашей компании на инфраструктуру.

  3. Разработчики не простаивают и могут создавать и удалять стенды когда им это потребуется.

  4. Также получили возможность создания виртуальных машин в любом облаке написав новый модуль Terraform и немного модифицировав сценарий созданиях\удаления окружений

  5. Можем поиграться с триггерами Gitlabи разворачивать новые стенды из пайплайнов команд разработчиков передавая версии сервисов.

Подробнее..

Code review в Gitlab CE если Merge request approvals нет, но очень хочется

24.06.2020 18:08:11 | Автор: admin

Одной из самых нужных функций, которой нет в бесплатной версии GitLab, является возможность контролировать Merge request (MR), используя обязательный code review.

Сделаем минимальный функционал сами запретим Merge, пока несколько разработчиков не поставят палец вверх на MR.



Зачем это вообще?



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

В итоге приходится:
  • либо совсем запрещать Merge в защищенные ветки для части разработчиков, но тогда разработчики, имеющие право на Merge, получают конфликты при слиянии чужих MR как бонус;
  • либо давать возможность делать бесконтрольные слияния с вашей мастер-веткой без code review, даже если это Junior, устроившийся только вчера.


Первым же делом отправился гуглить, полагая, что уж точно кто-то что-то подобное уже сделал (без доработки кода), но оказалось, что подобной реализации в community версии еще не было.

Общая схема работы



В качестве примера настроим Merge request approvals на тестовом репозитории myapp
gitlab.com/gitlab-ce-mr-approvals/myapp

  1. Создадим токен для доступа к API GitLab (через него будем получать информацию о количестве голосов за и против)
  2. Добавим токен в переменные GitLab
  3. Запретим Merge при ошибках в пайплайне (если голосов за недостаточно)
  4. Настроим проверку голосов как часть пайплайна CI/CD
  5. Запретим делать коммиты в защищенные ветки, все изменения проводим только через MR
  6. Проверим, что получилось в итоге


1. Создаем токен для доступа к API



Заходим в Настройки пользователя Токены доступа и записываем токен


Учетная запись для получения токена
Доступ к API позволяет делать практически все с вашими репозиториями, поэтому советую создать отдельную учетную запись Gitlab, дать ей минимальные права на ваши репозтории (например, Reporter) и получить токен для этой учетной записи.



2. Добавляем токен в переменные Gitlab



Например, на предыдущем шаге мы получили токен QmN2Y0NOUFlfeXhvd21ZS01aQzgK

Открываем Настройки CI/CD Переменные Добавить переменную GITLAB_TOKEN_FOR_CI



В итоге получим:



Это можно сделать как на одном репозитории, так и на группе репозиториев.

3. Ставим запрет на Merge, если не получены одобрения коллег после проведенного code review



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

Заходим в Настройки Основные Запросы на слияние Проверки слияния и включаем опцию Сборочные линии должны успешно выполниться


4. Настраиваем пайплайн



Если вы еще не делали CI/CD конвейер для вашего приложения
Создаем в корне репозитория файл .gitlab-ci.yml с простейшим содержанием

stages:  - build  - testvariables:  NEED_VOTES: 1include:  - remote: "https://gitlab.com/gitlab-ce-mr-approvals/ci/-/raw/master/check-approve.gitlab-ci.yml"run-myapp:  stage: build  script: echo "Hello world"




Отдельный репозиторий для конфигурации CI/CD
Я бы рекомендовал сделать отдельный репозиторий, в котором необходимо создать файл myapp.gitlab-ci.yml для настройки конвейера. Так вы сможете лучше контролировать доступ участников, которые могут изменить конвейер сборки и получить токен доступа.

Расположение нового файла конвейера нужно будет указать, зайдя в репозиторий myapp Настройки CI/CD Сборочные линии Пользовательский путь конфигурации CI указать новый файл, например myapp-ci.gitlab-ci.yml@gitlab-ce-mr-approvals/ci


Совет: используйте линтер для внесения изменений в файлы пайплайна
Даже если вы работаете один, хорошим помощником выступит работа через MR, прогоняя все ваши изменения файлов пайплайна через линтер. Если вы ошибетесь в синтаксисе YAML-файла, это не даст вам сломать рабочий конвейер, а просто заблокирует Merge.

Пример контейнеров с линтерами, которые вы можете встроить в ваш пайплайн:
hub.docker.com/r/gableroux/gitlab-ci-lint
hub.docker.com/r/sebiwi/gitlab-ci-validate

И пример стадии проверки:

stages:  - lintlint:  image: sebiwi/gitlab-ci-validate:1.3.0  variables:    GITLAB_HOST: https://gitlab.com  script:    - CI_FILES=(./*.yml)    - for f in "${CI_FILES[@]}"; do        gitlab-ci-validate $f;      done;




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

stages:
- test

variables:
NEED_VOTES: 1

include:
- remote: "https://gitlab.com/gitlab-ce-mr-approvals/ci/-/raw/master/check-approve.gitlab-ci.yml"


Переменная NEED_VOTES определяет сколько пальцев вверх должно быть у MR, чтобы был доступен Merge. Значение, равное единице, означает, что вы сами можете одобрить свой MR, лайкнув его.

include подключает стадию test, проверяющую количество лайков.

Простейший пайплайн на примере myapp.gitlab-ci.yml
stages:
- build
- test

variables:
NEED_VOTES: 0

include:
- remote: "https://gitlab.com/gitlab-ce-mr-approvals/ci/-/raw/master/check-approve.gitlab-ci.yml"

run-myapp:
stage: build
image: openjdk
script:
- echo CI_MERGE_REQUEST_TARGET_BRANCH_NAME $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
- java HelloWorld.java



Содержание check-approve.gitlab-ci.yml
ci-mr:
stage: test
script:
- echo ${CI_API_V4_URL}
- echo "CI_PROJECT_ID ${CI_PROJECT_ID}"
- echo "CI_COMMIT_SHA ${CI_COMMIT_SHA}"
- "export MR_ID=$(curl --silent --request GET --header \"PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI\" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests | jq \".[] | if .sha == \\\"${CI_COMMIT_SHA}\\\" then .id else {} end\" | grep --invert-match {})"
- "export MR_TITLE=$(curl --silent --request GET --header \"PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI\" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests | jq \".[] | if .sha == \\\"${CI_COMMIT_SHA}\\\" then .title else {} end\" | grep --invert-match {})"
- "export MR_WIP=$(curl --silent --request GET --header \"PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI\" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests | jq \".[] | if .sha == \\\"${CI_COMMIT_SHA}\\\" then .work_in_progress else {} end\" | grep --invert-match {})"
- "export MR_UPVOTES=$(curl --silent --request GET --header \"PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI\" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests | jq \".[] | if .sha == \\\"${CI_COMMIT_SHA}\\\" then .upvotes else {} end\" | grep --invert-match {})"
- "export MR_DOWNVOTES=$(curl --silent --request GET --header \"PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI\" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests | jq \".[] | if .sha == \\\"${CI_COMMIT_SHA}\\\" then .downvotes else {} end\" | grep --invert-match {})"
- MR_VOTES=$(expr ${MR_UPVOTES} - ${MR_DOWNVOTES})
- NEED_VOTES_REAL=${NEED_VOTES:-1}
- echo "MR_ID ${MR_ID} MR_TITLE ${MR_TITLE} MR_WIP ${MR_WIP} MR_UPVOTES ${MR_UPVOTES} MR_DOWNVOTES ${MR_DOWNVOTES}"
- echo "MR_VOTES ${MR_VOTES} Up vote = 1, down vote = -1, MR OK if votes >=${NEED_VOTES_REAL}"
- if [ "${MR_VOTES}" -ge "$(expr ${NEED_VOTES_REAL})" ];
then
echo "MR OK";
else
echo "MR ERROR Need more votes";
exit 1;
fi
image: laptevss/gitlab-api-util
rules:
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^release\/.*$/'



Подробнее о том, что происходит при проверке:
  • установлено ограничение, что проверка будет только при создании MR в ветки master или release/*
  • используя API GitLab, получаем количество лайков и дизлайков
  • вычисляем разность между положительными и отрицательными откликами
  • если разность меньше заданного нами значения в NEED_VOTES, то блокируем возможность сделать слияние


5. Запрещаем коммиты в защищенные ветки



Определяем ветки, для которых мы должны проводить code review и указываем, что работать с ними можно только через MR.

Для этого заходим в Настройки Репозиторий Protected Branches


6. Проверяем



Зададим NEED_VOTES: 0

Делаем MR и ставим дизлайк.



В логах сборки:


Теперь ставим лайк и запускаем повторную проверку:
Подробнее..

Пошаговая инструкция по настройке и использованию Gitlab CI Visual Studio для сборки приложения .NET Framework

12.03.2021 14:20:14 | Автор: admin

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


Как только кто-либо из нашей команды вносит изменения в код (читай мерджит feature-ветку в develop), наш билд-сервер:


  • Собирает исходный код и установщик приложения
    • проставляет номер сборки, каждый раз увеличивая последнюю цифру. Например, текущая версия нашего ПО 3.3.0.202 часть 3.3.0 когда-то ввёл разработчик (привет, SemVer), а 202 проставляется в процессе сборки.
    • В процессе анализирует качество кода (с использованием SonarQube) и отправляет отчёт во внутренний SonarQube,
  • Сразу после сборки запускает автотесты (xUnit) и анализирует покрытие тестами (OpenCover),

Также, в зависимости от ветки, в которую были внесены изменения, могут быть выполнены:


  • отправка сборки (вместе с changelog-ом) в один или несколько телеграм-каналов (иногда удобнее брать сборки оттуда).
  • публикация файлов в систему автообновления ПО.

Под катом о том, как мы научили Gitlab CI делать за нас бОльшую часть этой муторной работы.


Оглавление


  1. Устанавливаем и регистрируем Gitlab Runner.
  2. Что нужно знать про .gitlab-ci.yml и переменные сборки.
  3. Активируем режим Developer PowerShell for VS.
  4. Используем CI для проставления версии во все сборки решения.
  5. Добавляем отправку данных в SonarQube.
  6. Причёсываем автотесты xUnit + добавляем вычисление покрытия тестами через OpenCover.
  7. Послесловие.

Перед началом


Чтобы быть уверенными, что написанное ниже работает, мы взяли на github небольшой проект, написанный на WPF и имеющий unit-тесты, и воспроизвели на нём описанные в статье шаги. Самые нетерпеливые могут сразу зайти в созданный на сайте gitlab.com репозиторий и посмотреть, как это выглядит.


Устанавливаем и регистрируем Gitlab Runner


Для того чтобы Gitlab CI мог что-либо собрать, сначала установите и настройте Gitlab Runner на машине, на которой будет осуществляться сборка. В случае проекта на .Net Framework это будет машина с ОС Windows.


Чтобы настроить Gitlab Runner, выполните следующие шаги:


  1. Установите Git для Windows с сайта git.
  2. Установите Visual Studio с сайта Microsoft. Мы поставили себе Build Tools для Visual Studio 2019. Чтобы скачать именно его, разверните список Инструменты для Visual Studio2019.
  3. Создайте папку C:\GitLab-Runner и сохраните в неё программу gitlab runner. Скачать её можно со страницы [документации Gitlab] (https://docs.gitlab.com/runner/install/windows.html) ссылки скрыты прямо в тексте: Download the binary for x86 or amd64.
  4. Запустите cmd или powershell в режиме администратора, перейдите в папку C:\GitLab-Runner и запустите скачанный файл с параметром install (Gitlab runner установится как системная служба).

.\gitlab-runner.exe install

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


    • только в одном проекте смотрите токен в меню проекта Settings > CI/CD в разделе Runners,
    • в группе проектов смотрите токен в меню группы Settings > CI/CD в разделе Runners,
    • для всех проектов Gitlab-а смотрите токен в секции администрирования, меню Overview > Runners.

  2. Выполните регистрацию Runner-а, с помощью команды



.\gitlab-runner.exe register

Далее надо ввести ответы на вопросы мастера регистрации Runner-а:


  • coordinator URL http или https адрес вашего сервера gitlab;
  • gitlab-ci token введите токен, полученный на предыдущем шаге;
  • gitlab-ci description описание Runner-а, которое будет показываться в интерфейсе Gitlab-а;
  • gitlab-ci tags через запятую введите тэги для Runner-а. Если вы не знакомы с этим механизмом, оставьте поле пустым отредактировать его можно позднее через интерфейс самого gitlab-а. Тэги можно использовать для того, чтобы определённые задачи выполнялись на определённых Runner-ах (например, чтобы настроить сборку ПО на Runner-е, развёрнутом на копьютере ОС Windows, а подготовку документации на Runner-е с ОС Linux);
  • enter the executor ответьте shell. На этом шаге указывается оболочка, в которой будут выполняться команды; при указании значения shell под windows выбирается оболочка powershell, а последующие скрипты написаны именно для неё.

Что нужно знать про .gitlab-ci.yml и переменные сборки


В процессе соей работы Gitlab CI берёт инструкции о том, что делать в процессе сборки того или иного репозитория из файла .gitlab-ci.yml, который следует создать в корне репозитория.


Вбив в поиске содержимое .gitlab-ci.yml для сборки приложения .NET Framework можно найти несколько шаблонов: 1, 2. Выглядят они примерно так:


variables:  # Максимальное количество параллельно собираемых проектов при сборке решения; зависит от количества ядер ПК, выбранного для сборки  MSBUILD_CONCURRENCY: 4  # Тут куча путей до утилит, которые просто оябзаны лежать там, где ожидается  NUGET_PATH: 'C:\Tools\Nuget\nuget.exe'  MSBUILD_PATH: 'C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\msbuild.exe'  XUNIT_PATH: 'C:\Tools\xunit.runner.console.2.3.1\xunit.console.exe'  TESTS_OUTPUT_FOLDER_PATH: '.\tests\CiCdExample.Tests\bin\Release\'# Тут указываются стадии сборки. Указывайте любые названия которые вам нравятся, но по умолчанию используют три стадии: build, test и deploy.# Стадии выполняются именно в такой последовательности.stages:  - build  - test# Далее описываются задачи (job-ы)build_job:  stage: build # указание, что задача принадлежит этапу build  # tags: windows # если тут указать тэг, задача будет выполняться только на Runner-е с указанным тэгом   only: # для каких сущностей требуется выполнять задачу    - branches  script: # код шага    - '& "$env:NUGET_PATH" restore'    - '& "$env:MSBUILD_PATH" /p:Configuration=Release /m:$env:MSBUILD_CONCURRENCY /nr:false /clp:ErrorsOnly' # сборка; ключ clp:ErrorsOnlyоставляет только вывод ошибок; ключ nr:false завершает инстансы msbuild   artifacts: # где по завершении задачи будут результаты, которые надо сохранить в gitlab (т.н. артефакты) и которые можно будет передать другим задачам по цепочке    expire_in: 2 days # сколько хранить артефакты    paths: # список путей, по которым находятся файлы для сохранения      - '$env:TESTS_OUTPUT_FOLDER_PATH'test_job:  stage: test  only:    - branches  script:    - '& "$env:XUNIT_PATH" "$env:TESTS_OUTPUT_FOLDER_PATH\CiCdExample.Tests.dll"'  dependencies: # указание, что для запуска этой задачи требуется успешно завершенная задача build_job    - build_job

И последнее: если нам требуется передавать в скрипт значение параметра, который мы не хотим хранить в самом скрипте (например, пароль для подключения куда-либо), мы можем использовать для этого объявление параметров в gitlab. Для этого зайдите в проекте (или в группе проекта) в Settings > CI/CD и найдите раздел Variables. Прописав в нём параметр с именем (key) SAMPLE_PARAMETER, вы сможете получить его значение в в скрипте .gitlab-ci.yml через обращение $env:SAMPLE_PARAMETER.
Также в этом разделе можно настроить передачу введенных параметров только при сборке защищённых веток (галочка Protected) и/или скрытие значения параметра из логов (галочка Masked).
Подробнее о параметрах окружения сборки смотрите в документации к Gitlab CI.


Активируем режим Developer PowerShell for VS


Скрипт, приведённый выше, уже можно использовать для сборки и вызова тестов. Правда, присутствует НО: крайне неудобно прописывать абсолютные пути к разным установкам Visual Studio. К примеру, если на одной билд-машине стоит Visual Studio 2017 BuildTools, а на другой Visual Studio Professional 2019, то такой скрипт будет работать только для одной из двух машин.


К счастью, с версии Visual Studio 2017 появился способ поиска всех инсталляций Visual Studio на компьютере. Для этого существует утилита vswhere, путь к которой не привязан ни к версии Visual Studio, ни к её редакции. А в Visual Studio 2019 (в версии 16.1 или более новой) есть библиотека, которая умеет трансформировать консоль Powershell в режим Developer Powershell, в котором уже прописаны пути к утилитам из поставки VS.


Как применить


Дописываем переменную к секции Variables:


variables:  VSWHERE_PATH: '%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe'

Затем создаём новую секцию before_msbuild якорем enter_vsdevshell и следующим текстом:


.before_msbuild: &enter_vsdevshell  before_script:    - '$vsWherePath = [System.Environment]::ExpandEnvironmentVariables($env:VSWHERE_PATH)'    - '& $vsWherePath -latest -format value -property installationPath -products Microsoft.VisualStudio.Product.BuildTools | Tee-Object -Variable visualStudioPath'    - 'Join-Path "$visualStudioPath" "\Common7\Tools\Microsoft.VisualStudio.DevShell.dll" | Import-Module'    - 'Enter-VsDevShell -VsInstallPath:"$visualStudioPath" -SkipAutomaticLocation'

И всюду, где нам надо использовать утилиты Visual Studio, добавляем этот якорь. После этого задача сборки начинает выглядеть намного более опрятно:


build_job:  <<: *enter_vsdevshell  stage: build  only:    - branches  script:    - 'msbuild /t:restore /m:$env:MSBUILD_CONCURRENCY /nr:false /clp:ErrorsOnly'    - 'msbuild /p:Configuration=Release /m:$env:MSBUILD_CONCURRENCY /nr:false /clp:ErrorsOnly'  artifacts:    expire_in: 2 days    paths:      - '$env:TESTS_OUTPUT_FOLDER_PATH'

Подробно о том, что написано в .before_msbuild
  1. Утилита vswhere.exe умеет находить и выдавать список найденных инсталляций Visual Studio. Расположен она всегда по одному и тому же пути (этот путь записан в переменной VSWHERE_PATH). Поскольку в переменной фигурирует подстановка %programfiles%, её требуется раскрыть до пути к этой папке. Такое раскрытие проще всего сделать через статический метод .NET System.Environment.ExpandEnvironmentVariables.

Результат: мы имеем путь к vswhere.


  1. Вызовом vswhere получим путь к папке с установленной Visual Studio.
    Все параметры утилиты можно посмотреть, если запустить vswhere.exe с параметром -help, в статье же только перечислю использованные:
    • -latest (искать самые свежие установки),
    • -property installationPath (вывести параметр пути установки),
    • -format value (при печати параметра вывести только значение параметра, без его имени),
    • -products <список искомых инсталляций Visual Studio, пробел считается разделителем элементов списка> (указание искомых редакций Visual Studio). Например, при запуске с параметром -products Microsoft.VisualStudio.Product.Community Microsoft.VisualStudio.Product.BuildTools утилита попробует найти Visual Studio редакций Community или BuildTools. Подробнее об идентификаторах продуктов смотрите по ссылке https://aka.ms/vs/workloads.

Результат: в переменную $visualStudioPath записан путь к Visual Studio или пустая строка, если инсталляций Visual Studio не найдено (обработку этой ситуации мы ещё не добавили).


  1. Команда Import-Module загружает библиотеку Microsoft.VisualStudio.DevShell.dll, в которой прописаны командлеты трансформации консоли Powershell в Developer-консоль. А командлет Join-Path формирует путь к этой библиотеке относительно пути установки Visual Studio.
    На этом шаге нам прилетит ошибка, если библиотека Microsoft.VisualStudio.DevShell.dll отсутствует или путь к установке Visual Studio нужной редакции не был найден Import-Module сообщит, что не может загрузить библиотеку.

Результат: загружен модуль Powershell с командлетом трансформации.


  1. Запускаем передёлку консоли в Developer Powershell. Чтобы корректно прописать пути к утилитам, командлету требуется путь к установленной Visual Studio (параметр -VsInstallPath). А указаниеSkipAutomaticLocation требует от командлета не менять текущее расположение (без этого параметра путь меняется на <домашнаяя папка пользователя>\source\repos.

Результат: мы получили полноценную консоль Developer Powershell с прописанными путями к msbuild и многим другим утилитам, которые можноиспользовать при сборке.


Используем CI для проставления версии во все сборки решения


Раньше мы использовали t4 шаблоны для проставления версий: номер версии собиралась из содержимого файла в формате <major>.<minor>.<revision>, далее к ней добавлялся номер сборки из Gitlab CI и он передавался в tt-шаблон, добавляющий в решение везде, где требуется, номер версии. Однако некоторое время назад был найден более оптимальный способ использование команд git tag и git describe.


Команда git tag устанавливает коммиту метку (тэг). Отметить таким образом можно любой коммит в любой момент времени. В отличие от веток, метка не меняется. То есть если после помеченного коммита вы добавите ещё один, метка останется на помеченном коммите. Если попробуете переписать отмеченный коммит командами git rebase или git commit --amend, метка также продолжит указывать на исходный коммит, а не на изменённый. Подробнее о метках смотрите в git book.


Команда git describe, к сожалению, в русскоязычном gitbook не описана. Но работает она примерно так: ищет ближайшего помеченного родителя текущего коммита. Если такого коммита нет команда возвращает ошибку fatal: No tags can describe '<тут хэш коммита>'. А вот если помеченный коммит нашёлся тогда команда возвращает строку, в которой участвует найденная метка, а также количество коммитов между помеченным и текущим.


На заметку: чтобы данная команда работала корректно во всех случаях, автор gitflow даже чуть-чуть поменял скрипты finish hotfix и finish release. Если кому интересно посмотреть обсуждение с автором gitflow, а также увидеть что изменилось (картинка с актуальной схемой в последнем сообщении в треде).


Кстати, по этой же причине если вы используете gitflow, требуется после вливания feature-ветки в develop требуется удалить влитую локальную ветку, после чего пересоздать её от свежего develop:


Не забывайте пересоздавать ветки от develop
(обратите внимание на историю git в левой части картинки: из-за отсутствия пути из текущего коммита до коммита с меткой 1.0.5, команда git describe выдаст неверный ответ)


Но вернёмся к автопроставлению версии. В нашем репозитории царит gitflow (точнее его rebase-версия), метки расставляются в ветке master и мы не занываем пересоздавать feature-ветки от develop, а также merge-ить master в develop после каждого релиза или хотфикса.


Тогда получить версию для любого коммита и сразу передать её в msbuild можно добавив всего пару строк к задаче сборки:


build_job:  <<: *enter_vsdevshell  stage: build  only:    - branches  script:    - 'msbuild /t:restore /m:$env:MSBUILD_CONCURRENCY /nr:false /clp:ErrorsOnly'    - '$versionGroup = git describe --long | Select-String -Pattern "(?<major>[0-9]+)\.(?<minor>[0-9]*)\.(?<patch>[0-9]*)\-(?<commit>[0-9]+)\-g[0-9a-f]+" | Select-Object -First 1'    - '[int]$major, [int]$minor, [int]$patch, [int]$commit = $versionGroup.Matches[0].Groups["major", "minor", "patch", "commit"].Value'    - '[string]$version = "$major.$minor.$patch.$commit"'    - 'msbuild /p:Configuration=Release /p:AssemblyVersionNumber=$version /m:$env:MSBUILD_CONCURRENCY /nr:false /clp:ErrorsOnly'  artifacts:    expire_in: 2 days    paths:      - '$env:TESTS_OUTPUT_FOLDER_PATH'

Как это работает:


  1. Мы проставляем метки в формате <major>.<minor>.<revision>.
  2. Тогда git describe --long возвращает нам строку, описывающую версию в формате <major>.<minor>.<revision>-<количество новых коммитов>-g<хэш текущего коммита>.
  3. Парсим полученную строку через регулярные выражения, выделяя нужные нам части и записывем части в $versionGroup.
  4. Преобразовываем четыре найденные подстроки в 4 числа и пишем их в переменные $major, $minor, $patch, $commit, после чего собираем из них строку уже в нужном нам формате.
  5. Передаём указанную строку в msbuild чтобы он сам проставил версию файлов при сборке.

Обратите внимание: если вы, согласно gitflow, будете отмечать (тэгировать) ветку master после вливания в неё release или hofix, будьте внимательны: до простановки метки автосборка будет вестись относительно последней существующей ветки. Например, сейчас опубликована версия 3.4, а вы создаёте release-ветку для выпуска версии 3.5. Так вот: всё время существования этой ветки, а также после её вливания в master, но до простановки тэга, автосборка будет проставлять версию 3.4.


Добавляем отправку данных в SonarQube


SonarQube это мощный инструмент контроля качества кода.


SonarQube имеет бесплатную Community-версию, которая способна проводить полный анализ. Правда, только одной ветки. Чтобы настроить её на контроль качества ветки разработки (develop), требуется выполнить следующие шаги (разумеется, помимо установки и развёртывания сервера SonarQube):


  1. Создайте в SonarQube новый проект, после чего запомнить его ключ.


  2. Скачайте SonarScanner for MSBuild (с сайта sonarqube.org)[https://docs.sonarqube.org/latest/analysis/scan/sonarscanner-for-msbuild/] мы используем версию .NET Framework 4.6+.


  3. Распакуйте содержимое архива в папку. Например, в C:\Tools\SonarScanner.



На заметку: эту утилиту также можно скачать на сборочную машину через NuGet, но тогда надо будет чуть по-иному указывать её путь.


  1. Зайдите в параметры CI/CD в свойствах проекта в Gitlab следующие параметры:
    • SONARQUBE_PROJECT_KEY ключ проекта,
    • SONARQUBE_AUTH_TOKEN токен авторизации.

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


  1. Допишите переменные к секции Variables:


    variables:SONARSCANNER_MSBUILD_PATH: 'C:\Tools\SonarScanner\SonarScanner.MSBuild.exe'SONARQUBE_HOST_URL: 'url вашего сервера SonarQube'
    

  2. Допишите в задачу тестирования ветки разработки (test_job) команды для запуска анализа кода и уберите зависимость от задачи build_job:


    test_job:stage: testonly:- /^develop$/<<: *enter_vsdevshellscript:- '$versionGroup = git describe --long | Select-String -Pattern "(?<major>[0-9]+)\.(?<minor>[0-9]*)\.(?<patch>[0-9]*)\-(?<commit>[0-9]+)\-g[0-9a-f]+" | Select-Object -First 1'- '[int]$major, [int]$minor, [int]$patch, [int]$commit = $versionGroup.Matches[0].Groups["major", "minor", "patch", "commit"].Value'- '[string]$version = "$major.$minor.$patch.$commit"'- '& "$env:SONARSCANNER_MSBUILD_PATH" begin /key:$env:SONARQUBE_PROJECT_KEY /d:sonar.host.url=$env:SONARQUBE_HOST_URL /d:sonar.login=$env:SONARQUBE_AUTH_TOKEN /d:sonar.gitlab.project_id=$CI_PROJECT_PATH /d:sonar.gitlab.ref_name=develop /v:$version /d:sonar.dotnet.excludeGeneratedCode=true'- 'msbuild /t:rebuild /m:$env:MSBUILD_CONCURRENCY /nr:false /clp:ErrorsOnly'- '& "$env:SONARSCANNER_MSBUILD_PATH" end /d:sonar.login=$env:SONARQUBE_AUTH_TOKEN'- '& "$env:XUNIT_PATH" "$env:TESTS_OUTPUT_FOLDER_PATH\CiCdExample.Tests.dll"'
    


Теперь при каждой сборке ветки develop в SonarQube будет отправляться подробный анализ нашего кода.


На заметку: вообще команда msbuild /t:rebuild полностью пересобирает решение. Вероятно, в большинстве проектов анализ можно было бы встроить прямо в стадию сборки. Но сейчас у нас анализ в отдельной задаче.


Пара слов об использованных параметрах:


  • key ключ проекта на сервере SonarQube,
  • v собираемая версия. ИМХО отлично комбинируется с предыдущим шагом автопроставления версии,
  • sonar.gitlab.project_id ID проекта на сервере Gitlab,
  • sonar.gitlab.ref_name название ветки, которое получает сервер SonarQube при передаче результатов анализа,
  • sonar.dotnet.excludeGeneratedCode не включать в анализ объекты, отмеченные атрибутом System.CodeDom.Compiler.GeneratedCode (чтобы не оценивать качество автосгенерированного кода).

Причёсываем автотесты xUnit + добавляем вычисление покрытия тестами через OpenCover


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


  • сам находил библиотеки с тестами,
  • прогонял их пачкой через xUnit,
  • вычислял тестовое покрытие через OpenConver,
  • отправлял результаты покрытия тестами в SonarQube.

На заметку: обычно в паре с OpenCover используют ReportGenerator, но при наличии SonarQube мы с тем же успехом можем смотреть результаты через его интерфейс.


Для настройки выполним следующие шаги:


  1. Скачайте OpenCover в виде zip-файла с сайта github.


  2. Распакуйте содержимое архива в папку. Например, в C:\Tools\OpenCover.



На заметку: эту утилиту также можно скачать на сборочную машину через NuGet, но тогда надо будет чуть по-иному указывать её путь.


  1. Допишите переменные к секции Variables:


    variables:OBJECTS_TO_TEST_REGEX: '^Rt[^\n]*\.(dll|exe)$'OPENCOVER_PATH: 'C:\Tools\opencover-4.7.922\xunit.console.exe'OPENCOVER_FILTER: '+[Rt.*]* -[*UnitTests]* -[*AssemblyInfo]*'OPENCOVER_REPORT_FILE_PATH: '.\cover.xml'
    

  2. Модифицируйте задачу тестирования ветки разработки (test_job), чтобы она включала и команды вызова OpenCover:



test_job:  stage: test  only:    - /^develop$/  <<: *enter_vsdevshell  script:    - '$versionGroup = git describe --long | Select-String -Pattern "(?<major>[0-9]+)\.(?<minor>[0-9]*)\.(?<patch>[0-9]*)\-(?<commit>[0-9]+)\-g[0-9a-f]+" | Select-Object -First 1'    - '[int]$major, [int]$minor, [int]$patch, [int]$commit = $versionGroup.Matches[0].Groups["major", "minor", "patch", "commit"].Value'    - '[string]$version = "$major.$minor.$patch.$commit"'    - '& "$env:SONARSCANNER_MSBUILD_PATH" begin /key:$env:SONARQUBE_PROJECT_KEY /d:sonar.host.url=$env:SONARQUBE_HOST_URL /d:sonar.login=$env:SONARQUBE_AUTH_TOKEN /d:sonar.gitlab.project_id=$CI_PROJECT_PATH /d:sonar.gitlab.ref_name=develop /v:$version /d:sonar.cs.opencover.reportsPaths="$env:OPENCOVER_REPORT_FILE_PATH" /d:sonar.dotnet.excludeGeneratedCode=true'    - 'msbuild /t:rebuild /m:$env:MSBUILD_CONCURRENCY /nr:false /clp:ErrorsOnly'    - '$dllsToRunUnitTesting = @(Get-ChildItem "$env:TESTS_OUTPUT_FOLDER_PATH" -Recurse) | Where-Object {$_.Name -match $env:OBJECTS_TO_TEST_REGEX} | ForEach-Object { """""$_""""" } | Join-String -Separator " "'    - '& "$env:OPENCOVER_PATH" -register -target:"$env:XUNIT_PATH" -targetargs:"$dllsToRunUnitTesting -noshadow" -filter:"$env:OPENCOVER_FILTER" -output:"$env:OPENCOVER_REPORT_FILE_PATH" | Write-Host'    - 'if ($?) {'    - '[xml]$coverXml = Get-Content "$env:OPENCOVER_REPORT_FILE_PATH"'    - '$sequenceCoverage = $coverXml.CoverageSession.Summary.sequenceCoverage'    - '$branchCoverage = $coverXml.CoverageSession.Summary.branchCoverage'    - 'Write-Host "Total Sequence Coverage <!<$sequenceCoverage>!>"'    - 'Write-Host "Total Branch Coverage [![$branchCoverage]!]"'    - '} else {'    - 'Write-Host "One or more tests failed!"'    - 'Throw'    - '}'    - '& "$env:SONARSCANNER_MSBUILD_PATH" end /d:sonar.login=$env:SONARQUBE_AUTH_TOKEN'

Обратите внимание: в begin-команде запуска sonar scanner-а появился дополнительный параметр /d:sonar.cs.opencover.reportsPaths.


  1. (необязательный пункт) Ненадолго возврващаемся в Gitlab, заходим в меню проекта Settings > CI/CD и находим на странице настроек параметр Test coverage parsing. Указываем в нём регулярное выражение, которое позволит Gitlab-у также получать информацию о покрытии тестами приложения:
    • если хочется видеть значение покрытия тестов по строкам кода (его ешё называют Sequence Coverage или Statement Coverage), указываем выражение <!<([^>]+)>!>,
    • если хочется видеть значение покрытия тестов по веткам условных операторов (его называют Decision Coverage или Branch Coverage), указываем выражение \[!\[([^>]+)\]!\].

А теперь комментарии по изменениям в скрипте.


Длинная строка, начинающаяся с объявления переменной $dllsToRunUnitTesting, нужна для того, чтобы найти все библиотеки нашего приложения, которые потом будут участвовать в расчёте тестового покрытия. При этом выбираются они по регулярному выражению, заданному в параметре $env:OBJECTS_TO_TEST_REGEX (мы же не хотим, например, учитывать в покрытии библиотеки .net или сторонних nuget-пакетов). Пути ко всем найденным библиотекам склеиваются в строку с разделителем пробелом и двумя двойными кавычками для каждого параметра. Две двойные кавычки были добавлены потому, что OpenCover при вызове приложения xunit съедает одни из кавычек, а вторые кавычки нужны на случай наличия пробелов в абсолютных путях.


Следующая строка запуск утилиты OpenConver с передачей ей списка библиотек нашего приложения, фильтра, по которому OpenCover исключает из покрытия библиотеки с unit-тестами, а также часть классов, не требующих расчёта покрытия (например, AssemblyInfo). Конвейер и Write-Host были добавлены в порыве вдохновения, так как без него у нас не работал вывод OpenConver-а.


И следующий if проверяет, успешно ли завершился запуск OpenConver. Если не успешно кладём скрипт; если же успешно парсим получившийся xml-файлик с отчётом и печатаем значения покрытия тестами, чтобы затем его легко распарсил gitlab через указанное в настройках регулярное выражение.


Послесловие


Как известно, нет предела совершенству. Мы продолжим добавлять новые функции в используемые нами процессы сборки, тестирования и публикации. На момент написания тестируется переезд нашего ПО на .NET 5 (с ним OpenCover уже не работает, сборка выполняется через команду dotnet), а также мы переходим на другой способ публикации (к сожалению, пока не могу дать более подробный комментарий).


Если у вас появятся вопросы или предложения пишите в комментариях.


И спасибо за прочтение!

Подробнее..

Приглашаем на Live-Вебинар Автоматизация процессов с GitLab CICD 29 Окт., 1500 -1600 (MST)

06.10.2020 12:23:44 | Автор: admin

Расширяем знания и переходим на следующий уровень.




Вы только начинаете изучать основные принципы Continuous Integration / Continuous Delivery или написали уже не один десяток пайплайнов? Вне зависимости от уровня Ваших знаний, присоединяйтесь к нашему вебинару, чтобы на практике разобраться, почему тысячи организаций по всему миру выбирают GitLab в качестве ключевого инструмента для автоматизации IT процессов.

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

В этом вебинаре мы разберем:
  • Основные элементы автоматизации в GitLab CI/CD
  • Принципы Pipelines-as-Code
  • AutoDevOps автоматический полноценный CI/CD конвейер, который самостоятельно автоматизирует весь процесс
  • Расширенные настройки и оптимизацию GitLab CI/CD

Также мы вметсе на конкретных примерах покажем, как использование GitLab из коробки поможет значительно ускорить цикл разработки программного обеспечения.
Подробнее..

Настройка GitLab CI CD для Java приложения

19.02.2021 14:12:20 | Автор: admin

Из-за прекращения поддержи Bitbucket Setver пришлось переехать на GitLab.


В Bitbucket Server не было встроенного CI/CD, поэтому использовали Teamcity. Из-за проблемы интеграции Teamcity с GitLab, мы попробовали GitLab Pipline. И остались довольны.


Disclamer: У меня не так много опыта в CI/CD, так что статья скорее для новичков. Буду рад услышать конструктивную критику с предложениями по оптимизации скрипта сборки :)


Коротко о Gitlab Pipline


Если говорить простыми словами: сборка делится на задачи. Какие-то задачи выполняются последовательно и передают результаты следующим задачам, какие-то задачи распаралеливаются: например можно одновременно деплоить на сервер и в nexus.


Раннеры и задачи


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


Раннеры бывают разных типов. Мы рассмотрим executor docker. Для каждой задачи создается новый чистый контейнер. Но между контейнерами можно передавать промежуточные результаты это называется кэширование.


Кэширование и его особенности


Механизм кэширования разрабатывал какой-то одаренный человек. Поэтому сразу разобраться, как оно работает, будет не просто. В отдельной статье можно прочитать о кэшировании подробнее.


Каждый раннер хранит кэш в папке /cache. Для каждого проекта в этой папке создается еще папка. Сам кэш хранится в виде zip архива.


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


Можно определить только один набор папок для кэша. Также только для всего набора можно выбрать политику кэширования. Например если вы уверены, что задача не изменит кэш, то можно его не загружать. Но нельзя запретить загружать например две папки из четырех.


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


Так же стоит знать, что кэш не удаляется после окончания сборки. При следующей такой же сборке старый кэш будет доступен.


Эти особенности усложняют создание инструкций для CI CD.


Артефакты


Помимо кэша между сборками можно передавать артефакты.


Артефакт это файлы, которые считаются законченным продуктом сборки. Например .jar файлы приложения.


В отличие от кэша, артефакт передается между раннерами. Из-за этого многие злоупотребляют использованием артефактов там, где стоит использовать кэш.


Следуйте следующим правилам:


  1. Если текущее задание может выполняться самостоятельно, но в присутствии контента работа будет идти быстрее, используйте кэш.
  2. Если текущее задание зависит от результатов предыдущего, то есть не может выполняться само по себе, используйте артефакты и зависимости.

Установка Gitlab Runner


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


mkdir ~/runner_name

Само создание ранера выполняется в одну команду. Можно создать сколько угодно ранеров.


docker run -d --name gitlab-runner-name \  --restart always \  -v /var/run/docker.sock:/var/run/docker.sock \  -v /home/user/runner_name:/etc/gitlab-runner:z \  gitlab/gitlab-runner:latest

Мало создать раннер, теперь его нужно зарегистрировать в GitLab. Зарегистрировать можно на уровне всего GitLab, тогда сборки будут выполняться для любого проекта; на уровне группы выполнятся только для группы, и на уровне проекта.


Заходим в контейнер.


sudo docker exec -ti gitlab-runner-name bash

Внутри контейнера выполним команду регистрации. Регистрация происходит в интерактивном режиме.


gitlab-runner register

Отвечаем на вопросы:


Runtime platform                                    arch=amd64 os=linux pid=27 revision=888ff53t version=13.8.0Running in system-mode.                            Enter the GitLab instance URL (for example, https://gitlab.com/):http://git.company.name/Enter the registration token:vuQ6bcjuEPqc8dVRRhgYEnter a description for the runner:[c6558hyonbri]: runner_twoEnter tags for the runner (comma-separated):Registering runner... succeeded                     runner=YJt3v3QgEnter an executor: parallels, shell, virtualbox, docker+machine, kubernetes, custom, docker, docker-ssh+machine, docker-ssh, ssh:dockerEnter the default Docker image (for example, ruby:2.6):maven:3.3.9-jdk-8Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded! 

Тут все просто:


  1. Адрес вашего gitlab.
  2. Токен авторизации. Посмотреть его можно в настройках гурппы/проекта в разделе CI/CD Runners.
  3. Название раннера.
  4. Теги ранера, можно пропустить нажав Enter.
  5. Исполнитель сборки. Вводим docker.
  6. Образ, который будет использоваться по умолчанию, если не установлен другой.

После этого в настройках проекта можно посмотреть доступные раннеры.


Добавленный ранер GitLab


После регистрации, в папке /home/user/runner_name появится файл с настройками конфигурации config.toml. Нам нужно добавить docker volume для кэширования промежуточных результатов.


volumes = ["gitlab-runner-builds:/builds", "gitlab-runner-cache:/cache"]

Проблема кэширования.
В начале статьи я рассказал о проблеме кеширования. Ее можно решить с помощью монтирования одного volume к разным раннерам. То есть во втором своем раннере так же укажите volumes = ["gitlab-runner-cache:/cache"]. Таким образом разные раннеры будут иметь единый кэш.


В итоге файл конфигурации выглядит так:


concurrent = 1check_interval = 0[session_server]  session_timeout = 1800[[runners]]  name = "runner_name"  url = "gitlab_url"  token = "token_value"  executor = "docker"  [runners.custom_build_dir]  [runners.cache]    [runners.cache.s3]    [runners.cache.gcs]    [runners.cache.azure]  [runners.docker]    tls_verify = false    image = "maven:3.3.9-jdk-8"    privileged = false    disable_entrypoint_overwrite = false    oom_kill_disable = false    disable_cache = false    volumes = ["gitlab-runner-builds:/builds", "gitlab-runner-cache:/cache"]    shm_size = 0

После изменения перезапускаем раннер.


docker restart gitlab-runner-name

Что хотим получить от CI CD?


У нас на проекте было 3 контура:


  • dev-сервер, на него все попадает сразу после MR;
  • пре-прод-сервер, на него все попадает перед попаданием на прод, там проходит полное регресс тестирование;
  • прод-сервер, собственно сама прод среда.

Что нам было необходимо от нашего CI/CD:


  • Запуск unit-тестов для всех MergeRequest
  • При мерже в dev ветку повторный запуск тестов и автоматический деплой на dev-сервер.
  • Автоматическая сборка, тестирвоание и деплой веток формата release/* на пре-прод-сервер.
  • При мерже в master ничего не происходит. Релиз собирается при обнаружении тега формата release-*. Одновременно с деплоем на прод-сервер будет происходить загрузка в корпоративный nexus.
  • Бонусом настроим уведомления о статусе деплоя в Telegram.

Настройка GitLab CI для Maven


Что делать если у вас Gradle? Загляните в оригинал статьи, там я рассказываю про настройку и для Gradle. Она не сильно отличается.


Создайте в корне проекта файл .gitlab-ci.yml.


Настройку буду объяснять блоками, в конце прикреплю единый файл.


Вы можете указать нужный образ, вместо дефолтного образа у раннера.


image: maven:latest

Устанавливаем место хранения папки .m2 через переменные среды variables. Это понадобится, когда будем настраивать кэширование.


// ... ... ... ... ...variables:  MAVEN_OPTS: "-Dmaven.repo.local=./.m2/repository"// ... ... ... ... ...

Далее указываются этапы сборки. Они позволяют группировать задачи для одновременного выполнения.


// ... ... ... ... ...stages:  - build  - test  - package  - deploy  - notify// ... ... ... ... ...

  • build стадия сборки.
  • test стадия тестирования.
  • package стадия упаковки в jar.
  • deploy стадия деплоя на сервер.
  • notify стадия уведомления о провале.

Указываем непосредственно задачи для каждой стадии.


Сборка Build


// ... ... ... ... ...build:  stage: build  only:    - dev    - merge_requests    - /^release\/.*$/  except:    - tags  script:    - 'mvn --settings $MAVEN_SETTINGS compile'  cache:    paths:      - ./target      - ./.m2// ... ... ... ... ...

Раздел script выполняет linux команды.


Переменная GitLab CI/CD $MAVEN_SETTINGS необходима для передачи файла settings.xml, если вы используете нестандартные настройки, например корпоративные репозитории. Переменная создается в настройках CI/CD для группы/проекта. Тип переменной File.


Раздел only указывает для каких веток и тегов выполнять задачу. Чтобы не собирать каждую запушенную ветку устанавливаем: dev, merge_requests и ветки формата /release/*.


Раздел only не разделяет ветка это или тег. Поэтому мы указываем параметр except, который исключает теги. Из-за этого поведения за сборку на прод отвечают отдельные задачи. В противном случае, если бы кто-то создал тег формата release/*, то он бы запустил сборку.


Для защиты от случайного деплоя в прод джуном, рекомендую установить защиту на ветки dev, master, /release/*, а так же на теги release-*. Делается это в настройках проекта GitLab.


Раздел cache отвечает за кэширование. Чтобы каждый раз не выкачивать зависимости добавляем в кэш папку ./target и ./.m2.


Запуск unit тестов test


Создаем задачу для запука тестирования.


// ... ... ... ... ...test:  stage: test  only:    - dev    - merge_requests    - /^release\/.*$/  except:    - tags  script:    - 'mvn --settings $MAVEN_SETTINGS test'  cache:    paths:      - ./target      - ./.m2// ... ... ... ... ...

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


Упаковка package


Следом добавляем задачу упаковки с выключенными тестами. Она уже выполняется только для веток dev и release/*. Упаковывать Merge Request смысла нет.


// ... ... ... ... ...package:  stage: package  only:    - dev    - /^release\/.*$/  except:    - tags  script:    - 'mvn --settings $MAVEN_SETTINGS package -Dmaven.test.skip=true'  artifacts:    paths:      - target/*.jar  cache:    policy: pull    paths:      - ./target      - ./.m2// ... ... ... ... ...

Обратите внимание на policy: pull и artifacts. В следующих задачах не нужны исходники и зависимости, так что policy: pull отключает кэширование. Для сохранения результатов сборки используем artifacts, который сохраняет .jar файлы и передает их следующим задачам.


Деплой deploy


Теперь осталось задеплоить артефакт на dev-сервер с помощью ssh и scp.


// ... ... ... ... ...deploy_dev_server:  stage: deploy  only:    - dev  except:    - tags  before_script:    - which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )    - eval $(ssh-agent -s)    - echo "$SSH_PRIVATE_KEY" | ssh-add -    - mkdir -p ~/.ssh    - chmod 700 ~/.ssh    - ssh-keyscan $DEV_HOST >> ~/.ssh/known_hosts    - chmod 644 ~/.ssh/known_hosts  script:    - ssh $DEV_USER@$DEV_HOST "[ ! -f $DEV_APP_PATH/app_name.jar ] || mv $DEV_APP_PATH/app_name.jar $DEV_APP_PATH/app_name-build-$CI_PIPELINE_ID.jar"    - scp target/app_name.jar $DEV_USER@$DEV_HOST:$DEV_APP_PATH/    - ssh $DEV_USER@$DEV_HOST "sudo systemctl stop app_name_service && sudo systemctl start app_name_service"    - sh ci-notify.sh // ... ... ... ... ...

Не забудьте создать переменные в GitLab CI CD:


  • $DEV_USER пользователь системы, от чьего имени будет происходить деплой.
  • $DEV_HOST ip адрес сервера.
  • $DEV_APP_PATH путь до папки приложения.
  • $SSH_PRIVATE_KEY приватный ключ.

Раздел before_script отвечает за выполнение настроек перед основным скриптом. Мы проверяем наличие ssh-agent, устанавливаем его при отсутствии. После чего добавляем приватный ключ, устанавливаем правильные права на папки.


В разделе script происходит деплой на сервер:


  1. Проверяем наличие старого jar и переименовываем его. $CI_PIPELINE_ID это глобальный номер сборки Pipeline.
  2. Копируем новый jar на сервер.
  3. Останавливаем и запускаем службу, отвечающую за приложение.
  4. Отправляем уведомление об успехе в телеграм. Об этом ниже.

Как создавать службы linux для Spring Boot приложения, я написал в отдельной статье.


На пре-прод делаем по аналогии, только меняем переменные на $PRE_PROD_*.


// ... ... ... ... ...deploy_pre_prod:  stage: deploy  only:    - /^release\/.*$/  except:    - tags  before_script:    - which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )    - eval $(ssh-agent -s)    - echo "$SSH_PRIVATE_KEY" | ssh-add -    - mkdir -p ~/.ssh    - chmod 700 ~/.ssh    - ssh-keyscan $PRE_PROD_HOST >> ~/.ssh/known_hosts    - chmod 644 ~/.ssh/known_hosts  script:    - ssh $PRE_PROD_USER@$PRE_PROD_HOST "[ ! -f $PRE_PROD_APP_PATH/app_name.jar ] || mv $PRE_PROD_APP_PATH/app_name.jar $PRE_PROD_APP_PATH/app_name-build-$CI_PIPELINE_ID.jar"    - scp target/app_name.jar $DEV_USER@$PRE_PROD_HOST:$PRE_PROD_APP_PATH/    - ssh $PRE_PROD_USER@$PRE_PROD_HOST "sudo systemctl stop app_name_service && sudo systemctl start app_name_service"    - sh ci-notify.sh // ... ... ... ... ...

Настройка деплоя на прод


Для прод-деплоя можно объединить сборку, тестирование и упаковку в одну задачу.


// ... ... ... ... ...package_prod:  stage: package  only:    - /^release-.*$/  except:    - branches  script:    - 'mvn --settings $MAVEN_SETTINGS package'  artifacts:    paths:      - target/*.jar// ... ... ... ... ...

Мы защищаемся от срабатывания на ветки формата release-*, нам нужно срабатывание только по тегу.


Деплой аналогичен пре-проду, изменяется только only и переменные.


Дополнительно появляется задача с отправкой артефакта в корпоративный nexus. Деплой на сервер и в нексус работает параллельно.


// ... ... ... ... ...deploy_prod:  stage: deploy  only:    - /^release-.*$/  except:    - branches  ...deploy_nexus_server:  stage: deploy  only:    - /^release-.*$/  except:    - branches  script:    - 'mvn --settings $MAVEN_SETTINGS deploy -Dmaven.test.skip=true'// ... ... ... ... ...

Итоговый .gitlab-ci.yml:


image: maven:latestvariables:  MAVEN_OPTS: "-Dmaven.repo.local=./.m2/repository"stages:  - build  - test  - package  - deploy  - notifybuild:  stage: build  only:    - dev    - merge_requests    - /^release\/.*$/  except:    - tags  script:    - 'mvn --settings $MAVEN_SETTINGS compile'  cache:    paths:      - ./target      - ./.m2test:  stage: test  only:    - dev    - merge_requests    - /^release\/.*$/  except:    - tags  script:    - 'mvn --settings $MAVEN_SETTINGS test'  cache:    paths:      - ./target      - ./.m2package:  stage: package  only:    - dev    - /^release\/.*$/  except:    - tags  script:    - 'mvn --settings $MAVEN_SETTINGS package -Dmaven.test.skip=true'  artifacts:    paths:      - target/*.jar  cache:    policy: pull    paths:      - ./target      - ./.m2deploy_dev_server:  stage: deploy  only:    - dev  except:    - tags  before_script:    - which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )    - eval $(ssh-agent -s)    - echo "$SSH_PRIVATE_KEY" | ssh-add -    - mkdir -p ~/.ssh    - chmod 700 ~/.ssh    - ssh-keyscan $DEV_HOST >> ~/.ssh/known_hosts    - chmod 644 ~/.ssh/known_hosts  script:    - ssh $DEV_USER@$DEV_HOST "[ ! -f $DEV_APP_PATH/app_name.jar ] || mv $DEV_APP_PATH/app_name.jar $DEV_APP_PATH/app_name-build-$CI_PIPELINE_ID.jar"    - scp target/app_name.jar $DEV_USER@$DEV_HOST:$DEV_APP_PATH/    - ssh $DEV_USER@$DEV_HOST "sudo systemctl stop app_name_service && sudo systemctl start app_name_service"deploy_pre_prod:  stage: deploy  only:    - /^release\/.*$/  except:    - tags  before_script:    - which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )    - eval $(ssh-agent -s)    - echo "$SSH_PRIVATE_KEY" | ssh-add -    - mkdir -p ~/.ssh    - chmod 700 ~/.ssh    - ssh-keyscan $PRE_PROD_HOST >> ~/.ssh/known_hosts    - chmod 644 ~/.ssh/known_hosts  script:    - ssh $PRE_PROD_USER@$PRE_PROD_HOST "[ ! -f $PRE_PROD_APP_PATH/app_name.jar ] || mv $PRE_PROD_APP_PATH/app_name.jar $PRE_PROD_APP_PATH/app_name-build-$CI_PIPELINE_ID.jar"    - scp target/app_name.jar $DEV_USER@$DEV_HOST:$DEV_APP_PATH/    - ssh $PRE_PROD_USER@$PRE_PROD_HOST "sudo systemctl stop app_name_service && sudo systemctl start app_name_service"package_prod:  stage: package  only:    - /^release-.*$/  except:    - branches  script:    - 'mvn --settings $MAVEN_SETTINGS package'  artifacts:    paths:      - target/*.jardeploy_prod:  stage: deploy  only:    - /^release-.*$/  except:    - branches  before_script:    - which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )    - eval $(ssh-agent -s)    - echo "$SSH_PRIVATE_KEY" | ssh-add -    - mkdir -p ~/.ssh    - chmod 700 ~/.ssh    - ssh-keyscan $PROD_HOST >> ~/.ssh/known_hosts    - chmod 644 ~/.ssh/known_hosts  script:    - ssh $PROD_USER@$PROD_HOST "[ ! -f $PROD_APP_PATH/app_name.jar ] || mv $PROD_APP_PATH/app_name.jar $PROD_APP_PATH/app_name-build-$CI_PIPELINE_ID.jar"    - scp target/app_name.jar $PROD_USER@$PROD_HOST:$PROD_APP_PATH/    - ssh $PROD_USER@$PROD_HOST "sudo systemctl stop app_name_service && sudo systemctl start app_name_service"

Бонус: уведомления о деплое в Telegram


Полезная фишка для уведомления менеджеров. После сборки в телеграм отправляется статус сборки, название проекта и ветка сборки.


В задачах деплоя последней командой пропишите:


// ... ... ... ... ...script:  - ...  - sh ci-notify.sh // ... ... ... ... ...

Сам файл необходимо добавить в корень проекта, рядом с файлом .gitlab-ci.yml.


Содержимое файла:


#!/bin/bashTIME="10"URL="http://personeltest.ru/aways/api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage"TEXT="Deploy status: $1%0A-- -- -- -- --%0ABranch:+$CI_COMMIT_REF_SLUG%0AProject:+$CI_PROJECT_TITLE"curl -s --max-time $TIME -d "chat_id=$TELEGRAM_CHAT_ID&disable_web_page_preview=1&text=$TEXT" $URL >/dev/null

Скрипт отправляет запрос к API Telegram, через curl. Параметром скрипта передается emoji статуса билда.


Не забудьте добавить новые параметры в CI/CD:


  • $TELEGRAM_BOT_TOKEN токен бота в телеграмм. Получить его можно при создании своего бота.
  • $TELEGRAM_CHAT_ID идентификатор чата или беседы, в которую отправляется сообщение. Узнать идентификатор можно с помощью специального бота.

Необходимо создать задачи для уведомления о падении сборки.


// ... ... ... ... ...notify_error:  stage: notify  only:    - dev    - /^release\/.*$/  script:    - sh ci-notify.sh   when: on_failurenotify_error_release:  stage: notify  only:    - /^release-.*$/  except:    - branches  script:    - sh ci-notify.sh   when: on_failure// ... ... ... ... ...

По сложившейся традиции у нас отдельная задача для прод-среды. Благодаря параметру when: on_failure задача отрабатывает только, когда одна из предыдущих завершилась неудачно.


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

Подробнее..
Категории: Gitlab , Devops , Java , Gitlab-ci , Maven , Nexus , Gitlab-runner

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru