Это продолжение предыдущего туториала про командную разработку с использованием GitLab. Фокус предыдущей статьи был на организации непрерывной поставки в работе команды. В этой статье мы уделим основное внимание именно практическим действиям необходимым для развёртывания из GitLab в Kubernetes. А именно мы возьмём максимально простое но достаточно содержательное приложение на React.js, докеризуем его, затем развернём в Kubernetes локально при помощи Docker Desktop. После этого развернём его уже на Google Cloud Platform (GCP), и завершим разработкой CI/CD конвейера в GitLab для публикации нашего приложения в Google Kubernetes Engine. Желательны но необязательны базовые знания В дальнейшем мы сделаем следующее.  Время от времени я буду просить вас что-то сделать. Такие моменты помечены значком 🛠️. Пожалуйста выполняйте действия по мере чтения текста чтобы получить от данного туториала наибольшую пользу. ⚙️ Для того чтобы пройти этот туториал нужно чтобы эти программы были установлены на вашем компьютере. Команды в тексте рассчитаны в bash, некоторые из них используют клиент git для командной строки. Если вы используете Windows, то самый простой способ получить и то и другое — установить Git for Windows. Далее будем полагать что директории с исполняемыми файлами node, git, docker и kubectl включены в PATH. 📢 Знакомство с приложением будет включать эти шаги. 🛠️ Клонируйте репозиторий и перейдите в директорию приложения Приложение состоит из двух частей Зачем использовать именно такое приложение для этого туториала? Ну, это самое простое приложение, которое удовлетворяет таким требованиям.  Я стремился сохранять приложение наиболее простым, поэтому я сознательно не включил такие вещи: Цель туториала в том чтобы привести минимальные пример для развёртывания на Kubernetes и построения конвейера в GitLab. Даже несмотря на все упрощения туториал получился довольно огромный. Внутри только что клонированного репозитория будут файлы и директории: Остальные файлы либо стандартны для create-react-app, либо второстепенны для нашей задачи. Обратите внимание, что компонент в файле ./spa/src/components/Log.jsx посредством кода из ./spa/src/config.js получает значение переменной окружения REACT_APP_API_URL. Если ничего не передать, код будет использовать значение по умолчанию годное для запуска на локальной машине, но если мы захотим развернуть код где-либо ещё, нам понадобится присвоить актуальное значение данной переменной окружения на этапе сборки, перед выполнением npm run build.  Запустите API и SPA. Проще всего это сделать открыв два консольных окна. 🛠️ В первом окне запустите API. 🛠️ Во втором окне запустим SPA. 🛠️ Откройте в браузере SPA по адресу  🛠️ Нажмите кнопку Log Me!. Будет отправлен AJAX-запрос по адресу http://localhost:3000/log и данные User Agent текущего пользователя
будут сохранены в файл ./data/log. Новые данные будут отображены на странице под кнопкой. Почему API, запущенный на порту 4000 получает запрос, направленный по адресу http://localhost:3000/log? Дело в том что разработческий сервер create-react-app умеет работать как прокси, и перенаправляет запросы по адресу http://localhost:4000/log. Соответствующая настройка находится в ./spa/package.json, обратите внимание на последнюю строчку "proxy": "http://localhost:4000". Запустить тесты для SPA вы можете командой Подготовить статический сайт для размещения на продуктиве можно командой Целевую архитектуру можно отобразить на этой картинке.  Для простоты мы храним данные в файле, но если перейти на хранение данных в БД, наша архитектура станет высокодоступной. В первую очередь нам нужно упаковать наше приложение в качестве образа контейнера Docker.  Хорошее введение в основы Docker есть на официальном сайте. 📢 Для того что бы докеризовать наше приложение мы вначале докеризуем API а затем SPA. Для того чтобы осуществить сборку контейнера Docker, требуется создать файл с именем Dockerfile.  🛠️ Перейдите в директорию ./api и создайте внутри файл Dockerfile с таким кодом. 🛠️ Убедитесь что Docker Desktop запущен. Запустите локальную сборку и присвойте образу тег gitlab-course-api 🛠️ Запустите контейнер на основе образа Эта команда выведет хэш контейнера, который вы сможете использовать чтобы остановить контейнер в дальнейшем.
Проверьте что сервис возвращает тестовую строку при запросе к http://localhost:4000/log. 🛠️ Аналогично API добавим в директорию ./spa Dockerfile.  В данном случае Dockerfile вышел посложнее. Для того чтобы понять зачем такие сложности, давайте поймём как мы можем собирать SPA. Мы могли бы осуществлять сборку В случае локальной сборки SPA среда сборки будет различаться от разработчика к разработчику.
С последними двумя вариантами всё в порядке, для простоты выберем вариант сборки в контейнере.
Мы хотели бы чтобы образ нашего приложения в продуктивной среде был как можно меньше и содержал минимум софта из соображений стабильности и безопасности. Так как наше приложение на React всего лишь статический сайт, на продуктиве нам достаточно лишь небольшого образа с веб-сервером. Однако, чтобы "собрать" наше приложение требуется Node.js, а также нужно установить все зависимости, а это целая куча пакетов. Чтобы разрешить это противоречие, мы и используем паттерн, который называется multi-stage build. Мы вначале собираем наше приложение в "тяжёлом" контейнере который по сути является средой сборки а затем копируем лишь получившийся статический сайт в легковесный образ, который и будет "раздавать" его в продуктивной среде. 🛠️ Соберите образ SPA, запустив эту команду в директории со вновь созданным Dockerfile. Обратите внимание что мы передаём адрес API в качестве параметра чтобы в процессе сборки оказалась объявлена переменная REACT_APP_API_URL. Убедитесь что сборка была завершена успешно. 🛠️ Используйте следующую команду чтобы запустить контейнер на основе только что созданного образа. Если порт 80 по какой-то причине занят на вашем компьютере, вы можете опубликовать этот порт на другом порту хоста, например так -p 8080:80. 🛠️ Откройте в браузере страницу SPA http://localhost. Вы должны увидеть ту же страницу что и при локальном тесте, однако попытка вызвать API должна завершаться неуспешно с ошибкой связанной с CORS. Это потому что с точки зрения same origin policy/CORS должны совпадать протокол, доменное имя и порт, а если они не совпадают, то это разные сайты. http://localhost:80 и http://localhost:4000 — разные сайты. Обычно для того, чтобы реализовать сценарий, похожий на наш, требуется чтобы веб-сервер SPA "разрешал" запросы к API, отправляя в браузер пользователя заголовки с нужным сайтом. 💡 CORS — одна из многих тем, типа настройки SSL/TLS, которые мы не будем включать этот туториал чтобы он не разросся на 100 страниц. На данном этапе нам было важно убедиться что оба веб-приложения работают. Когда мы развернём наше приложение в Kubernetes, мы сможем получать запросы к SPA и API на один домен и порт и различать их на основе path. 💡 Для локальной разработки часто используется утилита docker-compose. Мы могли бы использовать её чтобы реализовать взаимодействие SPA и API, а также их взаимодействие с внешним миром через вспомогательный reverse proxy. docker-compose тесно связан с оркестратором Docker Swarm, продуктом Docker, и использует файлы конфигурации с совместимым синтаксисом. По той причине что в качестве оркестратора мы используем Kubernetes, мы будем выполнять наше приложение локально сразу в Kubernetes. Мы уже научились запускать наше приложение в Docker, перейдём теперь к развёртыванию в Kubernetes.  Если вы еще не знакомы с Kubernetes, то я бы рекомендовал эти материалы. Для знакомства с основами как правило достаточно дистрибуции Kubernetes, поставляемой вместе с Docker Desktop. 💡 Если у вас не получится создать все необходимые ресурсы из-за технических сложностей, вы можете либо заняться отладкой которая потребует основательно во всём разобраться либо перейти к следующему разделу где мы будем похожим образом разворачивать наше приложение в Kubernetes но на Google Kubernetes Engine (GKE). В другой среде те же технические проблемы могут не возникнуть.  Создадим директорию, в которой мы будем хранить файлы ресурсов Kubernetes. Перейдите в корень репозитория и выполните команду. Наше приложение развёрнутое в Kubernetes в Docker Desktop будет выглядеть так. 🛠️ Начнём с SPA. Добавим файл ./kubernetes/spa.yml с таким кодом. Данный файл содержит два YAML-документа разделённых ---.  💡 Мы дополнительно определяем метку project: gitlab-course для всех ресурсов нашего проекта чтобы в дальнейшем иметь возможность получать их всех запросом по label вроде такого: 🛠️ Кажется что у нас есть всё чтобы развернуть SPA, однако есть один нюанс. Дело в том, что мы обращались к API по адресу, заданному через API_URL, и это значение встроено в образ SPA. Пересоберём образ, используя значение API_URL, которое мы будем использовать в дальнейшем. 🛠️ Создадим ресурсы SPA выполнив команду Убедитесь, что в консоли подтверждается что оба ресурса созданы. 💡 Если вы захотите обновить образ Pod в Kubernetes, потребуется этот Pod пересоздать. Для SPA это можно сделать, например, этой командой. 🛠️ Перейдём к API. Добавим файл ./kubernetes/api.yml с таким кодом. Тут всё аналогично SPA кроме этой части: Она связана с тем что мы используем Persistent Volumes для того чтобы данные, сохраняемые API могли жить дольше чем Pod.  Однако для того чтобы это работало, нам требуется чтобы уже до этого был определён Persistent Volume Claim gitlab-course-pv-claim. 🛠️ Создайте новый файл ./kubernetes/volume.yml с таким кодом. Это, пожалуй, самый скучный момент туториала, но зато дальше так скучно уже не будет. Итак, разберёмся что тут и зачем.
Начнём с PersistentVolumeClaim с именем gitlab-course-pv-claim. Он определяет некий запрос на ресурсы и мы указываем его в определении SPA. Kubernetes расширяемая система, и потребности, в нашем случае 100MiB, могут быть удовлетворены разным образом, например предоставлением облачного ресурса. Так как мы публикуем наше приложение локально, мы попросту создаём PersistentVolume с названием gitlab-course-pv чуть выше. Чтобы создать PersistentVolume требуется, в свою очередь, указать storage class который содержит информацию о том, какого рода диск нам требуется. Поэтому ещё выше мы создаём storage class с storageClassName: local-storage, который подразумевает что диск создан вручную. 🛠️ Примените приведённую выше конфигурацию. Убедитесь что сообщение в консоли подтверждает что все три объекта созданы. 🛠️ Теперь у нас есть всё чтобы создать объекты API. Наши контейнеры уже выполняются в Kubernetes, но сервисы, которые мы создали доступны лишь из локальной сети внутри Kubernetes. Одним из способов предоставить доступ к нашему приложению извне является создание объекта который называется Ingress. Ingress определяет правила по которым наши сервисы будут доступны по HTTP.  💡 Убедитесь что в вашей версии Kubernetes установлен ingress controller, например поддерживаемый сообществом ingress-nginx. Если вы не можете найти соответствующий Deployment, вам может понадобиться установить его. На момент написания этой статья команда для Docker Desktop выглядела так 🛠️ Создадим файл kubernetes/ingress.yml с таким кодом.  В нашем случае в результате создания объекта Ingress будет создан reverse-proxy на основе nginx, который будет осуществлять передачу запросов к сервисам нашего приложения и ответов от них обратно клиенту. Суть данной конфигурации в том что мы принимаем запросы на порту 80 и перенаправляем все запросы на SPA помимо запросов по адресу http://spa.localtest.me/log, которые мы направляем к API. Мы тут используем localtest.me — доменное имя для которого возвращается IP 127.0.0.1 для всех поддоменов. 🛠️ Создайте ресурс Ingress. 🛠️ Перейдите в браузере по адресу http://spa.localtest.me. Важно использовать именно этот домен потому для другого запрос к API по адресу http://spa.localtest.me/log будет заблокирован CORS. 👍 Поздравляю, вы развернули приложение в Kubernetes, хоть пока и локально. Как мы увидим в дальнейшем, использование другого провайдера не будет значительно сложнее.  Мы будем использовать GitLab.com в качестве инсталляции GitLab чтобы избежать хлопот по установке и настройке локальной версии. 🛠️ Если у вас ещё нет учётной записи на GitLab.com, зайдите на https://gitlab.com и заведите её. GitLab позволяет нам создать проект просто выполнив push в удалённый репозиторий. 🛠️ Воспользуемся для этого следующей командой: <user name> тут — ваше имя пользователя на GitLab.com. Дальше будем считать переменную GITLAB_USER_NAME в других скриптах имеющей то же значение. Эта команда создаст приватный проект с именем gitlab-kubernetes внутри вашей учётной записи на GitLab.com. GitLab предоставляет доступ к "переменным развёртывания", однако для этого наша ветвь должна быть "защищённой" (protected). Мы основательно разберёмся с этими переменными позже, но убедиться что наша ветвь master protected нам удобнее всего именно сейчас. 🛠️ При помощи браузера зайдите на страницу вашего проекта в GitLab https://gitlab.com/<user name>/gitlab-kubernetes; 💡 Для выполнения задач данной части может понадобиться создать "пробный" аккаунт. Вы можете получить $300 USD на "поиграть" с ресурсами на протяжении пробного периода и дополнительно $200 USD если воспользуетесь предложением GitLab. По крайней мере, таковы были предложения на момент написания этого туториала. 💡 "Из коробки" GitLab теперь также поддерживает интеграцию с Amazon EKS. 🛠️ Создайте проект с названием gitlab-kubernetes в Google Cloud Platform. Если вы не делали это раньше, то тут написано как.  💡 На самом деле можно вначале создать кластер в Kubernetes, а затем указать в GitLab ключ и токен нужного service account, но этот путь чуть длиннее и в большей степени провоцируют ошибки, поэтому мы "сжульничаем" и создадим кластер в Kubernetes сразу из GitLab, тогда все реквизиты будут заполнены автоматически. 🛠️ Создайте кластер Kubernetes с названием gitlab-cluster-auto. UI GitLab.com постоянно меняется, поэтому действия могут несколько отличаться, следуйте духу а не букве. В данный момент действия таковы: Далее заполните форму. Нажмите кнопку Create Kubernetes cluster. Дождитесь завершения процесса, убедитесь что отображается сообщение Kubernetes cluster was successfully created. 💡 На данном этапе может понадобиться зайти в вашу учётную запись GCP и дать GitLab необходимые разрешения. 🛠️ Убедитесь что на странице Kubernetes clusters напротив нашего кластера gitlab-cluster-auto нет ошибок. 👍 Теперь у нас есть кластер Kubernetes который мы сможем использовать в дальнейшем. 📢 При работе с кластером Kubernetes в GCP будем выполнять консольные команды в Google Cloud Console Shell. Это позволит 🛠️ Откройте в браузере страницу Google Cloud Shell. 🛠️ В Google Cloud Shell установлен kubectl, но нам нужно "подключить" его к кластеру. Для этого нам понадобится указать PROJECT_ID. Выведем список проектов в нашем окружении. Например, для меня PROJECT_ID был gitlab-kubernetes-326101. Присвойте это значение переменной PROJECT_ID Теперь мы можем сохранить данные для подключения kubectl. Убедитесь, что выдача была Создадим namespace gitlab-course Давайте для удобства установим переменную GITLAB_USER_NAME в Google Cloud Shell 📢 Начнём развёртывание нашего приложения в GKE. 💡 При работе с "облачными" провайдерами обычно требуется лишь создать Persistent Volume Claim, а сам диск и реализующий его "облачный" ресурс создаются автоматически средствами провайдера. 🛠️ Удалите StorageClass и PersistentVolume в kubernetes/volume.yml. Удалите storageClassName: local-storage в определении PersistentVolumeClaim, это приведёт к использованию стандартного диска в GKE. Также заменим значение namespace на токен {{NAMESPACE}}. 💡 Идея в том что namespace в зависимости от того как и куда мы развёртываем приложение может может быть различным. По этой причине мы будем заменять {{ТОКЕНЫ}} на реальные значения перед применением файлов конфигурации. Мы могли бы использовать что-то типа Helm для работы с шаблонами, но мы же не хотим усложнять туториал, не так ли? 🛠️ Загрузите этот файл в Google Cloud Shell. Кнопка "Upload" находится в меню командной строки в интерфейсе Cloud Shell. На момент написания этого текста, опция находилась в подменю "⋮".
По той причине что мы токенизировали файл, нам нужно вначале заменить токен на реальное значение.
Например, так Затем примените полученную конфигурацию Или можете осуществить подстановку и применение конфигурации одной строчкой. Проверьте следующей командой что Persistent Volume Claim создан. 🛠️ Отредактируйте kubernetes/api.yml.
Замените namespace и в Deployment в Service таким образом. Поступим с image аналогично, мы будем передавать точные версии образа Docker для того чтобы дать Kubernetes знать что Pod следует обновить. Мы теперь можем подставить другое название образа, но наш локальный образ контейнера не будет доступ для GKE. Нам нужно разместить наш образ в сервисе доступном для Kubernetes. Загрузим локальный образ в GitLab Repository. 🛠️ В локальной консоли наберите и введите свои данные для входа в GitLab. 🛠️ Присвойте образу тэг с префиксом нашего репозитория в GitLab. 🛠️ Теперь мы можем загрузить наш образ в удалённый репозиторий. Выполните команду Дождитесь окончания загрузки и убедитесь что она прошла успешно. 🛠️ В Google Cloud Shell наберите 🛠️ Есть нюанс, заключающийся в том, что для того чтобы скачать образ из нашего приватного репозитория на GitLab.com нужно предоставить некоторый секрет. Для этого мы можем загрузить секрет, который был создан по пути ~/.docker/config.json когда мы выполнили команду docker login. Выполните эту команду в Google Cloud Shell. Последняя часть головоломки с настройкой образа — добавить информацию о нашем секрете для доступа к репозиторию в kubernetes/api.yml. Разместите этот код в конце определения Deployment сразу после volumes: на одном уровне с containers: В Google Cloud мы будем использовать Ingress по умолчанию для передачи траффика с внешнего IP на Pod в Kubernetes. Эта реализация Ingress "под капотом" использует External HTTP/S Load Balancer и требует чтобы все сервисы были доступны как NodePort.
Изменим тип сервиса в kubernetes/api.yml. В итоге внутри kubernetes/api.yml должен получиться такой код: Отлично! Теперь наш образ доступен для Kubernetes и мы можем создать Pod! 🛠️ Загрузите файл kubernetes/api.yml в Google Cloud Console и выполните команду. Убедитесь что ресурсы созданы успешно. 💡 Обратите внимание что мы используем ~ в качестве разделителя во втором выражении sed потому что в имени образа уже встречается /. 💡 Если вы захотите обновить образ Pod в Kubernetes, потребуется этот Pod пересоздать. Для API это можно сделать, например, этой командой. Потребуется выполнить kubectl apply ещё раз, указав sha256 явно, добавив к имени образа конструкцию @sha256:<hash>. 💡 Мы воспользуемся nip.io для того чтобы избежать необходимости регистрировать домен. Проблема тут в том, что для того чтобы указать правильный хост для API в определении Ingress, нам нужно знать внешний IP адрес Ingress. Однако IP адрес будет присвоен Ingress только после того как мы создадим Ingress. Решение в том чтобы вначале создать, а затем изменить Ingress.  🛠️ Создадим Ingress. Измените файл kubernetes/ingress.yml. 💡 Чтобы создать Ingress нам нужно указать или хотя бы одно правило либо бэкенд по умолчанию. Мы временно указываем сервис API потому что он уже создан. 🛠️ Загрузите этот файл в Google Cloud Shell. 🛠️ Давайте дождёмся присваивания внешнего IP нашему Ingress. Это удобно сделать выполните эту команду Прекратите выполнение команды при помощи Ctrl + C когда внешний IP будет присвоен. Присвойте его переменной EXTERNAL_IP Для простоты я буду называть сам адрес тоже EXTERNAL_IP.  🛠️ Перейдите в браузере по адресу http://EXTERNAL_IP.nip.io. Например, в процессе тестирования этого курса, адрес для меня выглядел http://34.102.175.33.nip.io/log Убедитесь, получен ответ с кодом 200 OK. Итак, мы почти закончили развёртывать наше приложение в GKE, осталось развернуть SPA и обновить Ingress. 🛠️ Помните про нюанс с SPA? Внутри образа встроен адрес API, заданный через API_URL. Хорошие новости в том что мы уже создали Ingress и поэтому знаем адрес, который можем использовать в качестве API_URL. Пересоберём образ SPA с использованием этого адреса. Выполните эту команду в локальной консоли. 🛠️ Отредактируйте kubernetes/spa.yml. Это будут те же правки что и в kubernetes/api.yml до того.  Замените namespace и в Deployment и в Service таким образом. Поступим с image аналогично. Изменим тип сервиса в kubernetes/spa.yml на NodePort. Разместите секрет для доступа в репозиторий GitLab в конце определения Deployment на одном уровне с containers: В итоге должно получиться такое: 🛠️ Загрузите kubernetes/spa.yml в директорию ~/gitlab-course Cloud Shell и выполните команду. 🛠️ Осталось обновить Ingress — и мы закончили. Давайте сделаем это сейчас. Укажем SPA в качестве сервиса по умолчанию и будем направлять запросы к API если путь равен /log. Обратите внимание что мы используем Ingress по умолчанию в GKE, и потому указываем pathType: ImplementationSpecific.  🛠️ Удалите файл ingress.yml в Google Cloud Shell, а затем заново загрузите файл kubernetes/ingress.yml в Google Cloud Shell и примените изменения командой. 🛠️ Зайдите в браузере на страницу EXTERNAL_IP.nip.io и убедитесь что наше приложение работает как предполагается. Вам может понадобиться подождать некоторое время пока конфигурация Ingress обновится в GCP.  👍 Поздравляем, вы развернули приложение в Kubernetes в Google Cloud Platform! Осталось только организовать развёртывание приложения в GitLab.  Самое сложное позади, осталось разработать pipeline в GitLab. Итак, начнём. 💡 Мы не будем показывать здесь как опубликовать приложение при помощи AutoDevOps (красивое!) потому что AutoDevOps и всё что с ним связано меняется крайне часто, и, кроме того мало подходит для чего-либо сложнее Hello World!. Тем не менее, я рекомендую пройти официальный туториал чтобы составить впечатление о встроенных возможностях GitLab и использовать их по необходимости в дальнейшем. 💡 Я не думаю что реально предоставить решение в стиле "один размер подходит всем" так как чем "продуктивнее" конвейер тем он сложнее и тем сильнее зависит от платформы на которой происходит развёртывание. По этой причине мы не будем стремиться создать высокооптимизированный конвейер, а вместо этого сосредоточимся на создании решения которое просто понять и которое демонстрирует разные возможности. 📢 План такой 🛠️ Будем удалять ресурсы "сверху вниз". Выполните эти команды в Google Cloud Shell. 🛠️ Итак, наш конвейер будет использовать переменную EXTERNAL_IP, которая будет содержать внешний IP нашего Ingress.
Давайте зададим её значение внутри GitLab.  Готово! Если вы отлаживаете ваш конвейер, вы также можете захотеть добавить переменную CI_DEBUG_TRACE со значением true. Если вы это сделаете, в логе job будут отображены значения всех переменных и параметров. 💡 Всегда включенный CI_DEBUG_TRACE создаёт риски для безопасности. 🛠️ Создайте файл .gitlab-ci.yml в корне проекта. Добавьте этот код DOCKER_DRIVER: overlay2 — Согласно документации, overlay2 эффективнее vfs. На самом деле это значение по умолчанию для shared runners, но мы оставили эту строчку на случай если вы захотите использовать self-hosted runners. Эти переменные мы завели для нашего удобства. Это названия наших образов внутри GitLab registry. Почему у нас только одна переменная для API, но две для SPA?  Для API мы просто упаковываем server.js внутрь небольшого образа с Node.js.  Для SPA, как вы помните, мы делали двухэтапную сборку.  Решение в том чтобы разделить нашу двухэтапную сборку на сборку 2-ух образов — "побольше" и "поменьше". 🛠️ Кстати, давайте это сделаем прямо сейчас. Создайте файл Build.Dockerfile и скопируйте в него первую часть кода из Dockerfile А вот исходный Dockerfile мы сократим. Удалим из файла код приведённый выше и параметризуем название образа, из которого мы будем в итоге копировать файлы. Как видите, просто и логично. 💡 В качестве альтернативы, мы могли бы повторно использовать "сборочный" образ, передав команду запуска тестов через командную строку. Теперь всё готово к тому чтобы реализовать все этапы связанные со сборкой и тестированием.
🛠️ Добавьте в .gitlab-ci.yml довольно большой кусок кода.  Данный код определяет три этапа build, test и package.
Эти этапы в свою очередь содержат jobs. Давайте обсудим какой job что делает. Команды, указанные в before_script будут выполнены перед каждым job. Мы используем docker login чтобы авторизовать runner для доступа к GitLab Docker Registry с использованием данных доступных через переменные окружения. Мы тут используем некоторый переменные, которые начинаются на CI_, это так называемые переменные GitLab CI/CD. Их значения предоставлены нам платформой. Большинство jobs выше устроены аналогично за исключением test_spa который проще потому что в итоге мы не загружаем образ в репозиторий. Если вы хотите убедиться, что перед запуском некого job завершится предыдущий, вы можете указать предыдущий в dependencies. Разберём команды на примере build_spa_builder. Тут мы создаём образ, затем тегируем его меткой состоящей из значения нашей переменной и короткого хэша текущего коммита в репозитории GitLab CI_COMMIT_SHORT_SHA. Затем мы загружаем образ в Docker-репозиторий GitLab. При создании образа SPA мы также используем переменную EXTERNAL_IP которую задали ранее. Целей в том чтобы тэгировать образ хэшем коммита две.  🛠️ Итак, волшебный момент запуска нашего конвейера настал. Обновите код в репозитории GitLab следующей командой в локальной консоли. Добавьте весь код, который вы еще не добавили в индекс, закоммитьте и сделайте push. 🛠️ Откройте в браузере страницу CI/CD -> Pipelines в GitLab и убедитесь что сборка и тестирование начались. Перейдите на страницу нашей конкретной сборки и дождитесь её успешного завершения. При этом полезно пооткрывать страницы разных jobs и понаблюдать за обновляющейся выдачей в консоли. Отлично! Мы уже собираем все нужные нам образы прямо в GitLab. 🛠️ Давайте сделаем последний шаг — реализуем развёртывание в Kubernetes на GCP при помощи GitLab. Итак, добавьте дополнительный stage в .gitlab-ci.yml. Затем после шагов сборки добавьте такой код. Тут внимания достоин тот факт что вместо docker: stable мы используем другой образ. В принципе, нам подойдёт любой образ, имеющий kubectl, поэтому для простоты мы используем образ registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image который GitLab использует для развёртывание в Kubernetes в режиме AutoDevOps.  Также мы пользуемся фактом что наш кластер интегрирован с GitLab. По этой причине мы можем полагаться на переменные окружения, которые начинаются с KUBE_, они будут переданы в конвейер автоматически. Мы переопределили before_scripts. Привожу полный код .gitlab-ci.yml который должен был получиться. 🛠️ Закоммитьте изменения и выполните push в репозиторий Перейдите по адресу http://EXTERNAL_IP.nip.io в браузере и убедитесь что приложение заработало. Может понадобиться подождать несколько минут чтобы все ресурсы успели обновиться.  💡 Если приложение не открывается, но Deployments функционируют без ошибок, особенно если при переходе по адресу выше вы получаете код 404, есть вероятность что GCP присвоил вновь созданному Ingress другой внешний IP. Это легко исправить! Вы докеризовали приложение на React.js, затем развернули его в Kubernetes вручную и в итоге создали конвейер непрерывного развёртывание этого приложения в Kubernetes при помощи GitLab! 🧹 Не забудьте удалить ненужные ресурсы, созданные на Google Cloud Platform.