Развёртывание в Kubernetes из GitLab (часть 1)
Это продолжение предыдущего туториала про командную разработку с использованием 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.
В дальнейшем мы сделаем следующее в части 1.
- 🧱 Познакомимся c нашим приложением, обсудим из чего оно состоит.
- 🐳 Докеризуем наше приложение.
- ☸️ Развернём наше приложение в Kubernetes локально на Docker Desktop.
- ☁️ Обсудим особенности GCP и как нужно изменить наше приложение, а затем ещё раз развернём наше приложение в Kubernetes но уже в GCP.
В части 2.
- 🦊 Завершим наш туториал созданием конвейера для развертывания приложения в 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:\volumes\data\gitlab-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.