Pull to refresh
VK
Building the Internet

Запуск проекта в Kubernetes за 60 минут: инструменты, GitLab, Terraform

Reading time14 min
Views14K


Привет, Хабр! Меня зовут Илья Нырков, я архитектор в VK Cloud. В своей работе встречаюсь с желанием партнеров (это и крупный энтерпрайз, и различные стартапы) использовать Kubernetes, но их останавливает сложность поднятия, конфигурирования кластера, деплоя в нём приложений и построения CI/CD-процессов вокруг него. Я постараюсь показать на практическом примере, который вы можете повторить сами, как развернуть за сравнительно небольшое время полноценный CI/CD с рабочим приложением, доступным для внешних пользователей.

Мы рассмотрим пример CI/CD-пайплайна, который собирает разбитое на два микросервиса CRUD-приложение, пушится в платформенный реджистри и деплоится в Kubernetes. 



Мы рассмотрим все шаги поднятия: создание кластера и базы данных, настройка абстракций Kubernetes вроде Service, Ingress, Deployment и т. д. и установку платформенных аддонов в кластер. Также мы рассмотрим поднятие инфраструктуры стенда при помощи подхода Infrastructure-as-a-code. Все файлы Terraform и Kubernetes, код приложения и сокращённая пошаговая инструкция доступны в репозитории

Уложитесь ли вы в час, зависит от скорости создания ресурсов и размера кластера, который вы будете создавать. Сами действия, если выполнять их внимательно, точно займут даже меньше часа.

Архитектура приложения


В качестве примера возьмём приложение на Go, работающее с базой данных пользователей и разбитое на два микросервиса: add_user (добавление и удаление пользователей) и get_users (получение информации о пользователях). Управлять ими будем с помощью API-вызовов.


Архитектура проекта

Для обоих приложений подготовлены Docker-файлы, обеспечивающие сборку, и Make-файл с командой для запуска сборки образов. В качестве хранилища данных пользователей будет использоваться платформенный сервис VK Cloud PostgreSQL в сингл-инстанс-конфигурации.

Для развёртывания приложений в кластере Kubernetes описаны соответствующие Deployment и Service, а также Ingress-объект, описывающий правила публикации приложений в интернете для кластера Kubernetes.

Подготовка инфраструктуры


Для поднятия инфраструктуры для нашего приложения воспользуемся подходом Infrastructure-as-a-Code, при котором все компоненты описываются в конфигурационных файлах. Де-факто стандартом в таком подходе стал инструмент Terraform (инструкция по установке), для которого у VK Cloud есть свой провайдер. Я расскажу о базовой конфигурации, а Алексей Волков, менеджер продукта Kubernetes в VK Cloud, подробнее рассказал о возможностях работы нашего провайдера Terraform с платформенным Kubernetes.

Инфраструктура будет использовать новый sdn — sprut, подробнее про него можно прочитать в этой статье.

Для начала нужно настроить двухфакторную аутентификацию и активировать доступ по API в проекте VK Cloud. Дальше нужно скачать RC-файл из личного кабинета, его можно использовать для доступа к облаку по API через OpenStack CLI и Terraform.


Получение RC-файла для доступа к API облака через Terraform

Указываем данные для доступа к API облака через Terraform при помощи команды:

source <имя RC-файла>

Конфигурационные файлы для поднятия инфраструктуры находятся в директории Terraform. Файлы из этого репозитория следует скопировать в свой собственный и вести работу в нём.

Опционально: можно указать удалённое хранилище в виде S3 для tfstate. Для этого создайте бакет S3 в VK Cloud, укажите его имя и добавьте этот блок кода в файл terraform/provider.tf.

terraform {
  backend "s3" {
    bucket = <имя бакета S3>
    key = "terraform.tfstate"
    endpoint = "https://hb.ru-msk.vkcs.cloud/"

    skip_region_validation = true
    skip_credentials_validation = true
    skip_metadata_api_check = true
  }
}

Или можно не указывать и хранить стейт локально.

Указываем переменную окружения с паролем, который будет установлен инстансу базы данных. В файле terraform/variables.tf выставляем значение переменной db_password. Важно помнить о критериях пароля БД для платформенного сервиса, иначе создание может завершиться ошибкой: «Заглавные и строчные буквы латинского алфавита, цифры, символы !$&*()-+=.,» 

Пароль должен содержать хотя бы одну букву и цифру, помимо специальных символов.

export TF_VAR_db_password=<пароль бд>

Переходим в папку с файлами Terraform:

