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

Bitbucket

Сам себе DevOps строим cloud-only CI для веб приложения

22.05.2021 18:12:52 | Автор: admin

Привет, Хабр! Сегодня мы немного поговорим о DevOps и самоорганизации на примере одного из наших проектов.

Начнем с фразы, с которой не соглашается добрая половина разработчиков в индустрии: "каждый разработчик должен быть сам себе DevOps". Кто-то считает, что этим должен заниматься отдельно выделенный человек, чтобы у разработчика оставалась забота только о качестве кода. А кому-то свойственно думать о конвейере доставки кода в той же степени, как и о самом коде. Я же считаю, что в современных реалиях рынка и избытке инструментов/знаний разработчик должен уметь настроить и обслуживать конвейер быстрой и предсказуемой доставки артефакта в нужную ему среду. В отличие от мобильных разработчиков, для которых вопросы инфраструктуры и доставки приложения в большей степени решены самим вендором (Google и Apple), backend и web разработчики должны если не владеть, то хотя бы интересоваться практиками доставки кода.

И речь не идет о настройке каких-то больших и громоздких билд-систем, для которых обычно приносится в жертвую целая штатная единица. Нет. DevOps - не человек, а система ежедневных маленьких привычек, основанных на самоорганизации. Понятие, взрастающее снизу вверх, а не сверху или в бок. И если вы, как разработчик, смогли ускорить поток артефактов (любимое американцами понятие "Value Stream") на небольшой процент, то поздравляем - это уже DevOps way. Рекомендуем прочесть книгу "DevOps Handbook" by Gene Kim - лучшая книга для понимания этого концепта (ссылка в конце статьи).

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

Кто

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

  • 3 фронтенд разработчика с кучей пулл реквестов в день

  • 2 тестировщика, бастующие за улучшение QX (QA experience)

Что

Клиентское и администраторское web-приложения на Angular 9.0, собираемые из одного репозитория.

Где

Моя команда известна как ярый адепт продуктов Atlassian, поэтому вся экосистема нашего проекта живет в "австралийских облаках":

  • задачи и релизы в Jira

  • код в Bitbucket

  • CI в Bitbucket Pipelines

  • подробная документация в Confluence.

Наша команда использует стандартный план Bitbucket за $4/чел, включающий в себя 1500 минут сборки в Bitbucket Pipelines. О нем в сегодняшней статье и пойдет речь. Принцип работы и синтаксис настройки на 90 процентов похожи на Gitlab CI, поэтому любому пользователю Gitlab вся схема работы будет максимально понятной.

Сама система интернет банкинга разбита на микросервисы и работает в контейнерах на серверах Банка. Но в этой статье речь будет идти не о контейнерах, хотя настройка CI с помощью Docker-образов звучит очевидным.

Немного контекста

Первые наши шаги в DevOps и конкретно в улучшении QX (QA experience) мы начали задолго до этого в проектах мобильных приложений. Мы интегрировали между собой Jira, Bitbucket и сервис Bitrise.io во всех наших пулл-реквестах, что позволило иметь на выходе конкретный билд на каждый коммит по конкретной задаче. Для наглядности: тестировщик понимал, что пулл реквест 30 выдает билд приложения 170, в которой нужно тестировать Jira-задачу 500. Если вкратце описать процесс пулл-реквестов, то обязательными требованиями к слиянию пулл-реквеста являются

  • Зеленый билд на последнем коммите

  • Добро от разработчика-ревьюера

  • Добро от тестировщика

Если один из этих шагов давал красный свет, то пулл-реквест проходит все шаги заново.
Такой процесс позволяет нам обеспечить высокое качество кода и продукта в стабильной ветке репозитория. Мы с высокой долей уверенности можем релизить приложение, собранное с master (мы начали работать по trunk-based development и поэтому master наша стабильная ветка).

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

