Развёртывание в Kubernetes из GitLab
Это продолжение предыдущего туториала про командную разработку с использованием GitLab. Фокус предыдущей статьи был на организации непрерывной поставки в работе команды. В этой статье мы уделим основное внимание именно практическим действиям необходимым для развёртывания из GitLab в Kubernetes.
А именно мы возьмём максимально простое но достаточно содержательное приложение на React.js, докеризуем его, затем развернём в Kubernetes локально при помощи Docker Desktop. После этого развернём его уже на Google Cloud Platform (GCP), и завершим разработкой CI/CD конвейера в GitLab для публикации нашего приложения в Google Kubernetes Engine.
Желательны но необязательны базовые знания
- Docker;
- Kubernetes;
- Git;
- Node.js;
- React;
- Bash.
В дальнейшем мы сделаем следующее.
- 🧱 Познакомимся c нашим приложением, обсудим из чего оно состоит.
- 🐳 Докеризуем наше приложение.
- ☸️ Развернём наше приложение в Kubernetes локально на Docker Desktop.
- ☁️ Обсудим особенности GCP и как нужно изменить наше приложение, а затем ещё раз развернём наше приложение в Kubernetes но уже в GCP.
- 🦊 Завершим наш туториал созданием конвейера для развертывания приложения в GCP при помощи GitLab.
Время от времени я буду просить вас что-то сделать. Такие моменты помечены значком 🛠️. Пожалуйста выполняйте действия по мере чтения текста чтобы получить от данного туториала наибольшую пользу.
⚙️ Для того чтобы пройти этот туториал нужно чтобы эти программы были установлены на вашем компьютере.
- Bash Shell
- Git
- Node.js
- Docker Desktop
- Современный веб-браузер
Команды в тексте рассчитаны в bash
, некоторые из них используют клиент git
для командной строки. Если вы используете Windows, то самый простой способ получить и то и другое — установить Git for Windows.
Далее будем полагать что директории с исполняемыми файлами node
, git
, docker
и kubectl
включены в PATH
.
🧱 Знакомство с приложением
📢 Знакомство с приложением будет включать эти шаги.
- Клонируем репозиторий и выясним из чего состоит наше приложение.
- Запустим приложение в консоли.
- Увидим каким приложение будет в дальнейшем в развёрнутом состоянии.
Из чего состоит приложение
🛠️ Клонируйте репозиторий и перейдите в директорию приложения
git clone https://github.com/ntaranov/gitlab-kubernetes
cd gitlab-kubernetes
Приложение состоит из двух частей
- Single Page Application (SPA) — статический сайт на React который получает и изменяет данные, отправляя AJAX-запросы к сервису API.
- API — веб сервис, который предоставляет данные о "записавшихся" пользователях и может добавлять новые данные. API записывает данные, которые должны надёжно храниться, в файл. Мы используем файл чтобы избежать лишних для туториала сложностей, которые привнесла бы работа с базой данных. Дополнительно, файл позволяет нам поговорить о persistent volumes.
Зачем использовать именно такое приложение для этого туториала? Ну, это самое простое приложение, которое удовлетворяет таким требованиям.
- Использует современный веб-фреймворк (React.js), требующий компиляции кода.
- Требует передачи параметра (
API_URL
) на этапе сборки. - Содержит серверный код.
- Включает Front End и API для эмуляции микросервисной архитектуры.
- Имеет некоторое хранимое состояние (persistent state).
- Содержит тесты.
Я стремился сохранять приложение наиболее простым, поэтому я сознательно не включил такие вещи:
- аутентификацию и авторизацию;
- логи и телеметрию;
- обработку ошибок;
- взаимодейтсвие с базой данных;
- какого либо осмысленного покрытия тестами.
Цель туториала в том чтобы привести минимальные пример для развёртывания на Kubernetes и построения конвейера в GitLab. Даже несмотря на все упрощения туториал получился довольно огромный.
Внутри только что клонированного репозитория будут файлы и директории:
api // Директория с файлами API
/data/log // Будем сюда сохранять данные
/package.json // NPM Манифест API
/server.js // Код API, не требует установки других пакетов
spa // Директория с файлами SPA
/public/. // Стандартные статические файлы React
/src
/components
/Log.jsx // Этот компонент шлёт запросы к API
/App.js // Главный компонент приложения SPA
/App.test.js // Тесты, у нас же должны быть тесты
/config.js // Загружает URL API из переменной окружения
/index.js // Корень React-приложения
/package.json // NPM Манифест SPA
/package-lock.json // С этими версиями пакетов работало
Остальные файлы либо стандартны для create-react-app
, либо второстепенны для нашей задачи.
Обратите внимание, что компонент в файле ./spa/src/components/Log.jsx
посредством кода из ./spa/src/config.js
получает значение переменной окружения REACT_APP_API_URL
. Если ничего не передать, код будет использовать значение по умолчанию годное для запуска на локальной машине, но если мы захотим развернуть код где-либо ещё, нам понадобится присвоить актуальное значение данной переменной окружения на этапе сборки, перед выполнением npm run build
.
Запускаем приложение локально
Запустите API и SPA. Проще всего это сделать открыв два консольных окна.
🛠️ В первом окне запустите API.
cd api
node server.js
🛠️ Во втором окне запустим SPA.
cd spa
- Установите пакеты npm, указанные в
package-lock.json
.npm ci
- SPA была создана при помощи
create-react-app
. Поэтому запустим его при помощи разработческого сервера изreact-scripts
.npm run start
🛠️ Откройте в браузере SPA по адресу
http://localhost:3000/
🛠️ Нажмите кнопку 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 вы можете командой
npm run test
Подготовить статический сайт для размещения на продуктиве можно командой
npm run build
Что мы хотим видеть в конце?
Целевую архитектуру можно отобразить на этой картинке.
Для простоты мы храним данные в файле, но если перейти на хранение данных в БД, наша архитектура станет высокодоступной.
В первую очередь нам нужно упаковать наше приложение в качестве образа контейнера Docker.
🐳 Докеризуем наше приложение
Хорошее введение в основы Docker есть на официальном сайте.
📢 Для того что бы докеризовать наше приложение мы вначале докеризуем API а затем SPA.
Докеризуем API
Для того чтобы осуществить сборку контейнера Docker, требуется создать файл с именем Dockerfile
.
🛠️ Перейдите в директорию ./api
и создайте внутри файл Dockerfile
с таким кодом.
# используем Alpine Linux чтобы контейнер был меньше
FROM node:16-alpine
WORKDIR /app
RUN mkdir data
# копируем код сервиса
COPY server.js ./
CMD ["node", "server.js"]
🛠️ Убедитесь что Docker Desktop запущен. Запустите локальную сборку и присвойте образу тег gitlab-course-api
docker build . -t gitlab-course-api
🛠️ Запустите контейнер на основе образа
docker run --publish 4000:4000 -d gitlab-course-api
Эта команда выведет хэш контейнера, который вы сможете использовать чтобы остановить контейнер в дальнейшем.
Проверьте что сервис возвращает тестовую строку при запросе к http://localhost:4000/log
.
Докеризуем SPA
🛠️ Аналогично API добавим в директорию ./spa
Dockerfile
.
# среда сборки
FROM node:14-alpine as builder
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
# копируем исходники React-приложение в среду сборки
COPY package.json ./
COPY package-lock.json ./
COPY src ./src
COPY public ./public
# устанавливаем NPM-пакеты в соответствии с package-lock.json
RUN npm ci
# мы должны передать этот параметр через --build-arg при запуске сборки
ARG API_URL
# осуществляем сборку приложение на React с использованием параметра
RUN REACT_APP_API_URL=$API_URL npm run build
# продуктивная среда
FROM nginx:stable-alpine
# копируем получившийся в результате сборки статический сайт из "тяжёлого" образа
# на маленький образ с веб-сервером nginx
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
В данном случае Dockerfile
вышел посложнее. Для того чтобы понять зачем такие сложности, давайте поймём как мы можем собирать SPA. Мы могли бы осуществлять сборку
- локально;
- на отдельном сервере;
- прямо в контейнере.
В случае локальной сборки SPA среда сборки будет различаться от разработчика к разработчику.
С последними двумя вариантами всё в порядке, для простоты выберем вариант сборки в контейнере.
Мы хотели бы чтобы образ нашего приложения в продуктивной среде был как можно меньше и содержал минимум софта из соображений стабильности и безопасности. Так как наше приложение на React всего лишь статический сайт, на продуктиве нам достаточно лишь небольшого образа с веб-сервером. Однако, чтобы "собрать" наше приложение требуется Node.js, а также нужно установить все зависимости, а это целая куча пакетов. Чтобы разрешить это противоречие, мы и используем паттерн, который называется multi-stage build. Мы вначале собираем наше приложение в "тяжёлом" контейнере который по сути является средой сборки а затем копируем лишь получившийся статический сайт в легковесный образ, который и будет "раздавать" его в продуктивной среде.
🛠️ Соберите образ SPA, запустив эту команду в директории со вновь созданным Dockerfile
. Обратите внимание что мы передаём адрес API в качестве параметра чтобы в процессе сборки оказалась объявлена переменная REACT_APP_API_URL
.
docker build . --build-arg API_URL=http://localhost:4000 -t gitlab-course-spa
Убедитесь что сборка была завершена успешно.
🛠️ Используйте следующую команду чтобы запустить контейнер на основе только что созданного образа.
docker run -it --publish 80:80 gitlab-course-spa
Если порт 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.
☸️ Развёртывание в Kubernetes
Мы уже научились запускать наше приложение в Docker, перейдём теперь к развёртыванию в Kubernetes.
Если вы еще не знакомы с Kubernetes, то я бы рекомендовал эти материалы.
- What is Kubernetes?
- Другие темы Kubernetes Documentation -> Concepts -> Overview
- Туториал Learn Kubernetes Basics.
Для знакомства с основами как правило достаточно дистрибуции Kubernetes, поставляемой вместе с Docker Desktop.
Развернём приложение локально в Kubernetes на Docker Desktop
💡 Если у вас не получится создать все необходимые ресурсы из-за технических сложностей, вы можете либо заняться отладкой которая потребует основательно во всём разобраться либо перейти к следующему разделу где мы будем похожим образом разворачивать наше приложение в Kubernetes но на Google Kubernetes Engine (GKE). В другой среде те же технические проблемы могут не возникнуть.
Создадим директорию, в которой мы будем хранить файлы ресурсов Kubernetes. Перейдите в корень репозитория и выполните команду.
mkdir kubernetes
Наше приложение развёрнутое в Kubernetes в Docker Desktop будет выглядеть так.
🛠️ Начнём с SPA. Добавим файл ./kubernetes/spa.yml
с таким кодом.
kind: Deployment
apiVersion: apps/v1
metadata:
name: gitlab-course-spa
namespace: gitlab-course
labels:
k8s-app: gitlab-course-spa
project: gitlab-course
spec:
replicas: 1
selector:
matchLabels:
k8s-app: gitlab-course-spa
template:
metadata:
name: gitlab-course-spa
labels:
k8s-app: gitlab-course-spa
spec:
containers:
- name: gitlab-course-spa
# так мы назвали наш docker image
image: gitlab-course-spa
imagePullPolicy: IfNotPresent
---
kind: Service
apiVersion: v1
metadata:
name: gitlab-course-spa
namespace: gitlab-course
labels:
k8s-app: gitlab-course-spa
project: gitlab-course
spec:
ports:
- protocol: TCP
port: 80
targetPort: 80
selector:
k8s-app: gitlab-course-spa
Данный файл содержит два YAML-документа разделённых ---
.
- Первый документ определяет Deployment для SPA. Deployment в данном случае содержит информацию о том какие Pods с какими контейнерами и в каком количестве мы хотим выполнять. Kubernetes полагается на
labels
для того чтобы понять какие Pods относятся к нашему Deployment. В данном случае в полеselector
мы указываем что это Pods ck8s-app: gitlab-course-spa
. Мы собираем образ локально, поэтому приimagePullPolicy: IfNotPresent
в случае локального развёртывания будет использоваться локальная копия образа. - Второй документ задаёт Service для SPA. Service определяет каким образом наши Pods будут доступны как сетевой ресурс.
💡 Мы дополнительно определяем метку project: gitlab-course
для всех ресурсов нашего проекта чтобы в дальнейшем иметь возможность получать их всех запросом по label
вроде такого:
kubectl get all --selector=project=gitlab-course -n gitlab-course
🛠️ Кажется что у нас есть всё чтобы развернуть SPA, однако есть один нюанс. Дело в том, что мы обращались к API по адресу, заданному через API_URL
, и это значение встроено в образ SPA. Пересоберём образ, используя значение API_URL
, которое мы будем использовать в дальнейшем.
docker build ./spa --build-arg API_URL=http://spa.localtest.me/log -t gitlab-course-spa
🛠️ Создадим ресурсы SPA выполнив команду
kubectl apply -f kubernetes/spa.yml
Убедитесь, что в консоли подтверждается что оба ресурса созданы.
💡 Если вы захотите обновить образ Pod в Kubernetes, потребуется этот Pod пересоздать. Для SPA это можно сделать, например, этой командой.
kubectl rollout restart deployment gitlab-course-api -n gitlab-course
🛠️ Перейдём к API. Добавим файл ./kubernetes/api.yml
с таким кодом.
kind: Deployment
apiVersion: apps/v1
metadata:
name: gitlab-course-api
namespace: gitlab-course
labels:
k8s-app: gitlab-course-api
project: gitlab-course
spec:
replicas: 1
selector:
matchLabels:
k8s-app: gitlab-course-api
template:
metadata:
name: gitlab-course-api
labels:
k8s-app: gitlab-course-api
spec:
containers:
- name: gitlab-course-api
# this is how we named our docker image
image: gitlab-course-api
imagePullPolicy: IfNotPresent
volumeMounts:
- name: gitlab-course-pv
mountPath: /app/data
volumes:
- name: gitlab-course-pv
persistentVolumeClaim:
claimName: gitlab-course-pv-claim
---
kind: Service
apiVersion: v1
metadata:
name: gitlab-course-api
namespace: gitlab-course
labels:
k8s-app: gitlab-course-api
project: gitlab-course
spec:
ports:
- protocol: TCP
port: 80
targetPort: 4000
selector:
k8s-app: gitlab-course-api
Тут всё аналогично SPA кроме этой части:
...
volumeMounts:
- name: gitlab-course-pv
mountPath: /app/data
volumes:
- name: gitlab-course-pv
persistentVolumeClaim:
claimName: gitlab-course-pv-claim
...
Она связана с тем что мы используем Persistent Volumes для того чтобы данные, сохраняемые API могли жить дольше чем Pod.
- В
volumes
мы объявляем новый диск и указываем что ресурсы для него должны быть получены с использованием Persistent Volume Claimgitlab-course-pv-claim
. - В
volumeMounts
мы монтируем этот диск в директорию/app/data
где наше приложение хранит данные.
Однако для того чтобы это работало, нам требуется чтобы уже до этого был определён Persistent Volume Claim gitlab-course-pv-claim
.
🛠️ Создайте новый файл ./kubernetes/volume.yml
с таким кодом.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
reclaimPolicy: Delete
volumeBindingMode: Immediate
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: gitlab-course-pv
labels:
k8s-app: gitlab-course-api
project: gitlab-course
spec:
capacity:
storage: 100Mi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
storageClassName: local-storage
local:
# данный пусть специфичен для Linux subsistem for Windows и транслируется в
# C:volumesdatagitlab-course
# эта директория должна быть создана перед созданием диска
path: "/run/desktop/mnt/host/c/volumes/data/gitlab-course"
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/os
operator: In
values:
- linux
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: gitlab-course-pv-claim
labels:
k8s-app: gitlab-course-api
project: gitlab-course
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Mi
storageClassName: local-storage
volumeName: gitlab-course-pv
Это, пожалуй, самый скучный момент туториала, но зато дальше так скучно уже не будет. Итак, разберёмся что тут и зачем.
Начнём с PersistentVolumeClaim
с именем gitlab-course-pv-claim
. Он определяет некий запрос на ресурсы и мы указываем его в определении SPA. Kubernetes расширяемая система, и потребности, в нашем случае 100MiB
, могут быть удовлетворены разным образом, например предоставлением облачного ресурса. Так как мы публикуем наше приложение локально, мы попросту создаём PersistentVolume
с названием gitlab-course-pv
чуть выше. Чтобы создать PersistentVolume
требуется, в свою очередь, указать storage class который содержит информацию о том, какого рода диск нам требуется. Поэтому ещё выше мы создаём storage class с storageClassName: local-storage
, который подразумевает что диск создан вручную.
🛠️ Примените приведённую выше конфигурацию.
kubectl apply -f kubernetes/volume.yml
Убедитесь что сообщение в консоли подтверждает что все три объекта созданы.
🛠️ Теперь у нас есть всё чтобы создать объекты API.
kubectl apply -f kubernetes/api.yml
Наши контейнеры уже выполняются в Kubernetes, но сервисы, которые мы создали доступны лишь из локальной сети внутри Kubernetes. Одним из способов предоставить доступ к нашему приложению извне является создание объекта который называется Ingress. Ingress определяет правила по которым наши сервисы будут доступны по HTTP.
💡 Убедитесь что в вашей версии Kubernetes установлен ingress controller, например поддерживаемый сообществом ingress-nginx
. Если вы не можете найти соответствующий Deployment, вам может понадобиться установить его. На момент написания этой статья команда для Docker Desktop выглядела так
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.0.4/deploy/static/provider/cloud/deploy.yaml
🛠️ Создадим файл kubernetes/ingress.yml
с таким кодом.
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: gitlab-course-ingress
namespace: gitlab-course
labels:
project: gitlab-course
spec:
rules:
- host: spa.localtest.me
http:
paths:
- path: /log
pathType: Prefix
backend:
service:
name: gitlab-course-api
port:
number: 80
defaultBackend:
service:
name: gitlab-course-spa
port:
number: 80
В нашем случае в результате создания объекта Ingress будет создан reverse-proxy на основе nginx, который будет осуществлять передачу запросов к сервисам нашего приложения и ответов от них обратно клиенту. Суть данной конфигурации в том что мы принимаем запросы на порту 80 и перенаправляем все запросы на SPA помимо запросов по адресу http://spa.localtest.me/log
, которые мы направляем к API. Мы тут используем localtest.me
— доменное имя для которого возвращается IP 127.0.0.1
для всех поддоменов.
🛠️ Создайте ресурс Ingress.
kubectl apply -f kubernetes/ingress.yml
🛠️ Перейдите в браузере по адресу http://spa.localtest.me
. Важно использовать именно этот домен потому для другого запрос к API по адресу http://spa.localtest.me/log
будет заблокирован CORS.
👍 Поздравляю, вы развернули приложение в Kubernetes, хоть пока и локально. Как мы увидим в дальнейшем, использование другого провайдера не будет значительно сложнее.
☁️ Развернём наше приложение в GCP
Создание проекта в GitLab
Мы будем использовать GitLab.com в качестве инсталляции GitLab чтобы избежать хлопот по установке и настройке локальной версии.
🛠️ Если у вас ещё нет учётной записи на GitLab.com, зайдите на https://gitlab.com и заведите её.
GitLab позволяет нам создать проект просто выполнив push в удалённый репозиторий.
🛠️ Воспользуемся для этого следующей командой:
GITLAB_USER_NAME=<user name>
git push https://gitlab.com/<user name>/gitlab-kubernetes
<user name>
тут — ваше имя пользователя на GitLab.com. Дальше будем считать переменную GITLAB_USER_NAME
в других скриптах имеющей то же значение.
Эта команда создаст приватный проект с именем gitlab-kubernetes
внутри вашей учётной записи на GitLab.com.
GitLab предоставляет доступ к "переменным развёртывания", однако для этого наша ветвь должна быть "защищённой" (protected). Мы основательно разберёмся с этими переменными позже, но убедиться что наша ветвь master
protected нам удобнее всего именно сейчас.
🛠️ При помощи браузера зайдите на страницу вашего проекта в GitLab https://gitlab.com/<user name>/gitlab-kubernetes
;
- Выберите в меню слева Settings -> Repository;
- Разверните подменю Protected branches;
- Убедитесь что ветвь
master
перечислена в списке, и в колонке Allowed to push указаны хотя бы Maintainers (значение не равно No one). - Если ветвь
master
в списке отсутствует, добавьте её (Protect) с вышеуказанными настройками.
Создание кластера Kubernetes в GKE через GitLab
💡 Для выполнения задач данной части может понадобиться создать "пробный" аккаунт. Вы можете получить $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 постоянно меняется, поэтому действия могут несколько отличаться, следуйте духу а не букве. В данный момент действия таковы:
- зайдите в проект
gitlab-kubernetes
; - в левом меню выберите Infrastructure -> Kubernetes clusters;
- нажмите кнопку Integrate with a cluster certificate в центре страницы;
- справа выберите вкладку Create new cluster -> Google GKE.
Далее заполните форму.
- Укажите
gitlab-cluster-auto
в качестве названия кластера - Убедитесь что выбран созданный для курса проект GCP
gitlab-kubernetes
- Установите Number of nodes 1 — это дешевле и API не рассчитан на больше одной реплики.
- Можете выбрать Machine type поменьше, например
n1-standard-1
. - Оставьте установленной опцию GitLab-managed cluster.
- Убедитесь что установлена опция Namespace per environment — мы не будем использовать много сред, но полезно включить эту опцию потому что это ближе к "реальной жизни".
Нажмите кнопку Create Kubernetes cluster. Дождитесь завершения процесса, убедитесь что отображается сообщение Kubernetes cluster was successfully created.
💡 На данном этапе может понадобиться зайти в вашу учётную запись GCP и дать GitLab необходимые разрешения.
🛠️ Убедитесь что на странице Kubernetes clusters напротив нашего кластера gitlab-cluster-auto
нет ошибок.
👍 Теперь у нас есть кластер Kubernetes который мы сможем использовать в дальнейшем.
Развёртывание в Кubernetes на GCP "вручную"
📢 При работе с кластером Kubernetes в GCP будем выполнять консольные команды в Google Cloud Console Shell. Это позволит
- упростить настройку авторизации при доступе к кластеру;
- сохранить локальные настройки
kubectl
прежними.
🛠️ Откройте в браузере страницу Google Cloud Shell.
🛠️ В Google Cloud Shell установлен kubectl
, но нам нужно "подключить" его к кластеру. Для этого нам понадобится указать PROJECT_ID. Выведем список проектов в нашем окружении.
gcloud projects list
Например, для меня PROJECT_ID был gitlab-kubernetes-326101
. Присвойте это значение переменной PROJECT_ID
PROJECT_ID=<ваше значение>
Теперь мы можем сохранить данные для подключения kubectl
.
gcloud container clusters get-credentials gitlab-cluster-auto --zone us-central1-a --project $PROJECT_ID
Убедитесь, что выдача была
Fetching cluster endpoint and auth data.
kubeconfig entry generated for gitlab-cluster-auto.
Создадим namespace gitlab-course
kubectl create namespace gitlab-course
Давайте для удобства установим переменную GITLAB_USER_NAME
в Google Cloud Shell
GITLAB_USER_NAME=<user name>
📢 Начнём развёртывание нашего приложения в GKE.
- Создадим Persistent Volume Claim для API.
- Развернём API.
- Загрузим образы API в репозиторий GitLab.
- Cоздадим Deployment и Service API.
- Развернём Ingress, ему будет присвоен внешний IP адрес. IP адрес нужен чтобы указать адрес API при создании образа SPA.
- Протестируем API
- Развернём SPA.
- Пересоздадим образ SPA с использованием полученного адреса и загрузим образ SPA в репозиторий GitLab.
- Наконец создадим Deployment и Service для SPA.
- Добавим правило для SPA в Ingress, обновим Ingress. Протестируем SPA и приложение целиком.
Создадим Persistent Volume Claim для API
💡 При работе с "облачными" провайдерами обычно требуется лишь создать Persistent Volume Claim, а сам диск и реализующий его "облачный" ресурс создаются автоматически средствами провайдера.
🛠️ Удалите StorageClass и PersistentVolume в kubernetes/volume.yml
. Удалите storageClassName: local-storage
в определении PersistentVolumeClaim, это приведёт к использованию стандартного диска в GKE. Также заменим значение namespace
на токен {{NAMESPACE}}
.
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: gitlab-course-pv-claim
namespace: {{NAMESPACE}}
labels:
k8s-app: gitlab-course-api
project: gitlab-course
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Mi
💡 Идея в том что namespace
в зависимости от того как и куда мы развёртываем приложение может может быть различным. По этой причине мы будем заменять {{ТОКЕНЫ}}
на реальные значения перед применением файлов конфигурации. Мы могли бы использовать что-то типа Helm для работы с шаблонами, но мы же не хотим усложнять туториал, не так ли?
🛠️ Загрузите этот файл в Google Cloud Shell. Кнопка "Upload" находится в меню командной строки в интерфейсе Cloud Shell. На момент написания этого текста, опция находилась в подменю "⋮".
По той причине что мы токенизировали файл, нам нужно вначале заменить токен на реальное значение.
Например, так
sed "s/{{NAMESPACE}}/gitlab-course/g" volume.yml > volume-replaced.yml
Затем примените полученную конфигурацию
kubectl apply -f volume-replaced.yml
Или можете осуществить подстановку и применение конфигурации одной строчкой.
sed "s/{{NAMESPACE}}/gitlab-course/g" volume.yml | kubectl apply -f -
Проверьте следующей командой что Persistent Volume Claim создан.
kubectl get persistentvolumeclaim --all-namespaces
Развёртывание API
🛠️ Отредактируйте kubernetes/api.yml
.
Замените namespace
и в Deployment в Service таким образом.
...
namespace: {{NAMESPACE}}
...
Поступим с image
аналогично, мы будем передавать точные версии образа Docker для того чтобы дать Kubernetes знать что Pod следует обновить.
...
image: {{API_IMAGE}}
...
Мы теперь можем подставить другое название образа, но наш локальный образ контейнера не будет доступ для GKE. Нам нужно разместить наш образ в сервисе доступном для Kubernetes. Загрузим локальный образ в GitLab Repository.
🛠️ В локальной консоли наберите
docker login registry.gitlab.com/$GITLAB_USER_NAME/gitlab-kubernetes
и введите свои данные для входа в GitLab.
🛠️ Присвойте образу тэг с префиксом нашего репозитория в GitLab.
docker tag gitlab-course-api registry.gitlab.com/$GITLAB_USER_NAME/gitlab-kubernetes/api
🛠️ Теперь мы можем загрузить наш образ в удалённый репозиторий. Выполните команду
docker push registry.gitlab.com/$GITLAB_USER_NAME/gitlab-kubernetes/api
Дождитесь окончания загрузки и убедитесь что она прошла успешно.
🛠️ В Google Cloud Shell наберите
docker login registry.gitlab.com/$GITLAB_USER_NAME/gitlab-kubernetes
🛠️ Есть нюанс, заключающийся в том, что для того чтобы скачать образ из нашего приватного репозитория на GitLab.com нужно предоставить некоторый секрет. Для этого мы можем загрузить секрет, который был создан по пути ~/.docker/config.json
когда мы выполнили команду docker login
. Выполните эту команду в Google Cloud Shell.
kubectl create secret generic regcred
--from-file=.dockerconfigjson=$HOME/.docker/config.json
--type=kubernetes.io/dockerconfigjson
--namespace=gitlab-course
Последняя часть головоломки с настройкой образа — добавить информацию о нашем секрете для доступа к репозиторию в kubernetes/api.yml
. Разместите этот код в конце определения Deployment сразу после volumes:
на одном уровне с containers:
...
imagePullSecrets:
- name: regcred
...
В Google Cloud мы будем использовать Ingress по умолчанию для передачи траффика с внешнего IP на Pod в Kubernetes. Эта реализация Ingress "под капотом" использует External HTTP/S Load Balancer и требует чтобы все сервисы были доступны как NodePort
.
Изменим тип сервиса в kubernetes/api.yml
.
...
spec:
type: NodePort
...
В итоге внутри kubernetes/api.yml
должен получиться такой код:
kind: Deployment
apiVersion: apps/v1
metadata:
name: gitlab-course-api
namespace: {{NAMESPACE}}
labels:
k8s-app: gitlab-course-api
project: gitlab-course
spec:
replicas: 1
selector:
matchLabels:
k8s-app: gitlab-course-api
template:
metadata:
name: gitlab-course-api
labels:
k8s-app: gitlab-course-api
spec:
containers:
- name: gitlab-course-api
image: {{API_IMAGE}}
imagePullPolicy: IfNotPresent
volumeMounts:
- name: gitlab-course-pv
mountPath: /app/data
volumes:
- name: gitlab-course-pv
persistentVolumeClaim:
claimName: gitlab-course-pv-claim
imagePullSecrets:
- name: regcred
---
kind: Service
apiVersion: v1
metadata:
name: gitlab-course-api
namespace: {{NAMESPACE}}
labels:
k8s-app: gitlab-course-api
project: gitlab-course
spec:
type: NodePort
ports:
- protocol: TCP
port: 80
targetPort: 4000
selector:
k8s-app: gitlab-course-api
Отлично! Теперь наш образ доступен для Kubernetes и мы можем создать Pod!
🛠️ Загрузите файл kubernetes/api.yml
в Google Cloud Console и выполните команду.
sed -e 's/{{NAMESPACE}}/gitlab-course/g' -e "s~{{API_IMAGE}}~registry.gitlab.com/$GITLAB_USER_NAME/gitlab-kubernetes/api~g" api.yml | kubectl apply -f -
Убедитесь что ресурсы созданы успешно.
💡 Обратите внимание что мы используем ~
в качестве разделителя во втором выражении sed
потому что в имени образа уже встречается /
.
💡 Если вы захотите обновить образ Pod в Kubernetes, потребуется этот Pod пересоздать. Для API это можно сделать, например, этой командой.
kubectl rollout restart deployment gitlab-course-api -n gitlab-course
Затем потребуется выполнить kubectl apply
ещё раз, указав sha256 явно, добавив к имени образа конструкцию @sha256:<hash>
.
Развёртывание Ingress
💡 Мы воспользуемся nip.io для того чтобы избежать необходимости регистрировать домен. Проблема тут в том, что для того чтобы указать правильный хост для API в определении Ingress, нам нужно знать внешний IP адрес Ingress. Однако IP адрес будет присвоен Ingress только после того как мы создадим Ingress. Решение в том чтобы вначале создать, а затем изменить Ingress.
🛠️ Создадим Ingress. Измените файл kubernetes/ingress.yml
.
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: gitlab-course-ingress
namespace: {{NAMESPACE}}
labels:
project: gitlab-course
spec:
defaultBackend:
service:
name: gitlab-course-api
port:
number: 80
💡 Чтобы создать Ingress нам нужно указать или хотя бы одно правило либо бэкенд по умолчанию. Мы временно указываем сервис API потому что он уже создан.
🛠️ Загрузите этот файл в Google Cloud Shell.
sed "s/{{NAMESPACE}}/gitlab-course/g" ingress.yml | kubectl apply -f -
🛠️ Давайте дождёмся присваивания внешнего IP нашему Ingress. Это удобно сделать выполните эту команду
kubectl get ingress --all-namespaces --watch
Прекратите выполнение команды при помощи Ctrl + C
когда внешний IP будет присвоен. Присвойте его переменной EXTERNAL_IP
EXTERNAL_IP=<внешний IP полученный предыдущей командой>
Для простоты я буду называть сам адрес тоже EXTERNAL_IP
.
🛠️ Перейдите в браузере по адресу http://EXTERNAL_IP.nip.io
. Например, в процессе тестирования этого курса, адрес для меня выглядел http://34.102.175.33.nip.io/log
Убедитесь, получен ответ с кодом 200 OK
.
Итак, мы почти закончили развёртывать наше приложение в GKE, осталось развернуть SPA и обновить Ingress.
Развёртывание SPA
🛠️ Помните про нюанс с SPA? Внутри образа встроен адрес API, заданный через API_URL
. Хорошие новости в том что мы уже создали Ingress и поэтому знаем адрес, который можем использовать в качестве API_URL
. Пересоберём образ SPA с использованием этого адреса. Выполните эту команду в локальной консоли.
docker build ./spa --build-arg API_URL="http://$EXTERNAL_IP.nip.io" -t registry.gitlab.com/$GITLAB_USER_NAME/gitlab-kubernetes/spa
docker push registry.gitlab.com/$GITLAB_USER_NAME/gitlab-kubernetes/spa
🛠️ Отредактируйте kubernetes/spa.yml
. Это будут те же правки что и в kubernetes/api.yml
до того.
Замените namespace
и в Deployment и в Service таким образом.
...
namespace: {{NAMESPACE}}
...
Поступим с image
аналогично.
...
image: {{SPA_IMAGE}}
...
Изменим тип сервиса в kubernetes/spa.yml
на NodePort
.
...
spec:
type: NodePort
...
Разместите секрет для доступа в репозиторий GitLab в конце определения Deployment на одном уровне с containers:
...
imagePullSecrets:
- name: regcred
...
В итоге должно получиться такое:
kind: Deployment
apiVersion: apps/v1
metadata:
name: gitlab-course-spa
namespace: {{NAMESPACE}}
labels:
k8s-app: gitlab-course-spa
project: gitlab-course
spec:
replicas: 1
selector:
matchLabels:
k8s-app: gitlab-course-spa
template:
metadata:
name: gitlab-course-spa
labels:
k8s-app: gitlab-course-spa
spec:
containers:
- name: gitlab-course-spa
image: {{SPA_IMAGE}}
imagePullPolicy: IfNotPresent
imagePullSecrets:
- name: regcred
---
kind: Service
apiVersion: v1
metadata:
name: gitlab-course-spa
namespace: {{NAMESPACE}}
labels:
k8s-app: gitlab-course-spa
project: gitlab-course
spec:
type: NodePort
ports:
- protocol: TCP
port: 80
targetPort: 80
selector:
k8s-app: gitlab-course-spa
🛠️ Загрузите kubernetes/spa.yml
в директорию ~/gitlab-course
Cloud Shell и выполните команду.
sed -e 's/{{NAMESPACE}}/gitlab-course/g' -e "s~{{SPA_IMAGE}}~registry.gitlab.com/$GITLAB_USER_NAME/gitlab-kubernetes/spa~g" spa.yml | kubectl apply -f -
🛠️ Осталось обновить Ingress — и мы закончили. Давайте сделаем это сейчас. Укажем SPA в качестве сервиса по умолчанию и будем направлять запросы к API если путь равен /log
. Обратите внимание что мы используем Ingress по умолчанию в GKE, и потому указываем pathType: ImplementationSpecific
.
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: gitlab-course-ingress
namespace: {{NAMESPACE}}
labels:
project: gitlab-course
spec:
rules:
- host: {{EXTERNAL_IP}}.nip.io
http:
paths:
- path: /log
pathType: ImplementationSpecific
backend:
service:
name: gitlab-course-api
port:
number: 80
defaultBackend:
service:
name: gitlab-course-spa
port:
number: 80
🛠️ Удалите файл ingress.yml
в Google Cloud Shell, а затем заново загрузите файл kubernetes/ingress.yml
в Google Cloud Shell и примените изменения командой.
sed -e 's/{{NAMESPACE}}/gitlab-course/g' -e "s/{{EXTERNAL_IP}}/$EXTERNAL_IP/g" ingress.yml | kubectl apply -f -
🛠️ Зайдите в браузере на страницу EXTERNAL_IP.nip.io
и убедитесь что наше приложение работает как предполагается. Вам может понадобиться подождать некоторое время пока конфигурация Ingress обновится в GCP.
👍 Поздравляем, вы развернули приложение в Kubernetes в Google Cloud Platform! Осталось только организовать развёртывание приложения в GitLab.
🦊 Развёртывание в Kubernetes при помощи GitLab
Самое сложное позади, осталось разработать pipeline в GitLab. Итак, начнём.
💡 Мы не будем показывать здесь как опубликовать приложение при помощи AutoDevOps (красивое!) потому что AutoDevOps и всё что с ним связано меняется крайне часто, и, кроме того мало подходит для чего-либо сложнее Hello World!
. Тем не менее, я рекомендую пройти официальный туториал чтобы составить впечатление о встроенных возможностях GitLab и использовать их по необходимости в дальнейшем.
💡 Я не думаю что реально предоставить решение в стиле "один размер подходит всем" так как чем "продуктивнее" конвейер тем он сложнее и тем сильнее зависит от платформы на которой происходит развёртывание. По этой причине мы не будем стремиться создать высокооптимизированный конвейер, а вместо этого сосредоточимся на создании решения которое просто понять и которое демонстрирует разные возможности.
📢 План такой
- Удалим ресурсы, созданные на предыдущем этапе
- Зададим переменные окружения
- Реализуем сборку и тестирование
- Реализуем развёртывание в GKE
Удалим ресурсы, созданные на предыдущем этапе
🛠️ Будем удалять ресурсы "сверху вниз". Выполните эти команды в Google Cloud Shell.
kubectl delete ingress gitlab-course-ingress -n gitlab-course
kubectl delete service gitlab-course-api -n gitlab-course
kubectl delete service gitlab-course-spa -n gitlab-course
kubectl delete deployment gitlab-course-api -n gitlab-course
kubectl delete deployment gitlab-course-spa -n gitlab-course
kubectl delete pvc gitlab-course-pv-claim -n gitlab-course
kubectl delete secret regcred -n gitlab-course
Задаём переменные окружения
🛠️ Итак, наш конвейер будет использовать переменную EXTERNAL_IP
, которая будет содержать внешний IP нашего Ingress.
Давайте зададим её значение внутри GitLab.
- Откройте в браузере страницу нашего проекта в GitLab
gitlab-kubernetes
- В левом меню в подменю Settings выберите CI/CD.
- Раскройте подменю Variables.
- Нажмите кнопку Add Variable, в поле Key введите
EXTERNAL_IP
, в поле Value введите значение, полученное вами на предыдущем этапе. На самом деле нет гарантии что GCP присвоит вновь созданному Ingress тот же IP, что и недавно удалённому, но вероятность этого высока, а если этого не произойдём, мы сможем легко это исправить. - Нажмите кнопку Add Variable внизу формы.
Готово!
Если вы отлаживаете ваш конвейер, вы также можете захотеть добавить переменную CI_DEBUG_TRACE
со значением true
. Если вы это сделаете, в логе job будут отображены значения всех переменных и параметров.
💡 Всегда включенный CI_DEBUG_TRACE
создаёт риски для безопасности.
Реализуем сборку и тестирование
🛠️ Создайте файл .gitlab-ci.yml
в корне проекта. Добавьте этот код
image: docker:stable
services:
# we will build our images by running docker daemon inside a container
- docker:dind
variables:
DOCKER_DRIVER: overlay2
SPA_DOCKER_IMAGE: $CI_REGISTRY_IMAGE/spa
SPA_DOCKER_BUILDER_IMAGE: $CI_REGISTRY_IMAGE/spa-build
API_DOCKER_IMAGE: $CI_REGISTRY_IMAGE/api
DOCKER_DRIVER: overlay2
— Согласно документации, overlay2
эффективнее vfs
. На самом деле это значение по умолчанию для shared runners, но мы оставили эту строчку на случай если вы захотите использовать self-hosted runners.
SPA_DOCKER_IMAGE: $CI_REGISTRY_IMAGE/spa
SPA_DOCKER_BUILDER_IMAGE: $CI_REGISTRY_IMAGE/spa-build
API_DOCKER_IMAGE: $CI_REGISTRY_IMAGE/api
Эти переменные мы завели для нашего удобства. Это названия наших образов внутри GitLab registry. Почему у нас только одна переменная для API, но две для SPA?
Для API мы просто упаковываем server.js
внутрь небольшого образа с Node.js.
Для SPA, как вы помните, мы делали двухэтапную сборку.
- С одной стороны, мы хотим чтобы образ в продакшне был как можно меньше.
- С другой стороны, в нашем CD-конвейере мы будем запускать автотесты, а для этого нужны установленные модули NPM, т.е. для этого нам нужен "большой" образ.
Решение в том чтобы разделить нашу двухэтапную сборку на сборку 2-ух образов — "побольше" и "поменьше".
🛠️ Кстати, давайте это сделаем прямо сейчас.
-
Создайте файл
Build.Dockerfile
и скопируйте в него первую часть кода изDockerfile
# build environment FROM node:14-alpine as builder WORKDIR /app ENV PATH /app/node_modules/.bin:$PATH # copy React source codes into the build image COPY package.json ./ COPY package-lock.json ./ COPY src ./src COPY public ./public # install NPM packages according to package-lock.json RUN npm ci # this param needs to be supplied as --build-arg when building the image ARG API_URL # building the react application using the param RUN REACT_APP_API_URL=$API_URL npm run build
-
А вот исходный
Dockerfile
мы сократим. Удалим из файла код приведённый выше и параметризуем название образа, из которого мы будем в итоге копировать файлы.ARG build # build environment FROM $build as builder # production environment FROM nginx:stable-alpine # copy the static site build result from our "heavy" build container # with NPM packages installed to a slim nginx web server image COPY --from=builder /app/build /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
Как видите, просто и логично.
- Добавим ещё один, третий файл
Test.Dockerfile
, в котором мы и будем проводить тесты# test environment FROM spa-build:latest RUN npm run test
💡 В качестве альтернативы, мы могли бы повторно использовать "сборочный" образ, передав команду запуска тестов через командную строку.
Теперь всё готово к тому чтобы реализовать все этапы связанные со сборкой и тестированием.
🛠️ Добавьте в .gitlab-ci.yml
довольно большой кусок кода.
stages:
- build
- test
- package
before_script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
build_spa_builder:
stage: build
script:
- docker build -f ./spa/Build.Dockerfile --build-arg API_URL="http://$EXTERNAL_IP.nip.io" -t $SPA_DOCKER_BUILDER_IMAGE:$CI_COMMIT_SHORT_SHA ./spa
- docker tag $SPA_DOCKER_BUILDER_IMAGE:$CI_COMMIT_SHORT_SHA $SPA_DOCKER_BUILDER_IMAGE:latest
- docker push $SPA_DOCKER_BUILDER_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $SPA_DOCKER_BUILDER_IMAGE:latest
test_spa:
stage: test
script:
- docker run $SPA_DOCKER_BUILDER_IMAGE:latest sh -c "CI=true npm test"
dependencies:
- build_spa_builder
build_spa:
stage: package
script:
- docker build -f ./spa/Dockerfile --build-arg build=$SPA_DOCKER_BUILDER_IMAGE:latest -t $SPA_DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA ./spa
- docker tag $SPA_DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA $SPA_DOCKER_IMAGE:latest
- docker push $SPA_DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $SPA_DOCKER_IMAGE:latest
dependencies:
- build_spa_builder
build_api:
stage: package
script:
- docker build -f ./api/Dockerfile -t $API_DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA ./api
- docker tag $API_DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA $API_DOCKER_IMAGE:latest
- docker push $API_DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $API_DOCKER_IMAGE:latest
Данный код определяет три этапа build
, test
и package
.
Эти этапы в свою очередь содержат jobs. Давайте обсудим какой job что делает.
- build
- build_spa_builder — создаём образ "сборщика"
- test
- test_spa — тестируем SPA
- package
- build_spa — копируем файлы SPA на продуктивный образ
- build_api — копируем файлы API
Команды, указанные в before_script
будут выполнены перед каждым job. Мы используем docker login
чтобы авторизовать runner для доступа к GitLab Docker Registry с использованием данных доступных через переменные окружения.
before_script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
Мы тут используем некоторый переменные, которые начинаются на CI_
, это так называемые переменные GitLab CI/CD. Их значения предоставлены нам платформой.
Большинство jobs выше устроены аналогично за исключением test_spa который проще потому что в итоге мы не загружаем образ в репозиторий. Если вы хотите убедиться, что перед запуском некого job завершится предыдущий, вы можете указать предыдущий в dependencies
. Разберём команды на примере build_spa_builder.
script:
- docker build -f ./spa/Build.Dockerfile --build-arg API_URL="http://$EXTERNAL_IP.nip.io" -t $SPA_DOCKER_BUILDER_IMAGE:$CI_COMMIT_SHORT_SHA ./spa
- docker tag $SPA_DOCKER_BUILDER_IMAGE:$CI_COMMIT_SHORT_SHA $SPA_DOCKER_BUILDER_IMAGE:latest
- docker push $SPA_DOCKER_BUILDER_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $SPA_DOCKER_BUILDER_IMAGE:latest
Тут мы создаём образ, затем тегируем его меткой состоящей из значения нашей переменной и короткого хэша текущего коммита в репозитории GitLab CI_COMMIT_SHORT_SHA
. Затем мы загружаем образ в Docker-репозиторий GitLab. При создании образа SPA мы также используем переменную EXTERNAL_IP
которую задали ранее.
Целей в том чтобы тэгировать образ хэшем коммита две.
- Во-первых, мы поймём из какой версии кода собран образ.
- Во-вторых, для образов, на которые будут ссылаться YAML-файлы ресурсов, Kubernetes сможет понять что образ изменился и что нужно обновить Pod.
🛠️ Итак, волшебный момент запуска нашего конвейера настал. Обновите код в репозитории GitLab следующей командой в локальной консоли. Добавьте весь код, который вы еще не добавили в индекс, закоммитьте и сделайте push.
git add -A
git commit -m "Commit all the code remaining to build images with GitLab"
git push
🛠️ Откройте в браузере страницу CI/CD -> Pipelines в GitLab и убедитесь что сборка и тестирование начались. Перейдите на страницу нашей конкретной сборки и дождитесь её успешного завершения. При этом полезно пооткрывать страницы разных jobs и понаблюдать за обновляющейся выдачей в консоли.
Отлично! Мы уже собираем все нужные нам образы прямо в GitLab.
Реализуем развёртывание в GKE
🛠️ Давайте сделаем последний шаг — реализуем развёртывание в Kubernetes на GCP при помощи GitLab. Итак, добавьте дополнительный stage
в .gitlab-ci.yml
.
stages:
- build
- test
- package
- deploy
Затем после шагов сборки добавьте такой код.
deploy_spa:
stage: deploy
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image"
# нам нужно тут задать environment иначе GitLab не передаст значения переменных начинающихся с KUBE_
environment: production
before_script:
- |
kubectl create secret -n "$KUBE_NAMESPACE"
docker-registry regcred
--docker-server="$CI_REGISTRY"
--docker-username="${CI_DEPLOY_USER:-$CI_REGISTRY_USER}"
--docker-password="${CI_DEPLOY_PASSWORD:-$CI_REGISTRY_PASSWORD}"
--docker-email="$GITLAB_USER_EMAIL"
-o yaml --dry-run | kubectl replace -n "$KUBE_NAMESPACE" --force -f -
script:
# создаём диск
- sed -e "s/{{NAMESPACE}}/$KUBE_NAMESPACE/g" kubernetes/volume.yml | kubectl apply -f -
# развёртываем API
- sed -e "s/{{NAMESPACE}}/$KUBE_NAMESPACE/g" -e "s~{{API_IMAGE}}~$API_DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA~g" kubernetes/api.yml | kubectl apply -f -
# развёртываем SPA
- sed -e "s/{{NAMESPACE}}/$KUBE_NAMESPACE/g" -e "s~{{SPA_IMAGE}}~$SPA_DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA~g" kubernetes/spa.yml | kubectl apply -f -
# обновляем ingress
- sed -e "s/{{NAMESPACE}}/$KUBE_NAMESPACE/g" -e "s/{{EXTERNAL_IP}}/$EXTERNAL_IP/g" kubernetes/ingress.yml | kubectl apply -f -
Тут внимания достоин тот факт что вместо docker: stable
мы используем другой образ. В принципе, нам подойдёт любой образ, имеющий kubectl
, поэтому для простоты мы используем образ registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image
который GitLab использует для развёртывание в Kubernetes в режиме AutoDevOps.
Также мы пользуемся фактом что наш кластер интегрирован с GitLab. По этой причине мы можем полагаться на переменные окружения, которые начинаются с KUBE_
, они будут переданы в конвейер автоматически.
Мы переопределили before_scripts
.
- Мы не хотим чтобы перед выполнением команд нашего
job
была попытка залогиниться вdocker
. - Мы хотим дать нашим Pods возможность загружать образы из GitLab Docker Registry.
Привожу полный код .gitlab-ci.yml
который должен был получиться.
image: docker:stable
services:
- docker:dind
variables:
DOCKER_DRIVER: overlay2
SPA_DOCKER_IMAGE: $CI_REGISTRY_IMAGE/spa
SPA_DOCKER_BUILDER_IMAGE: $CI_REGISTRY_IMAGE/spa-build
API_DOCKER_IMAGE: $CI_REGISTRY_IMAGE/api
stages:
- build
- test
- package
- deploy
before_script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
build_spa_builder:
stage: build
script:
- docker build -f ./spa/Build.Dockerfile --build-arg API_URL="http://$EXTERNAL_IP.nip.io" -t $SPA_DOCKER_BUILDER_IMAGE:$CI_COMMIT_SHORT_SHA ./spa
- docker tag $SPA_DOCKER_BUILDER_IMAGE:$CI_COMMIT_SHORT_SHA $SPA_DOCKER_BUILDER_IMAGE:latest
- docker push $SPA_DOCKER_BUILDER_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $SPA_DOCKER_BUILDER_IMAGE:latest
test_spa:
stage: test
script:
- docker run $SPA_DOCKER_BUILDER_IMAGE:latest sh -c "CI=true npm test"
dependencies:
- build_spa_builder
build_spa:
stage: package
script:
- docker build -f ./spa/Dockerfile --build-arg build=$SPA_DOCKER_BUILDER_IMAGE:latest -t $SPA_DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA ./spa
- docker tag $SPA_DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA $SPA_DOCKER_IMAGE:latest
- docker push $SPA_DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $SPA_DOCKER_IMAGE:latest
dependencies:
- build_spa_builder
build_api:
stage: package
script:
- docker build -f ./api/Dockerfile -t $API_DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA ./api
- docker tag $API_DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA $API_DOCKER_IMAGE:latest
- docker push $API_DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $API_DOCKER_IMAGE:latest
deploy_spa:
stage: deploy
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image"
# нам нужно тут задать environment иначе GitLab не передаст значения переменных начинающихся с KUBE
environment: production
before_script:
- |
kubectl create secret -n "$KUBE_NAMESPACE"
docker-registry regcred
--docker-server="$CI_REGISTRY"
--docker-username="${CI_DEPLOY_USER:-$CI_REGISTRY_USER}"
--docker-password="${CI_DEPLOY_PASSWORD:-$CI_REGISTRY_PASSWORD}"
--docker-email="$GITLAB_USER_EMAIL"
-o yaml --dry-run | kubectl replace -n "$KUBE_NAMESPACE" --force -f -
script:
# создаём диск
- sed -e "s/{{NAMESPACE}}/$KUBE_NAMESPACE/g" kubernetes/volume.yml | kubectl apply -f -
# развёртываем API
- sed -e "s/{{NAMESPACE}}/$KUBE_NAMESPACE/g" -e "s~{{API_IMAGE}}~$API_DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA~g" kubernetes/api.yml | kubectl apply -f -
# развёртываем SPA
- sed -e "s/{{NAMESPACE}}/$KUBE_NAMESPACE/g" -e "s~{{SPA_IMAGE}}~$SPA_DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA~g" kubernetes/spa.yml | kubectl apply -f -
# обновляем ingress
- sed -e "s/{{NAMESPACE}}/$KUBE_NAMESPACE/g" -e "s/{{EXTERNAL_IP}}/$EXTERNAL_IP/g" kubernetes/ingress.yml | kubectl apply -f -
🛠️ Закоммитьте изменения и выполните push в репозиторий
git add -A
git commit -m "Commit the code to deploy to GKE with GitLab"
git push
Перейдите по адресу http://EXTERNAL_IP.nip.io
в браузере и убедитесь что приложение заработало. Может понадобиться подождать несколько минут чтобы все ресурсы успели обновиться.
💡 Если приложение не открывается, но Deployments функционируют без ошибок, особенно если при переходе по адресу выше вы получаете код 404, есть вероятность что GCP присвоил вновь созданному Ingress другой внешний IP. Это легко исправить!
- Узнайте новый адрес IP
kubectl get ingress -n <namespace созданнае GitLab>
- Измените значение переменной
EXTERNAL_IP
в GitLab соответственно. - Перезапустите конвейер в GitLab.
🎉 Поздравляю!
Вы докеризовали приложение на React.js, затем развернули его в Kubernetes вручную и в итоге создали конвейер непрерывного развёртывание этого приложения в Kubernetes при помощи GitLab!
🧹 Не забудьте удалить ненужные ресурсы, созданные на Google Cloud Platform.
Автор:
NickT