cd terraform

При помощи конфигурационных файлов в директории мы развернём в проекте VK Cloud следующие компоненты инфраструктуры:

  • кластер Kubernetes-as-a-Service:

    ○ один мастер: тип виртуальной машины: 2-4 (2 CPU, 4 ГБ памяти). Диск: 50 ГБ;

    ○ одна нод-группа: тип виртуальной машины: 2-4 (2 CPU, 4 ГБ памяти). Минимальное кол-во нод: 3. Максимальное кол-во нод: 4;
  • инстанс PostgreSQL:

    ○ тип виртуальной машины: 2-8 (2 CPU, 8 Гб памяти);
  • сеть/подсеть;
  • роутер.

Проект Terraform в этом примере имеет следующую структуру:

  • variables.tf,
  • main.tf,
  • data.tf,
  • provider.tf.

Рассмотрим содержимое каждого файла.

В provider.tf описаны настройки провайдера. На данный момент актуальная версия — 0.6.1, другие можно посмотреть на GitHub провайдера.

terraform {
  required_providers {
    vkcs = {
      source = "vk-cs/vkcs"
      version = "~> 0.6.1"
    }
  }
}

В файле variables.tf описаны значения переменных. Они могут быть разных типов. Значения можно задать либо в поле default, либо вручную при создании (если не выставлено), либо выставив переменную окружения на хосте, где применяется Terraform, с префиксом TF_VAR_.

# ---- KUBERNETES -----

variable "k8s_master_count" {
  type = number
  default = 1
}

variable "k8s_master_volume_size" {
  type = number
  default = 50
}
...

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

data "vkcs_compute_flavor" "k8s_node" {
  name = "Standard-2-4-50"
}

data "vkcs_kubernetes_clustertemplate" "ct" {
  version = "1.26"
}

...

В файле main.tf описаны ресурсы, которые будут созданы в облаке. В нашем случае создаются:

  • кластер Kubernetes и устанавливается дополнение;
  • одиночный экземпляр БД Postgres;
  • сеть, подсеть и роутер.

Инициализируем Terraform:

terraform init


В результате будут созданы необходимые файлы вроде tfstate для хранения состояния и tf.lock для совместной работы нескольких пользователей.

Применяем конфигурационные файлы:

terraform apply

Вы увидите ресурсы, которые будут созданы или изменены:



Введём «yes» и запустим создание ресурсов. В результате увидим сообщение Terraform об успешном создании и информацию в блоках «output», и будут созданы ресурсы в облаке.



Подготовка репозитория и инфраструктуры для работы с ним


Создайте новый Private репозиторий в публичном GitLab.


Cоздание репозитория в GitLab

Клонируйте репозиторий и перейдите в директорию с ним. Скопируйте содержимое репозитория статьи

Для подключения к Kubernetes вам понадобятся утилиты keystone authentication и kubectl. Подключитесь к кластеру Kubernetes, подробности описаны в документации.

Установите в кластер Kubernetes исполнители GitLab — это приложения, которые выполняют задачи (job), то есть этапы некоторого CI/CD-пайплайна. 
Добавим репозиторий с Helm-чартом (установка Helm):

helm repo add gitlab https://charts.gitlab.io

Если репозиторий уже добавлен, можно проверить его на обновления:

helm repo update gitlab

Helm — это, по сути, пакетный менеджер для Kubernetes, позволяющий с помощью «чартов» — пакетов с набором всех необходимых ресурсов — разворачивать приложения в кластере. Helm предоставляет широкий инструментарий для создания таких пакетов, главным из которых является шаблонизация.

Создадим секрет (secret) с токеном раннера. Секрет — это ресурс в Kubernetes, позволяющий хранить sensitive информацию. Токен раннера необходим для аутентификации приложения-раннера в соответствующем репозитории GitLab. Получить его можно в веб-интерфейсе GitLab.

Создадим раннер:



Выбираем тип Linux. Это не столь важно, главное — создать сам токен:



Копируем токен, он понадобится нам для установки исполнителей GitLab через Helm:


Токен раннера

Создадим секрет с полученным runner token:

cat << EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
  name: dev-gitlab-runner
  namespace: gitlab
type: Opaque
stringData:
  # это поле необходимо явно объявить для обратной совместимости
  runner-registration-token: ""
  # тут подставляем полученный в веб-интерфейсе токен
  runner-token: "glrt-1KJ_ky-66jzYyrpvxM_L"
EOF


Для деплоя раннеров нам понадобится конфигурационный файл раннера GitLab gitlab-config-values.yml