Подобного механизма пулл-реквестов никогда не существовало в web приложениях. Мы всегда делали приемку задач после слияния пулл-реквестов в стабильную ветку, из-за чего каждый третий коммит в ней был дефектным. Настроить такой же процесс приема пулл реквестов, как в мобилке, было для нас очевидным шагом. Сделать CI окружение для web приложения на инфраструктуре Банка было для нас слишком долгой историей, потому что хотелось настроить и поехать очень быстро. А все, кто работал с большими банками, почувствовал "скорость" продвижения задач по железу. Все процессы, что мы опишем в этой статье, мы планируем воссоздать в инфраструктуре банка с помощью оркестратора (Kubernetes или OpenShift, на усмотрение заказчика), но это уже другая история. В тот момент нам нужно было как можно быстрее начать работать правильно.

Первый очевидный вопрос: куда доставлять? Мы начали присматриваться к разным вариантам: Heroku, AWS, Netlify, Surge итд. В итоге остановились на использовании AWS S3. Для тех, кто думал, что S3 это всего лишь файловое хранилище - S3 может работать как сайт и его можно привязать к доменному имени. Подробнее об этом можно прочитать на страничке AWS.

Так почему же AWS?

  • Доступная цена. При всей репутации AWS как дорогой экосистемы, ежемесячные счета за S3 выходят в среднем 2 доллара при следующих метриках:

    • Новых ПР в день ~ 2

    • Пайплайнов в день ~ 12

    • Кол-во единовременно существующих бакетов ~ 5

    • Средний размер бакета = 13 Mb

  • У AWS отличный API и CLI. у "Surge" и других легковесных сервисов хостинга не настолько качественный и полноценный тулинг, как у Amazon AWS. Надо отметить, что CLI и документация Heroku не уступает Амазону, но высокий на наш взгляд порог вхождения и специфика работы Heroku Dynos заставили нас отойти от его выбора.

  • У команды уже был опыт работы с продуктами AWS.

Можно было бы настроить весь этот процесс в контейнерах в самом Amazon, но это повлечет за собой запуск EC2 машин. Даже с использованием Docker Hub вместо Elastic Container Registry, прогноз затрат вываливался у нас за $100 в месяц. В конечном итоге у нас получилась именно та схема работы с пулл-реквестами, которую мы представляли себе в самом начале. Но давайте проанализируем каждую ступень нашей эволюции и посмотрим на принятые решения.

Уровень 1: создание S3 бакета

Мы начали с того, что создали по одному выделенному S3 bucket для хостинга клиентского и админского приложений. Настроили конфигурацию сборки нашего проекта (bitbucket-pipelines.yml), чтобы он собирал приложения (html/css/js/img) и заливал их в соответствующий S3 bucket. В начале был использован AWS CLI, но, как оказалось, Bitbucket предоставляет набор готовых официальных Pipes (аналог Github actions), среди которых оказался Pipe для выгрузки файлов в S3 bucket. В итоге: тестировщик имеет сайт, на котором он может проверить реализацию задачи пулл-реквеста с постоянной условной ссылкой web.s3-website.ap-northeast-2.amazonaws.com.

Обязательным предварительным шагом при создании бакета через консоль AWS является включение опции "Enable static hosting" в настройках бакета. Без этой опции bucket является просто файловым хранилищем.

- step:      name: Build and deploy webadmin PR version into AWS for QA      caches:        - node      script:        # начальная конфигурация        - apk update && apk add git        - npm install        # сборка        - npm run build:admin        - cd dist/admin        # загрузка в S3        - pipe: atlassian/aws-s3-deploy:0.2.4          variables:            AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID            AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY            AWS_DEFAULT_REGION: $AWS_DEFAULT_REGION            S3_BUCKET: $S3_WEBADMIN_BUCKET_NAME            DELETE_FLAG: 'true'            LOCAL_PATH: $(pwd)            ACL: 'public-read'

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

Оценка:

  • за старания - четверка

  • за QX - двойка

Уровень 2: выделение S3 bucket под каждого автора

