Привет, Хабр! Меня зовут Кирилл Шеховцов, и я технический лидер в SberCloud. Сегодня расскажу, как быстро интегрировать пайплайн Continuous Integration/Continuous Delivery (CI/CD) и продуктовый подход к проектированию приложений в облаке SberCloud.Advanced. Эта обзорная статья будет полезна начинающим ИТ-специалистам и предпринимателям, которые только осваивают работу в облаке. Я составил материал в виде пошаговой инструкции, которой могут воспользоваться как крупные, так и небольшие команды разработчиков.
![](https://habrastorage.org/getpro/habr/upload_files/f6c/6e0/8aa/f6c6e08aad9250266502725baa161f79.jpg)
Что нам потребуется
Работа над проектом в рамках CI/CD включает семь этапов: написание кода, сборку, тестирование, релиз, развертывание, поддержку и мониторинг, плюс — планирование доработок. Благодаря неразрывному алгоритму, методология позволяет команде выпускать обновления для приложений в течение нескольких недель и даже дней. При классическом подходе на это могут уходить месяцы. Поэтому задействуем CI/CD.
Мы будем работать с сервисами SberCloud.Advanced — пара слов о каждом из них:
FunctionGraph — позволяет размещать и запускать код в бессерверной среде по заданному триггеру, автоматически масштабируется.
API Gateway — единая точка входа в наш бэкенд для API.
Cloud Container Engine — для управления приложениями в среде Kubernetes.
Software Repository for Container — хранилище образов docker-контейнеров.
Cloud Performance Test Service — позволяет проводить нагрузочное тестирование.
Писать демонстрационное приложение будем на React, в частности, используем свежеустановленный Next.js (установить его можно при помощи npx create-next-app@latest). Для того, чтобы у приложения появился бэкенд, воспользуемся FunctionGraph, а также протестируем наше приложение при помощи Cloud Performance Test Service.
Переходим к коду
Первым делом откроем среду разработки и установим зависимости. Для этого в консоли выполним команду npm i. Таким образом, наше приложение проинициализировалось.
![](https://habrastorage.org/getpro/habr/upload_files/751/598/55c/75159855cffb6e51c937b427f90eb9ae.jpg)
После инициализации приложение можно запустить локально и разместить код в репозитории. В данном случае его структура стандартна для Next.js-приложений, а единственное дополнение, которое я внес — dockerfile с инструкциями по сборке.
![](https://habrastorage.org/getpro/habr/upload_files/325/b82/a28/325b82a285ff97949742057f3d71fb0f.jpg)
В этом docker-файле я отметил, что приложение будет работать со средой node:16-buster-slim. Затем указал рабочую директорию инструкцией WORKDIR и скопировал все содержимое данного приложения директорию /opt/front. Далее, установил зависимости командой RUN и вновь обратился к среде node:16-buster-slim: фактически я использую многоэтапную сборку (multi-stage build). Этот шаг помогает сократить размер финального образа, так как по умолчанию NODE-зависимости «тянут» за собой достаточно большое количество дополнительных зависимостей (так называемые peerDependencies). Повторное обращение к инструкции FROM позволяет сократить число ненужных объектов в финальном образе. Так, мы подключаем только те зависимости, которые нужны для запуска приложения. Обращаться к нему мы будем через порт 3000.
Перейдем к настройке Jenkins. Его я развернул в Elastic Cloud Service в облаке SberCloud. В разделе, который называется «Управление исходным кодом» подключаем нашу git-среду — указываем ссылку на наш репозиторий smartdev21.git. В качестве типа подключения выбираем HTTPS, хотя есть и другие варианты — SSL или SSH.
![](https://habrastorage.org/getpro/habr/upload_files/abc/e97/719/abce97719b49ef0bd709646a5c66fb4a.jpg)
В выпадающем списке поля Credentials выберем данные учетной записи, необходимой для подключения к репозиторию с контейнером.
![](https://habrastorage.org/getpro/habr/upload_files/ce6/448/bab/ce6448bab683ee554ded2c9a22fae3ba.jpg)
Далее, настроим работу триггеров, являющихся неотъемлемой частью CI/CD-пайплайна. Они реагируют на изменения в master-ветке, чтобы Jenkins автоматически производил сборку кода. В блоке «Триггеры сборки» установим галочку напротив пункта «Опрашивать SCM об изменениях» и в поле ниже пропишем следующее условие: * 10-19 * * 1-5. Оно означает, что система будет просматривать репозиторий на наличие изменений с 10 часов утра до 19 часов вечера с понедельника по пятницу, то есть каждый рабочий день.
![](https://habrastorage.org/getpro/habr/upload_files/cc1/5a8/97a/cc15a897a7abf19525211e69df43fbd5.jpg)
Сразу после этого отметим галочкой пункты Delete workspace before build starts и Use secret. Первая настройка укажет системе очищать наше рабочее пространство перед каждой новой сборкой, а вторая — запретит вывод токенов или паролей в логи.
Далее прописываем в Jenkins имя пользователя и пароль от контейнера с нашим приложением.
![](https://habrastorage.org/getpro/habr/upload_files/371/8bf/acb/3718bfacb6835373cf0ca6ef4e1ad319.jpg)
На этой же странице задаем последовательность shell-команд для этой операции. Первая команда осуществляет сборку:
docker build -t smartdev: $BUILD_NUMBER
Второй набор команд отправляет образ в облачный репозиторий – сервис SWR, Software Repository – с адресом в нашем облаке:
docker tag smartdev:$BUILD_NUMBER swr.ru-moscow-1.hc.sbercloud.ru/devconf/smartdev:$BUILD_NUMBER
docker login -u ru-moscow-1@$SWR_USER -p $SWR_PASS https://swr.ru-moscow-1.hc.sbercloud.ru
docker push swr.ru-moscow-1.hc.sbercloud.ru/devconf/smartdev:$BUILD_NUMBER
docker logout
![](https://habrastorage.org/getpro/habr/upload_files/f05/790/c61/f05790c615fd3f1f030a36d979553fe7.jpg)
Наконец, третья команда обновляет приложение, развернутое в Kubernetes (сервис ССЕ, Cloud Container Engine), устанавливая новый образ контейнера:
kubectl set image deployment/smartdev21 container-0=100.125.13.65:20202/devconf/smartdev:$BUILD_NUMBER
Функция FunctionGraph
В нашем приложении создадим функцию из FunctionGraph. Первым делом на панели управления SberCloud.Advanced выбираем одноименный пункт во вкладке Computing.
![](https://habrastorage.org/getpro/habr/upload_files/904/a36/f5e/904a36f5e11c6b130eaf716dceeb8809.jpg)
Чтобы создать новую функцию, в открывшемся окне нажимаем Create Function (правый верхний угол).
![](https://habrastorage.org/getpro/habr/upload_files/ec0/aa4/e20/ec0aa4e204399d722c44ad268c2cd9b5.jpg)
Появится страница, на которой нам нужно прописать название функции — в моем случае это smartdev21. В поле Runtime выбираем среду выполнения, пусть это будет версия Node.js 12.13.
![](https://habrastorage.org/getpro/habr/upload_files/f98/c08/1d3/f98c081d35f36620e76110eb981a013c.jpg)
В конце страницы вы можете видеть небольшое окно, напоминающее среду разработки. Здесь уже сформирован код express-приложения (в терминологии Node.js). По умолчанию к нему на вход поступают переменные event и context. Нам они не нужны, поэтому мы их заменим.
![](https://habrastorage.org/getpro/habr/upload_files/f67/bc3/c77/f67bc3c7741a9d604b20d6fffc1c6f92.jpg)
Пропишем в операторе JSON.stringify следующую строчку — response: Math.random(). Она генерирует случайное числовое значение. В рамках туториала условимся, что это будет единственной задачей создаваемой нами функции.
![](https://habrastorage.org/getpro/habr/upload_files/503/3ac/bd5/5033acbd59d0b94d42dcb50bdd6802f5.jpg)
Далее, добавим в код обработку CORS-запросов, так как наше приложение работает по HTTPS в среде localhost. В блоке headers укажем название заголовка: Access-Controll-Allow-Origin и присвоим ему значение ‘*’. Такой подход позволит нам принимать заголовки со всех адресов. Далее, нажимаем на Create Now, на чем заканчиваем описание функции FunctionGraph.
В окне только что сформированной функции есть вкладка Configuration. В ней вы можете добавить описание, увеличить объем оперативной памяти или установить таймаут на выполнение. Однако в настоящий момент нас интересует вкладка Triggers, где мы подключим триггер. Переходим в неё и нажимаем Create Trigger.
В выпадающем меню выбираем тип триггера — API Gateway, и даем ему имя. Я решил оставить предложенное системой по умолчанию. В этом же окне в поле Security Authentication ставим значение None, чтобы отключить авторизацию. Я это делаю исключительно в демонстрационных целях, чтобы упростить дальнейшую настройку — на реальной системе, разумеется, так делать нельзя. На практике чаще всего применяют авторизацию через App или IAM.
![](https://habrastorage.org/getpro/habr/upload_files/dd6/9cf/987/dd69cf98736462c52afc8ae5038ef736.jpg)
После нажатия на клавишу OK, в правом верхнем углу появится уведомление об успешном создании триггера — он отобразится на главном экране. В строке URL показан адрес нашей функции.
![](https://habrastorage.org/getpro/habr/upload_files/723/4ed/b38/7234edb38fb7530a37c00754d41e9f18.jpg)
Если сейчас вставить его в адресную строку браузера, на странице появится надпись response со случайным числом.
Передаем данные из FunctionGraph в React
Вернемся в среду разработки и настроим импорт двух хуков — useEffect и useState. Первый позволяет выполнять сторонние эффекты внутри компонентов React, а второй позволяет управлять состоянием.
![](https://habrastorage.org/getpro/habr/upload_files/35c/955/0f8/35c9550f8ecbd5b480b17ee4df4c8def.jpg)
Импортируем useEffect с помощью import. Эта функция в качестве параметра будет получать массив зависимостей. Если мы изменим его значение, то сработает хук useEffect. Далее, добавляем state с названием data. Прописываем его как [data, setData], а базовое значение устанавливаем в null при помощи useState.
![](https://habrastorage.org/getpro/habr/upload_files/eae/484/c62/eae484c624559c1b207a00d356ec0446.jpg)
Далее, в useEffect используем fetch. Это интерфейс для работы с web-запросами, который пришел на замену AJAX или XHR. Далее, нам необходимо получить из нашего API Gateway значение res — это ответ функции fetch. Если он придет в формате JSON, то его нужно расшифровать и привести к JS-объекту с помощью return res.json(). Наконец, осталось выполнить then, и установить объект как setData.
Добавим в наш код обработку ошибок — отлавливать нештатные ситуации будем с помощью анонимной функции catch. Чтобы вывести информацию о сбое в консоль, пропишем console.error, а setData присвоим сообщение ошибки. Если такого сообщения нет, будем получать его из поля err?.error.
![](https://habrastorage.org/getpro/habr/upload_files/8fa/423/632/8fa42363211b8bdc8fd91d73f3dea297.jpg)
Вы могли заметить, что я использовал символы ‘?’. Это — синтаксис в JS, реализующий операцию опциональной последовательности. Она позволяет получить значение свойства, находящегося на любом уровне вложенности в цепочке связанных между собой объектов. Таким образом, если запись err?.message вернет нам null или undefined, мы перейдем к отображению ошибки. Чтобы перестраховаться, я также добавил вывод фразы «Непредвиденная ошибка» на тот случай, если причина сбоя окажется неизвестна.
Нам осталось в блоке code запросить из data наш response — именно так назывался ключ в FunctionGraph, куда мы записали случайное математическое значение. Если он по какой-то причине отсутствует, выводим сообщение об ошибке.
![](https://habrastorage.org/getpro/habr/upload_files/83e/528/ae5/83e528ae52de2de909a00450b964e9ce.jpg)
Проверим работу кода, запустим его на локальном сервере. Страница браузера поприветствует нас следующим сообщением:
![](https://habrastorage.org/getpro/habr/upload_files/39d/124/ac7/39d124ac72c50bbc437b8c05f7b95420.jpg)
Переходим к релизу
Теперь зальем наш код в репозиторий и проведем релиз. Для этого в консоли среды разработки прописываем git-коммит с флагом -am, обозначающим все модифицированные файлы, и даем ему название (в моем случае это Update).
![](https://habrastorage.org/getpro/habr/upload_files/868/a49/8d3/868a498d3c5b4a260a9a77f9ed629f77.jpg)
Далее, при помощи команды git push отправляем их в репозиторий.
Перейдем в Jenkins и посмотрим на состояние сборки. Идем в «Историю сборок». Прежде чем информация отобразится в панели управления («Состояние сборников»), нужно дождаться срабатывания триггера (он действует на интервале 1–5 минут).
![](https://habrastorage.org/getpro/habr/upload_files/109/a42/306/109a4230657d1bc28a9dd04ac2f1d652.jpg)
Если мы кликнем на её название — devconf, то увидим подробности о ходе загрузки.
![](https://habrastorage.org/getpro/habr/upload_files/b6a/090/098/b6a090098d7f0fd8e1b8e272be1b6466.jpg)
По клику на новую сборку откроется окно со свойствами.
![](https://habrastorage.org/getpro/habr/upload_files/59c/9f1/b54/59c9f1b547b128db524baec358f73a3d.jpg)
В меню «Вывод консоли» можно следить за прогрессом загрузки и обработки в реальном времени.
![](https://habrastorage.org/getpro/habr/upload_files/1a7/069/5bc/1a70695bcffe0c175b47f88622e076cd.jpg)
Спустя какое-то время, в конце текстового фида появится надпись SUCCESS. Это означает, что отправка кода в docker завершена успешно.
![](https://habrastorage.org/getpro/habr/upload_files/756/ac1/852/756ac1852817e0ef2a4edfe4b49d6b94.jpg)
В этот момент произойдет так называемый «прогрев» приложения Next.js, и на какое-то время оно станет недоступным. Дело в том, что приложение проходит этап предварительной сборки, прежде чем дойти до пользователя. Разумеется, в реальной продуктовой разработке такая ситуация недопустима, поэтому там более сложные подходы, например, «сине-зеленые» сборки. Это сборки, которые можно менять местами при релизе, таким образом, в случае недоступности одной сборки – будет отображаться другая сборка. Этот подход также комбинируют вместе с георезвированием.
Пока идет сборка, посмотрим, как выглядит серверная инфраструктура в облаке. Для этого в меню платформы SberCloud.Advanced выбираем пункт Elastic Cloud Server.
![](https://habrastorage.org/getpro/habr/upload_files/8e7/7fe/4b8/8e77fe4b88b2532ed5767c45814304a4.jpg)
Откроется список запущенных приложений — тут есть воркеры выполняющие сборки в Jenkins, виртуальная машина для самого Jenkins’а (ecs-jenkins) и дополнительные машины, участвующие в сборках (они поднимаются автоматически).
![](https://habrastorage.org/getpro/habr/upload_files/977/508/8ff/9775088fffb48dcb823e22107a7c4ff8.jpg)
Но вернемся к нашему приложению. Сборка завершена, приложение развернуто на нашем Elastic Cloud Service, а также функция из FunctionGraph возвращает нам случайное значение.
![](https://habrastorage.org/getpro/habr/upload_files/e12/90d/d3a/e1290dd3a7bcd27caf8bcc429ac8b646.jpg)
Протестируем приложение
Для этого воспользуемся сервисом для нагрузочного тестирования в облаке SberCloud.Advanced — Cloud Performance Test Service.
![](https://habrastorage.org/getpro/habr/upload_files/d18/722/58b/d1872258b7a14af3ce0277d08a950ac0.jpg)
В меню слева представлены несколько вариантов тестов: JMeter с поддержкой jar-скриптов, а также CPTS для запуска проектов внутри среды. Для примера воспользуемся последним вариантом, нажимаем на соответствующий пункт меню, а затем — на Create Test Project. Во всплывающем окне прописываем только название проекта (smartdev).
![](https://habrastorage.org/getpro/habr/upload_files/af1/514/aa6/af1514aa6be739dc52d77e2b611c5992.jpg)
Он отобразился на главном экране, теперь настроим его. Кликаем по проекту, затем по Add Task, чтобы добавить задачу.
![](https://habrastorage.org/getpro/habr/upload_files/6ac/6ec/66f/6ac6ec66f73b46ce170b81d7501677cf.jpg)
Во всплывающем окне прописываем название — smartdev-test — и подтверждаем действие.
![](https://habrastorage.org/getpro/habr/upload_files/a76/197/041/a7619704105bce02a6209e1748c6337b.jpg)
Задача появилась в главном окне, на её выполнение уже выделен определенный объем ресурсов. Теперь нужно добавить кейсы.
![](https://habrastorage.org/getpro/habr/upload_files/0f3/19c/a44/0f319ca4491a747d3c83a8a654bd1864.jpg)
Для этого нажимаем большую кнопку Add Case. Во всплывающем окне указываем его название — smartdev_test_case.
![](https://habrastorage.org/getpro/habr/upload_files/370/dec/2f3/370dec2f39778cf015b2986edb899635.jpg)
Переходим к конфигуратору запросов — нажимаем на Add Request.
![](https://habrastorage.org/getpro/habr/upload_files/e6d/42a/d79/e6d42ad796b1b2fd97a01c0b46464ebf.jpg)
В открывшемся окне будет обширное количество настроек. Тут можно выбрать тип протокола — например, доступны тесты по TCP, UDP, скоро появится поддержка WEBSOCKET. В поле Request Mode мы можем сконфигурировать метод тестирования, а чуть ниже — указать время на выполнение. Я переименую запрос в smartdev-request, но остальные настройки оставлю без изменений. Также вставлю ссылку нашего опубликованного приложения в качестве источника тестирования — в поле Request URL.
![](https://habrastorage.org/getpro/habr/upload_files/3cd/c7b/951/3cdc7b95148866e8ab8e32eceaee5d71.jpg)
При желании в этом же окне можно отметить, хотим мы получать cookies или нет, и при необходимости указать заголовки. Мы с ними работать не будем, поэтому я нажму Delete.
Подтверждаем формирование запроса, осталось добавить фазу. Она определяет, каким нагрузкам будет подвергнуто приложение. В рамках этого туториала я остановлюсь на одной. Кликаем Add Phase.
![](https://habrastorage.org/getpro/habr/upload_files/b61/4be/6f2/b614be6f23cc01892f890260e059900f.jpg)
В первом поле окна создания фазы прописываем имя — Stage-Smartdev-Phase1. Если переключить флажок Gradient Increment, система будет увеличивать нагрузку постепенно. В противном случае она начнет «бомбардировать» приложение всеми запросами сразу. За их количество отвечает настройка Concurrent User — предположим, что у нас 100 пользователей. На тестирование выделим три минуты.
![](https://habrastorage.org/getpro/habr/upload_files/126/0bb/f28/1260bbf283014e791d1bee24adc89af3.jpg)
Созданный нами тест появится сразу после нажатия на кнопку ОК. Теперь его нужно запустить — нажимаем Start и переходим к выбору ресурсов.
![](https://habrastorage.org/getpro/habr/upload_files/bf3/485/42b/bf348542b4d03e6e11ac501f4fd4369f.jpg)
Во всплывающем окне выберем пункт меню «extranet». Этого должно хватить для демонстрации возможностей системы, однако для проведения серьезных стресс-тестов вам, скорее всего, придется сформировать собственный пул в Kubernetes.
![](https://habrastorage.org/getpro/habr/upload_files/21e/591/328/21e591328c99ce7888362ba801d9e8e1.jpg)
Когда тест запущен, откроем dashboard’ы и взглянем на логи. На отображение первых данных у системы может уйти до десяти секунд.
![](https://habrastorage.org/getpro/habr/upload_files/59a/04b/f67/59a04bf67c82f03565eceec26f381f6b.jpg)
На изображении выше в блоке Maximum Response Time видно, каким образом делится нагрузка. Эти шкалы отражают, сколько миллисекунд понадобилось на получение ответа от приложения. На графиках показан рост нагрузки и количество успешных тестов.
Отмечу, что отчеты всех тестов позже будут доступны в офлайн-режиме — во вкладке Offline Reports. Также в облаке SberCloud есть инструмент Intelligent Analysis, который позволяет подключить к отчетам аналитику и получить более глубокие инсайты.
![](https://habrastorage.org/getpro/habr/upload_files/f91/9a7/87a/f919a787a5f0a488d447258ffe0aedf9.jpg)
Теперь если мы вернемся в настройки FunctionGraph, то увидим, что в окне мониторинга (вкладка Functions — Monitoring) отобразились метрики.
![](https://habrastorage.org/getpro/habr/upload_files/e1c/2ab/99c/e1c2ab99c6b5c0e6f66fee13e747cd2d.jpg)
Что в итоге
Мы разработали простое приложение, которое умеет забирать данные из двух источников, складывать их и выводить результат на страничке браузера. Сделали мы это достаточно быстро — при должной сноровке можно уложиться в обеденный перерыв. Экономия времени достигается за счет облачных сервисов с большим количеством автоматизированных конфигураторов.
При этом мы настроили полный CI/CD-пайплайн, благодаря которому поддерживать наше приложение в дальнейшем будет достаточно просто. Отмечу, что подобный алгоритм разработки может быть излишне сложным для маленьких команд, однако затраты непременно себя окупят, если проект вдруг начнет разрастаться. CI/CD помогает исключить влияние человеческого фактора на разработку — когда админ пишет большое количество команд вручную, глаз «замыливается» и легко сделать опечатку. В данном случае мы развернули простой вариант CI/CD при помощи системы Jenkins, а в облаке сборку выполняют автоматизированные PaaS/IaaS-сервисы — они это делают быстрее и точнее.
В то же время последовательность действий CI/CD можно улучшать практически до бесконечности. Например, для нашего примера стоит решить проблему с отключением приложения в момент сборки. На реальном проекте такое недопустимо, так как клиентам важна отказоустойчивость. Исправить ситуацию можно несколькими способами — например, использовать «сине-зеленые билды», которые периодически меняются друг с другом. Реализовать такой подход на практике можно с помощью контейнеров — достаточно сформировать два инстанса приложения и переключать их, когда одно уходит на обновление. Также можно добавить больше тестов — реализовать не только внешнее юнит-тестирование, но и дописать несколько проверок прямо в коде приложения. Наконец, можно внедрить TypeScript — в будущем он позволит сэкономить время на переиспользовании типов внутри приложения.