gitlabUrl: <a href="https://gitlab.com/">https://gitlab.com</a>
# поле для обратной совместимости, мы воспользуемся токеном из
# ранее созданного секрета
runnerToken:
# Это настройки Role-Based Access Control (RBAC) для раннера в Kubernetes. Это права, которые будет иметь под GitLab в кластере
rbac:
# создаём роль с необходимыми правами, если её не существует
 create: true
# ресурсы и действия с ними, которые разрешены
 rules:
 - resources: ["pods", "secrets", "configmaps"]
   verbs: ["get", "list", "watch", "create", "patch", "delete", "update"]
 - apiGroups: [""]
   resources: ["pods/exec", "pods/attach"]
   verbs: ["create", "patch", "delete"]
# Ресурсами, описанными выше, под GitLab сможет управлять во всех
# неймспейсах. Для продсреды лучше указывать конкретные неймспейсы
 clusterWideAccess: true
# отключаем pod security policy для подов GitLab раннера 
 podSecurityPolicy:
   enabled: false
   resourceNames:
   - gitlab-runner
# конфигурация самих раннеров
runners:
 config: |
   # можно задать уровень логирования у подов раннера
   log_level = "debug"
   # настройки подов раннеров
   [[runners]]
     [runners.kubernetes]
       # базовый образ
       image = "ubuntu:22.04"
       # для простоты создаются поды с максимальными привилегиями
       # использовать поды с рут-правами настоятельно не рекомендуется
       privileged = true
       allow_privilege_escalation = true
# указываем тип раннера
 executor: kubernetes
# указываем имя ранее созданного секрета
 secret: dev-gitlab-runner

Деплоим раннеры GitLab через Helm.

helm install gitlab-runner gitlab/gitlab-runner -f gitlab-config-values.yml -n gitlab

Деплой собранного приложения


Создадим сервис-аккаунт, который будет использован GitLab для деплоя приложений в кластере. Это служебная учётная запись для отправки запросов в API-сервер из подов.

kubectl create sa deploy -n vk-cloud-project

И создадим rolebinding, являющийся привязкой служебного аккаунта к созданной роли:

kubectl create rolebinding deploy \
  -n vk-cloud-project \
  --clusterrole edit \
  --serviceaccount vk-cloud-project:deploy

Создадим токен под этот сервис-аккаунт:

kubectl apply -n vk-cloud-project -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
  name: deploy
  annotations:
    kubernetes.io/service-account.name: deploy
  type: kubernetes.io/service-account-token
EOF

Получим значение токена сервис-аккаунта:

kubectl get secrets -n vk-cloud-project deploy -o jsonpath='{.data.token}'

В разделе переменных GitLab создадим переменную K8S_CI_TOKEN со значением токена. полученным ранее. Токен будет использоваться GitLab для отправки запросов в API-сервер Kubernetes при деплое приложений, описанных в пайплайне:


Создание переменной GitLab


Указание значения переменной GitLab

Далее нужно создать токен, который будет использоваться Kubernetes для извлечения образов контейнеров из реджистри GitLab.

Создаём pull token в web-интерфейсе:





Сохраним полученные значения:



Создадим секрет, который будет использоваться для доступа к реджистри GitLab:

kubectl create secret docker-registry vkcloud-kubernetes-project-image-pull \
--docker-server registry.gitlab.com \
--docker-email 'admin@mycompany.com' \
--docker-username '<первая строка>' \
--docker-password '<вторая строка>' \
--namespace vk-cloud-project

Подготовка базы данных для работы с приложением


Создадим секрет, который под будет использовать для доступа к базе данных. Для этого используем информацию, которую указывали при создании инстанса БД Postgres.

kubectl create secret generic user-app \
--from-literal=DB_NAME=<имя бд> \
--from-literal=DB_USER=<имя пользователя БД> \
--from-literal=DB_PASSWORD=<пароль пользователя БД> \
--from-literal=DB_HOST=<серый IP-адрес БД> \
--from-literal=DB_PORT=5432

Описание ресурсов Kubernetes, используемых в приложении


Рабочая нагрузка


Под (Pod)


Базовой единицей нагрузки в кластере Kubernetes является под: в нём запускаются приложения при помощи контейнеров. В одном поде может быть несколько контейнеров — например, ещё Sidecar-контейнер для сбора логов или Reverse-прокси при использовании подхода Service Mesh. В нашем Kubernetes используется среда контейнеризации CRI-O. Поды запускаются на нодах кластера. Сам под редко создаётся отдельно, при деплое приложений используется ресурс Deployment, который создаёт поды через ReplicaSet.