В ответ на обратную связь от тестировщиков командой было решено выделить по одному S3 bucket на каждого фронтенд разработчика. В нашем проекте были разработчики Манар, Миша - следовательно были созданы условные S3 бакеты jsn-web-manar и jsn-web-michael. В bitbucket-pipelines.yml в step для пулл-реквестов была добавлена логика определения конечного S3 бакета в зависимости от PR автора.

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

  1. Гонка пулл-реквестов одного автора. Если один и тот же разработчик создаст 3 параллельных пулл-реквеста, то все они вызовут запуск пайплайна сборки. Мы не можем точно знать, какой из пайплайнов закончится быстрее. Команде, в частности тестировщику, без использования консоли Chrome сложно понять, какой из пулл-реквестов сейчас развернут на S3 бакете разработчика Михаила.

  2. Появление нового автора. В наших репозиториях создавать пулл-реквест может любой член команды, поэтому эта схема сломалась ровно в тот момент, когда ПР создал кто-то, кроме фронтенд-разработчиков. По нашей тривиальной логике определения бакета его запущенный пайплайн "угонит" S3 бакет одного из разработчиков. В итоге другой тестировщик может потерять version-under-test сайт прям в момент тестирования.

  3. Смена никнейма. Наши разработчики забавы ради любят менять свои git author name время от времени. Для нас это никогда не являлось проблемой до того, как мы применили логику с бакетами на каждого автора. К сожалению, Bitbucket Pipelines из коробки не предоставляют возможности определения автора по его Jira account, поэтому в логике присвоения бакета пришлось оперировать стандартным commit git author. Как вы и сами догадались, при смене имени с "Manar Kurmanov" на "Dark Lord" повторилась ситуация из пункта 2 - был угнан бакет другого разработчика.

С этой шаткой схемой команда прожила еще несколько месяцев.

Оценка:

  • за старания - четверка

  • за QX - тройка

Уровень 3: добавление штампа авторства в web приложение

Команда решила проблему гонки пулл-реквестов добавлением пояснительного текста в footer сайта:

Каждый пайплайн добавлял в футер сайта название ветки, автора и timestamp. Таким образом решалась проблема параллельных пулл-реквестов от одного автора - тестировщик четко понимает, какая Jira-задача разработчика Георгия представлена в бакете.

Фрагмент из bitbucket-pipelines.yml

- step:    name: Build PR version    caches:      - node    script:      # initial configuration      - apk update && apk add git      - npm install      # preparing site footer text      - TIMESTAMP_FILE="./src/app/some/folder/copyright.timestamp.html"      - GIT_AUTHOR=$(git log -n 1 --format=format:'%an')      - PR_URL="$BITBUCKET_GIT_HTTP_ORIGIN/pull-requests/$BITBUCKET_PR_ID"      - BRANCH_TEXT="PR branch <a href=\\"$PR_URL\\">$BITBUCKET_BRANCH</a><br>"      - echo $BRANCH_TEXT >> $TIMESTAMP_FILE      - echo "Author $GIT_AUTHOR<br>" >> $TIMESTAMP_FILE      - echo "Built at $(TZ=UTC-6 date '+%d-%m-%Y %H:%M') <br>" >> $TIMESTAMP_FILE      - echo "</small>" >> $TIMESTAMP_FILE      - cat $TIMESTAMP_FILE > src/app/target/folder/copyright.component.html      # building artefacts      - npm run build    artifacts:      paths:        # кеширование артефактов для следующего Build Step         - dist/web/**

Казалось бы, +100 к QX, куда еще прозрачнее. Но поставьте себя на место тестировщика в ежедневной работе и вы поймете еще одно скрытое неудобство. Допустим, что разработчик создал 3 параллельных пулл-реквеста и тестировщик проверил сайт на S3 бакете. Что он должен делать дальше? Тестировщику не очевидно, что он находится в ситуации очереди ПР-ок на один и тот же S3 бакет. После он должен зайти в странице Pipelines, найти нужную ветку и сделать ручной Rerun.

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

Оценка:

  • за старания - четверка

  • за QX - тройка с плюсом

Уровень 4: динамичные бакеты под каждый пулл реквест

Мы решил копнуть глубже в возможности AWS API и воссоздать поведение динамических сред для тестировщиков и разработчиков. Какие были требования:

  • Каждый пулл реквест должен породить свой отдельный S3 бакет и задеплоить сайт туда.

  • Нужно, чтобы в комментарий к пулл-реквесту писалась ссылка на этот бакет при каждом новом билде.

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