Набор реплик (ReplicaSet)


ReplicaSet — это ресурс в Kubernetes, который следит за заданным при помощи меток (Labels) и селекторов (Selector) количеством подов. Если один из подов упадёт, репликасет попробует поднять новый. Если уменьшить количество подов в репликасете, то он удалит лишние.



Деплоймент (Deployment)


Deployment — это основной способ поднятия приложений. При создании деплоя создаётся репликасет, который создаёт поды. Деплой отвечает за версионирование приложений и плавное обновление (под за подом).



Сетевые абстракции и публикация приложений


Для публикаций приложений в Kubernetes используется ресурс Service, у которого есть несколько типов.

ClusterIP 


Сервис типа СlusterIP публикует под, деплоймент или репликасет с помощью меток или селекторов внутри кластера (по аналогии с методом определения подов, принадлежащим конкретному репликасету). Будет назначен IP, доступный только внутри кластера. При отправке запросов на этот IP они будут балансироваться между подами приложений (в случае репликасета или деплоймента).



NodePort


Сервис типа NodePort позволяет открывать приложения во внешний мир, публикуя их через порты на нодах кластера. Порты выделяются в диапазоне 30 000–32 767, так что это неудобный способ доступа к приложению извне. Также возникает сложность из-за необходимости назначения внешних IP всем воркер-нодам, так как поды могут перемещаться на разные ноды и все они должны быть доступны.



LoadBalancer 


LoadBalancer позволяет решить описанную выше проблему с NodePort и открывать приложение в интернет по нормальному порту. При создании сервиса такого типа отправляется запрос в облако на создание платформенного балансировщика, который будет принимать запросы на свой внешний IP и порты 80/443 и направлять трафик в воркер-ноды по портам NodePort.



Ingress Controller


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

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



Запуск пайплайна и проверка работоспособности приложения


После внесения всех изменений можно выполнить git push. Конвейер отработает, используя конфигурационный файл .gitlab-ci.yml и развернёт приложение:



В .gitlab-ci.yml описано 2 этапа (stage), в которых выполняются джобы.
stages:
  - build
  - deploy


В нашем случае, во время этапа build собираются и пушатся образы приложений add_user, get_users в контейнерный реджистри репозитория gitlab:
build_add_user:
  image: docker:19.03.12
  stage: build
  services:
    - docker:19.03.12-dind
  variables:
    DOCKER_HOST: tcp://docker:2375
    DOCKER_TLS_CERTDIR: ""
  before_script:
# выполняем login в контейнерный реджистри репозитория гитлаба
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
# собираем приложение с названием на основе идентификаторов коммита и выполняемого пайплайна
    - cd app && docker build -t "$CI_REGISTRY_IMAGE:add_user.$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID" -f cmd/add_user/Dockerfile .
# пушим собранный образ приложения в реджистри
    - docker push "$CI_REGISTRY_IMAGE:add_user.$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID"

# тоже самое, что и для add_user
build_get_users:
  image: docker:19.03.12
  stage: build
  services:
    - docker:19.03.12-dind
  variables:
    DOCKER_HOST: tcp://docker:2375
    DOCKER_TLS_CERTDIR: ""
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
    - cd app && docker build -t "$CI_REGISTRY_IMAGE:get_users.$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID" -f cmd/get_users/Dockerfile .
    - docker push "$CI_REGISTRY_IMAGE:get_users.$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID" 


Во время этапа deploy разворачиваются приложения в kubernetes из образов, которые были запушены ранее:

deploy:
  stage: deploy
  image: 
    name: bitnami/kubectl:latest
    entrypoint: ['']
  variables:
    K8S_NAMESPACE: vk-cloud-project
  before_script:
    # Собираем кубконфиг, который гитлаб будет использовать для выполнения команд в кластере
    - export KUBECONFIG=/tmp/kubeconfig
    - kubectl config set-cluster k8s --insecure-skip-tls-verify=true --server=https://kubernetes.default
    - kubectl config set-credentials ci --token="$(echo $K8S_CI_TOKEN | base64 -d)"
    - kubectl config set-context ci --cluster=k8s --user=ci --namespace $K8S_NAMESPACE
    - kubectl config use-context ci
  script:
# редактируем in-place файл с описанием деплоймента, указав последнюю версию образа приложения add-user, загруженного в реджистри гитлаба
    - sed -i -e "s,<add_user_image>,$CI_REGISTRY_IMAGE:add_user.$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID,g" kube/add-user.yml
# тоже самое для приложения get-users
    - sed -i -e "s,<get_users_image>,$CI_REGISTRY_IMAGE:get_users.$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID,g" kube/get-users.yml
# применяем файлы с описанием деплойментов приложений и сервисами
    - kubectl -n $K8S_NAMESPACE apply -f kube/
# в случае ошибки деплоя приложения, откатываем деплой назад, на прошлую версию
    - kubectl -n $K8S_NAMESPACE rollout status deployment/add-user ||
      (kubectl rollout undo deployment/add-user && exit 1)
    - kubectl -n $K8S_NAMESPACE rollout status deployment/get-users ||
      (kubectl -n $K8S_NAMESPACE rollout undo deployment/get-users && exit 1)
# настраиваем horizontal pod autoscaler для приложения get-users:
# --cpu-percent=50 - процент нагрузки на цпу подов, при достижении/превышении которого, будут создаваться новые поды, вплоть до количества указанного в --max
# --min - минимальное количество подов, которое может существовать при низкой нагрузке
# --max - максимальное количество подов, которое может существовать при высокой нагрузке
    - kubectl autoscale deployment get-users --cpu-percent=50 --min=3 --max=10


После поднятия приложения, для просмотра внешнего IP Ingress Controller, на который можно слать запросы, нужно выполнить команду:

kubectl get svc -n ingress-nginx



Смотрим на TYPE = LoadBalancer и значение EXTERNAL-IP. Можно обращаться по DNS-имени или отбросить .nip.io и использовать внешний IP.

Проверка работоспособности приложения


Сервис get users


Получение списка пользователей:

curl <внешний IP ingress>/info/users

Результат:

[{"id":1,"name":"Maxim","location":"Moscow","age":21},{"id":2,"name":"Dima","location":"Moscow","age":22}]

Получение пользователя по ID:

curl <внешний IP ingress>/info/user/{id}

Результат:

{"id":1,"name":"Maxim","location":"Moscow","age":21}

Сервис add user


curl -X POST -H "Content-Type: application/json" -d '{
  "name": "Dima",
  "location": "Moscow",
  "age": 22 }' http://<внешний_IP_ingress>/user-storage/user

Результат:

{"id":2,"message":"User created successfully"}

Проверка выкатки новых версий


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

для сервиса add_user:

curl http://<внешний IP ingress>/user-storage/version

для сервиса get_users:

curl http://<внешний IP ingress>/info/version

Для тестирования изменений можно переписать вывод сообщения у метода /info/hello. Для этого в файле app/internal/middleware/handlers.go отредактируйте переменную helloMessage:

func Hello(w http.ResponseWriter, r *http.Request) {
  helloMessage := "hello message" // здесь можно изменить сообщение

  res := response {
  Message: helloMessage,
  }

  json.NewEncoder(w).Encode(res)
}

Тестирование автомасштабирования


В джобе «deploy» ci/cd пайплайна, описанного ранее, мы настроили hpa для сервиса get-users. Автоскейлинг нод был настроен в конфигурационном файле terraform. Проверить его можно, подав нагрузку на сервис. Для этого применим файл get-users-load-generator.yml. Подставим полученный ранее внешний IP Ingress в файл. Также мы можем менять количество реплик для изменения нагрузки.

kubectl apply -f get-users-load-generator.yml

Статус HPA можно посмотреть командой:

kubectl get hpa

При нехватке ресурсов кластера для новых реплик поды будут в статусе pending, пока Autoscaler не создаст новые ноды.

Удаление ресурсов


После удаления источника нагрузки поды, созданные HPA, и ноды, созданные Autoscaler, будут удалены.

Удалить кластер и инстанс базы данных можно через личный кабинет, а в случае с Terraform — командой:

terraform destroy


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

Вы прямо сейчас можете воспользоваться Kubernetes от VK Cloud. Для тестирования мы начисляем новым пользователям 3000 бонусных рублей и будем рады вашей обратной связи.

Stay tuned.

Присоединяйтесь к телеграм-каналу «Вокруг Kubernetes», чтобы быть в курсе новостей из мира K8s: регулярные дайджесты, полезные статьи, а также анонсы конференций и вебинаров.
Tags:
Hubs:
Total votes 38: ↑37 and ↓1+36
Comments0

Articles

Information

Website
vk.com
Registered
Founded
Employees
5,001–10,000 employees
Location
Россия
Representative
Миша Берггрен