Для реализации этих требований не хватало стандартных Bitbucket Pipes, поэтому нужно было писать кастомные скрипты для взаимодействия с AWS S3. К счастью Bitbucket Pipelines, как и многие CI системы, является cloud-first и предоставляет возможность запускать свои пайплайны на базе любого публичного Docker образа. Мы использовали официальный образ aws-cli, включающий в себя AWS CLI и все базовые утилиты командной строки (curl, sed, xargs).

Ниже фрагмент из bitbucket-pipelines.yml по загрузке статики сайта в динамический бакет. NOTE: в скрипте используются ключи и секреты из учетной записи AWS S3, их можно сгенерировать по официальной инструкции.

- step:    name: Deploy PR version into AWS bucket for QA    image:      name: amazon/aws-cli    script:      # 1. Настройка сессии в aws cli с помощью ключей      - aws configure set aws_access_key_id=$AWS_ACCESS_KEY_ID aws_secret_access_key=$AWS_SECRET_ACCESS_KEY      # 2. определяем название для динамического бакета      - export BUCKET_NAME=web-pullrequest-$BITBUCKET_PR_ID      # 3. если в AWS нету бакета с таким названием, создаем его с нужными флагами      - if [ -z $(aws s3 ls | grep $BUCKET_NAME) ]; then aws s3api create-bucket --bucket $BUCKET_NAME --acl public-read --region ap-northeast-2 --create-bucket-configuration LocationConstraint=ap-northeast-2; fi      # 4. задаем это бакету настройку статичного хостинга      - aws s3api put-bucket-website --website-configuration "{\\"ErrorDocument\\":{\\"Key\\":\\"error.html\\"},\\"IndexDocument\\":{\\"Suffix\\":\\"index.html\\"}}" --bucket $BUCKET_NAME      # 5. очищаем содержимое бакета      - aws s3 rm s3://$BUCKET_NAME --recursive       # 5. заливаем в него собранные html/css/js      - aws s3 cp dist/web s3://$BUCKET_NAME --acl public-read --recursive      # 6. Пишем коммент со ссылкой от имени сервисной учетки в нужный пулл реквест      - export PR_API_URL=https://api.bitbucket.org/2.0/repositories/$BITBUCKET_REPO_FULL_NAME/pullrequests/$BITBUCKET_PR_ID/comments      - export BUCKET_PUBLIC_URL=http://$BUCKET_NAME.s3-website.ap-northeast-2.amazonaws.com      - curl $PR_API_URL -u $CI_BB_USERNAME:$CI_BB_APP_PASSWORD --request POST --header 'Content-Type:application/json' --data "{\\"content\\":{\\"raw\\":\\"[http://$BUCKET_NAME.s3-website.ap-northeast-2.amazonaws.com](http://personeltest.ru/away/$BUCKET_NAME.s3-website.ap-northeast-2.amazonaws.com)\\"}}"

В качестве автора комментарий в пулл реквест мы использовали нашу сервисную учетную запись для CI с использованием App-specific password. В этой статье от Atlassian можно узнать, как создать такой пароль.

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

"Единственный ручной процесс в этой схеме - чистка неиспользуемых S3 бакетов раз в неделю. Зачем это автоматизировать?" - подумали мы. Но по закону жанра команда благополучно забывала подчищать бакеты и вспомнила об этом только после того, как бухгалтер показал счет на 25 долларов от AWS из-за скопившихся бакетов.

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

- step:    name: Remove dangling s3 buckets left after PR merges    image:        name: amazon/aws-cli    script:      # 1. Запросить список 10 последних MERGED пулл реквестов      - export API_URL="<https://api.bitbucket.org/2.0/repositories/$BITBUCKET_REPO_FULL_NAME/pullrequests?state=MERGED>"      - curl "$API_URL" -u $CI_BB_USERNAME:$CI_BB_APP_PASSWORD > pr_list.json      # 2. выделить бакеты, соответствующие спец-формату       - aws s3 ls | grep -o '[a-zA-Z\\-]\\+pullrequest\\-[0-9]\\+' > buckets.txt- set +e      # очистить все бакеты с номер ПР-ок, которые уже MERGED      # (AWS API требует очистки бакета перед его полным удалением)      - echo "$(cat pr_list.json | grep -o '"id":\\s[0-9]\\+')" | sed 's/[^0-9]//g' | xargs -I{} grep {} buckets.txt | xargs -I{} aws s3 rm s3://{} --recursive      # удалить все бакеты с номер ПР-ок, которые уже MERGED      - echo "$(cat pr_list.json | grep -o '"id":\\s[0-9]\\+')" | sed 's/[^0-9]//g' | xargs -I{} grep {} buckets.txt | xargs -I{} aws s3api delete-bucket --bucket {}

Оценка:

  • За старания пятерочка

  • за QX - четверка с плюсом. Почему не пять? Потому что на своей шкуре мы поняли, что улучшение любого X (QX, DevX, HX) - это бесконечный процесс

Технические ремарки

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

#1: По поводу CORS

Так как API запросы совершаются с одного хоста (.amazonaws.com) на другой хост (*.somebank.com), по умолчанию они будут блокироваться браузером из-за настроек CORS (cross origin resource sharing) сервера. Если вкратце, то браузер позволяет отправлять запросы только из того же хоста, откуда сайт был запрошен. Для примера, API на api.server.com будет принимать запросы только с сайта server.com. При попытке сделать GET запрос с сайта another.com браузер сначала совершит "pre-flight" запрос на сервер и поймет, что сервер строго выдерживает правило "same-origin-policy".

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

Access-Control-Allow-Origin: <http://bucket.s3-website.amazonaws.com># илиAccess-Control-Allow-Origin: *

Во всех популярных фреймворках есть поддержка управления Cross Origin.

#2: По поводу расходов

В уровне 4 в скрипте присутствует строка очистки содержимого бакета:

aws s3 rm s3://$BUCKET_NAME --recursive 

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

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

ВАЖНО! Если в вашем проекте будет использоваться долгоживущий S3 bucket и вы будете использовать официальный aws-s3-deploy pipe, то убедитесь, что вы используете DELETE_FLAG. Этот флаг очищает bucket перед очередной выгрузкой файлов. Во время уровня #1 наша команда об этом флаге не знала в течение 2 месяцев и узнала только после обнаружения нескольких тысяч файлов в одном бакете. Поэтому парочку десяток американских долларов было сожжено во имя наших познаний.

# вызов пайпа загрузки файлов в S3 с флагом DELETE_FLAG- pipe: atlassian/aws-s3-deploy:0.2.4    variables:      AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID      AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY      AWS_DEFAULT_REGION: $AWS_DEFAULT_REGION      S3_BUCKET: $S3_WEBADMIN_BUCKET_NAME      DELETE_FLAG: 'true' # не забыть этот флаг      LOCAL_PATH: $(pwd)      ACL: 'public-read'

Вывод

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

Финальную версию bitbucket-pipelines.yml можно посмотреть в github репозитории.

Материалы к прочтению

Подробнее..
Категории: S3 , Angular , Devops , Frontend , Amazon web services , Aws , Cicd , Pipelines , Bitbucket

Настраиваем Continuous Integration для Jenkins и Bitbucket с werf

18.12.2020 12:07:33 | Автор: admin


Утилита werf создана так, чтобы её было легко интегрировать с любыми CI/CD-системами. Подробнее об этом процессе в общем случае читайте в эпилоге этой статьи, но основное её содержимое практический пример по организации CI в Jenkins и Bitbucket.

Подразумевается, что в результате наших действий мы ожидаем получить следующее:

  1. Shared Library для Jenkins, чтобы все сценарии CI хранились в одном месте и их можно было править единым коммитом.
  2. Интеграцию Jenkins с Bitbucket, чтобы запускать CI по коммиту в определенные ветки или по созданию тега.

Поехали!

Конфигурация Jenkins


Для реализации задуманного в статье будут задействованы:


В Jenkins для проектов используется multibranch pipeline.

Начнем с того, что подключим к Jenkins репозиторий, в котором будет храниться наша Shared Library. Shared Library это единая библиотека, что может содержать в себе код для исполнения CI и хранится отдельно в своем собственном репозитории. Это значительно упрощает процесс модернизации и работы над CI (вместо использования для хранения CI стандартного Jenkinsfile, который нужно подкладывать в каждый проект).

Итак, подключаем: Manage Jenkins Configure System Global Pipeline Libraries.



Нужно указать имя, ветвь репозитория, из которой Jenkins будет забирать код библиотеки, а в Source Code Management указать адрес и доступ до репозитория (в нашем случае SSH-ключ для доступа ReadOnly).

Структура Shared Library


Теперь приступим к описанию самой библиотеки. Структура очень проста и состоит всего из трёх директорий:



  • vars директория для глобальных методов библиотеки, что будут вызываться из пайплайна;
  • src тоже директория для скриптов, но в основном используется для вашего кастомного кода;
  • resources всё, что не является скриптом и может понадобиться в исполнении.

Для наших целей в Jenkins будет достаточно только нескольких методов в директории vars, потому как мы настроим сам werf, что и сделает всю основную работу.

К тому же, хотелось бы, чтобы весь пайплайн был полностью описан внутри библиотеки, а в Jenkinsfile мы передавали только некоторые параметры деплоя, которые в 99,9% случаев вообще не будут меняться.

Реализуем методы


Итак, реализуем 2 метода.

Для вызова утилиты werf -runWerf.groovy.

#!/usr/bin/env groovydef call(String dockerCreds, String werfargs){  // логин в registry  // первый аргумент - url (пуст, т.к. используем DockerHub)  // второй - имя Jenkins-секрета, где лежат доступы (login, password)  docker.withRegistry("", "${dockerCreds}") {    sh """#!/bin/bash -el          set -o pipefail          type multiwerf && source <(multiwerf use 1.1 stable --as-file)          werf version          werf ${werfargs}""".trim()    }}

Все параметры в библиотеку для пайплайна передаются как Map, что удобно:

#!/usr/bin/env groovydef call( Map parameters = [:] ) { // функция принимает в качестве аргумента Map с параметрами  def namespace = parameters.namespace // имя неймспейса для выката  // имя ключа по умолчанию для расшифровки секретов (если не указан в параметрах)  def werf_secret_key = parameters.werfCreds != null ? parameters.werfCreds : "werf-secret-key-default"  // имя секрета по умолчанию для логина в docker registry  def dockerCreds = parameters.dockerCreds != null ? parameters.dockerCreds : "docker-credentials-default"  // получаем имя проекта из имени multibranch pipeline  def PROJ_NAME = "${env.JOB_NAME}".split('/').first()  // имя registry в docker hub или адрес до кастомного registry  def imagesRepo = parameters.imagesRepo != null ? parameters.imagesRepo : "myrepo"  if( namespace == null ) { // единственный обязательный аргумент и проверка на его наличие    currentBuild.result = 'FAILED'    return  }  pipeline {    agent { label 'werf' }    options { disableConcurrentBuilds() } // запрещаем параллельную сборку для пайплайна    environment { // переменные для работы werf      WERF_IMAGES_REPO="${imagesRepo}"             WERF_STAGES_STORAGE=":local"      WERF_TAG_BY_STAGES_SIGNATURE=true      WERF_ADD_ANNOTATION_PROJECT_GIT="project.werf.io/git=${GIT_URL}"      WERF_ADD_ANNOTATION_CI_COMMIT="ci.werf.io/commit=${GIT_COMMIT}"      WERF_LOG_COLOR_MODE="off"      WERF_LOG_PROJECT_DIR=1      WERF_ENABLE_PROCESS_EXTERMINATOR=1      WERF_LOG_TERMINAL_WIDTH=95      PATH="$PATH:$HOME/bin"      WERF_KUBECONFIG="$HOME/.kube/config"      WERF_SECRET_KEY = credentials("${werf_secret_key}")    }    triggers {      // Execute weekdays every four hours starting at minute 0      cron('H 21 * * *')     // для werf cleanup, что будет чистить registry и хост-раннер от устаревших кэшей и образов    }    stages {      stage('Checkout') {        steps {          checkout scm // получаем код из репозитория        }      }      stage('Build & Publish image') {        when {            not { triggeredBy 'TimerTrigger' } // чтобы stage не запускался по крону        }        steps {          script {            // запуск нашего метода из runWerf.groovy            runWerf("${dockerCreds}","build-and-publish")          }        }      }      stage('Deploy app') {        when {            not { triggeredBy 'TimerTrigger' }          }        environment {          // название окружения, куда осуществляется деплой (важно для шаблонизации Helm-чарта)          WERF_ENV="production"        }        steps {          runWerf("${dockerCreds}","deploy --stages-storage :local --images-repo ${imagesRepo}")        }      }      stage('Cleanup werf Images') {        when {          allOf {            triggeredBy 'TimerTrigger'            branch 'master'           }        }        steps {          sh "echo 'Cleaning up werf images'"            runWerf("${dockerCreds}","cleanup --stages-storage :local --images-repo ${imagesRepo}")        }      }    }  }}

Примечания:

  • Сборка и выкат происходят для любой ветки, указанной в секции discover у Jenkins. После наших манипуляций в следующей главе это будет происходить автоматически.
  • Все секреты, такие как werf-secret-key-default и docker-credential-default, хранятся в Jenkins Credentials:



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

@Library('common-ci') _multiStage ([namespace: 'yournamespace'])

Имя метода это название файла в каталоге vars.

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

Пример реализации:

def namespace = "test"def werf_env = "test"if (env.JOB_BASE_NAME == 'master') { namespace = "stage" werf_env = "stage"}if (env.TAG_NAME) { namespace = "production" werf_env = "production"}# и добавляем в environment стадииenvironment {  WERF_ENV="${werf_env}" }

Если вы хотите автоматический запуск stage со всех веток, а с тегов в production только при нажатии кнопки в Jenkins, то можно использовать такое условие: currentBuild.rawBuild.getCauses()[0].toString().contains('UserIdCause'). Оно позволяет отследить, сборка была запущена человеком или началась как событие от webhook'а.

Триггеры по коммитам из Bitbucket


По умолчанию Jenkins сам не умеет интегрироваться в Bitbucket. Для этого нужно установить уже упомянутые плагины:

  • Bitbucket Branch Source Plugin добавляет Bitbucket как source для multibranch pipeline;
  • Basic Branch Build Strategies Plugin позволит запуск тегов по webhook. По умолчанию Jenkins не позволяет любые автоматизированные действия с тегами, т.к. не понимает какой из тегов последний.

Если вы используете cloud-версию Bitbucket, то нужно только поставить разрешение на создание webhook'ов автоматически.

Также требуется создать служебного пользователя с доступом к репозиториям, т.к. Jenkins будет обнаруживать весь репозиторий через API. Это касается настройки как для cloud-версии, так и для собственного Bitbucket-сервера.

Пример из глобальных настроек Jenkins:



Далее понадобится настроить source в Multibranch Pipeline, что происходит в интерактивном режиме. Это означает, что, когда вы добавите credentials bitbucket пользователя и имя команды или пользователя с проектами, которых мы будем работать, Jenkins найдет все доступные пользователю репозитории и позволит выбрать один из списка.

В самом репозитории мы полагались на поиск только определенных веток, т.к. не уверены, как много веток может быть, а Jenkins может надолго задуматься, если начнет исследовать каждую ветку. Это накладывает определенные ограничения, т.к. теги тоже попадают под регулярное выражение. Однако Java Regular Expressions довольно гибкие, так что большой проблемы нет.

Альтернативный путь: если есть желание совсем отделить теги от веток, можно добавить еще один абсолютно такой же Source в репозиторий и настроить его только на обнаружение тегов.

Итак, конфигурация:



После этого Jenkins с помощью сервис-аккаунта сам сходит в Bitbucket и создаст webhook:



Теперь при каждом коммите Bitbucket будет триггерить пайплайны (но только для тех веток и тегов, что мы отфильтровали) и даже посылать статус пайплайна обратно в Bitbucket в последнем столбце коммита:


Статусы кликабельные: при нажатии перекидывают в нужный пайплайн в Jenkins

Последний штрих про Jenkins, который находится за nginx proxy и работает с определенного location. Тогда нужно в основных настройках исправить его location, чтобы он сам знал, как выглядит его endpoint:



Без этого ссылки на pipeline в Bitbucket будут генерироваться некорректно.

Заключение


В статье рассмотрен вариант настройки CI с использованием Jenkins, Bitbucket и werf. Это очень общий пример, который не является панацеей для организации процесса разработки, однако даёт представление о том, как вообще подойти к построению своего CI с использованием werf.

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

У данного подхода также есть большой плюс это гибкость. Мы буквально можем прописать в CI всё что угодно. Хотя и порог вхождения для того, чтобы понимать, как именно это сделать, чуть выше, чем у других CI-систем.

Эпилог: про werf и CI/CD в целом


Общий подход к интеграции werf с CI/CD-системами описан в документации. Вкратце рекомендуемые для любых проектов шаги сводятся к следующим:

  1. Создание временного DOCKER_CONFIG для исключения конфликтов между параллельными job'ами на одном runner'е (подробнее здесь).
  2. Выполнение авторизации Docker для используемых Docker Registry. Это может быть родная реализация Docker Registry внутри CI-системы либо какая-то сторонняя. В случае со встроенными имплементациями (к примеру, GitLab Container Registry или GitHub Docker Package) все необходимые параметры доступны среди переменных окружения. Выполнять авторизацию для альтернативных registry можно вручную на каждом runner'е или через параметры, хранящиеся в секретах (также для каждого job'а).
  3. Простановка WERF_IMAGES_REPO, WERF_STAGES_STORAGE, а также необходимых параметров, которые варьируются в зависимости от имплементации. Утилита werf должна знать, с какой реализацией работает, так как часть требует использования нативного API. Стоит отметить, что по умолчанию werf пытается определить, с какой имплементацией работает, исходя из адреса registry, но это задача часто невыполнима (и тогда требует явного указания имплементации).
  4. Простановка опций тегирования WERF_TAG_*: используя переменные окружения CI, определяем, чем инициирован текущий job, и выбираем подходящую опцию тегирования или всегда используем content-based тегирование (рекомендованный путь).
  5. Использование окружения CI-системы для последующего использования при выкате. Для понимания environment в GitLab.
  6. Простановка автоматических аннотаций для всех выкатываемых ресурсов WERF_ADD_ANNOTATION_*. Среди этих аннотаций могут быть произвольные данные, которые помогут вам работать и отлаживать ресурсы приложения в Kubernetes. Мы пришли к тому, что все ресурсы должны содержать следующий набор:
    1. WERF_ADD_ANNOTATION_PROJECT_GIT адрес проекта в Git;
    2. WERF_ADD_ANNOTATION_CI_COMMIT коммит, соответствующий выкату;
    3. WERF_ADD_ANNOTATION_JOB или WERF_ADD_ANNOTATION_PIPELINE адрес job или pipeline (зависит от CI-системы и желания), который связан с выкатом.
  7. Простановка по умолчанию комфортной работы с логом werf:
    1. WERF_LOG_COLOR_MODE=on включение цветного вывода (werf запускается не в интерактивном терминале, по умолчанию цвета отключены);
    2. WERF_LOG_PROJECT_DIR=1 вывод полного пути директории проекта;
    3. WERF_LOG_TERMINAL_WIDTH=95 установка ширины вывода (werf запускается не в интерактивном терминале, по умолчанию ширина равна 140).

За время применения werf в большом количестве проектов у нас сформировался набор решений, который унифицирует конфигурацию, решает общие проблемы и делает сопровождение проще и нагляднее. В настоящий момент все описанные выше шаги с учетом этих решений уже встроены в команду werf ci-env для GitLab CI/CD и GitHub Actions. Пользователям других CI-систем необходимо реализовывать аналогичные действия самостоятельно подобно тому, как описано в этой статье для примера с Jenkins.

P.S.


Читайте также в нашем блоге:

Подробнее..

Категории

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

  • Имя: Макс
    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