Сегодня Вы узнаете, как онлайн, с смс и регистрацией задеплоить своё приложение в kubernetes. Поехали!

Совсем мало теории

Итак, что такое kubernetes? Это штука, предназначенная для автоматизации развёртывания, масштабирования и координации контейнеризированных приложений в условиях кластера (спасибо, википедия). Для деплоя приложений в kubernetes можно использовать очень удобный инструмент - helm. Для того чтобы им воспользоваться, вам нужен chart одного или нескольких ваших приложений. chart - описание ресурсов, необходимые для запуска приложения. Выглядит это как куча yaml файлов, в которых описаны объекты kubernetes.

Итого, нам понадобятся:

  • Один кластер kubernetes. Нету? Что ж, гуглим что-нибудь бесплатное. Находим OpenShift Sandbox. Есть.

  • Утилиты oc (OpenShift CLI) и helm. Качаем. Есть.

  • Приложения для деплоя. Тыщу лет назад писал про то как собрать образы для Spring Boot и Vue.js приложений. Вот их и возьмём. Есть.

  • Чарты для деплоя. Нету. Напишем.

OpenShift Sandbox

Идём на сайт Red Hat за песочницей. На текущий момент Red Hat позволяет получить песочницу на 30 дней. По истечении этого времени песочница будет удалена, но вы сможете её пересоздать снова на 30 дней. Ограничения на количество попыток повторного заведения песочниц, на сколько я знаю, на текущий момент нет. При создании песочницы потребуется ввести свой номер телефона для подтверждения по смс (потребуется каждый раз при пересоздании песочницы).

После создания песочницы нужно будет войти в неё из консоли с помощью утилиты oc. Для этого жмём сюда:

А затем сюда:

Интересующая нас команда:

Через какое-то время токен протухнет и нужно будет перелогинеться.

Helm chart

Готовые чарты можно посмотреть тут. А ниже приведён процесс готовки этих самых чартов.

Создать чарты можно командой:

helm create list-keep-chart

Так же чарты можно создать, например, с помощью IntelliJ IDEA, выбирайте удобный для вас вариант.

Cтруктура чарта, созданного данной командой, представлена ниже.

Созданный чарт будет является вполне рабочим, достаточно подставить свои значения в values.yaml. Правда, данный чарт предназначен для одного приложения. Если требуется (как в нашем случае) несколько, то нужно изменить структуру чарта. Для каждого приложения требуется свой чарт. Чтобы чарты всех приложений находились в одном чарте - нужно будет положить их как сабчарты. Для этого достаточно просто положить их в папку charts родительского чарта. В родительском чарте будут только содержаться сабчарты и больше ничего в нём происходить не будет, поэтому нужно удалить из него папку templates с шаблонами, описывающими объекты kubernetes. Также требуется очистить файл values.yaml, содержащий значения для шаблонов из templates.

Чарты для Spring Boot приложения

Создадим сабчарт для Spring Boot приложения. Создание сабчартов ничем не отличается от создания просто чартов (кроме, разве что, того, что они должны быть созданы в директории charts родительского чарта). Структура данного сабчарта представлена ниже.

Если кому-то интересно, то подробно познакомиться со структурой чартов и конкретно с шаблонами можно в соответствующих разделах документации на сайте helm'а, в статье затрону лишь то, с чем предстоит непосредственно взаимодействовать.

Также следует добавить сабчарт как зависимость в Chart.yaml родительского чарта, т.е. в list-keep-chart\Chart.yaml. Это не обязательно, но считается best practice. В конец файла добавим:

 dependencies:
  - name: list-keep
    condition: list-keep.enabled
    version: "*"

name - имя сабчарта. condition - условие с помощью которого можно включать/выключать сабчарт, т.е. управлять тем нужно ли деплоить это приложение в kubernetes или нет. version - версия в формате semver. В данном случае в качестве версии указано значение * для того, чтобы при изменении версии в сабчарте не менять версию в родительском Chart.yaml.

Итак, что вообще требуется, чтобы задеплоить наш сервис? Как минимум нужно как-то указать наш докер образ. Т.к. это веб сервис, то ещё нужно как-то указать порт контейнера. Собственно, осталось понять как. Для этого проще всего посмотреть, что сгенерилось в values.yaml сабчарта.

Заходим в list-keep-chart\charts\list-keep\values.yaml и, в общем-то, сразу же вверху файла видим как следующее:

image:
  repository: nginx
  pullPolicy: IfNotPresent
  # Overrides the image tag whose default is the chart appVersion.
  tag: ""

Достаточно переопределить repository и tag , для того чтобы задать свой образ.

Теперь поищем как нам указать свой порт. Для этого листаем ниже и находим следующее:

service:
  type: ClusterIP
  port: 80

Видим здесь port. Но, увы, это не тот порт, что мы ищем. Этот порт используется в шаблоне service.yaml, который описывает соответствующий объект kubernetes - Service. Данный объект, по сути, определяет способ предоставления доступа к нашему приложению. В данном конкретном случае он описывает, что наше приложение принимает трафик на порту 80. Также у Service существует параметр targetPort, если он не задан, то он равен тому же значению, что и port. Данный параметр задаёт порт назначения, т.е. это будет порт в вашем контейнере (а точнее в pod).

Что ж, ищем дальше. Открываем deployment.yaml. Этот шаблон описывает объект kubernetes - Deployment. Этот объект предназначен для деплоя вашего приложения, и в нём описывается, как это нужно делать. Интересующий нас блок:

          ports:
            - name: http
              containerPort: 80
              protocol: TCP

Сделаем так, чтобы сюда подставлялось значение порта из values.yaml. Установим сюда то же значение, что сейчас устанавливается для Service, т.е. значение порта, на котором приложение принимает трафик, будет совпадать со значением порта из контейнера. Сделать это можно следующим образом:

          ports:
            - name: http
              containerPort: {{ .Values.service.port }}
              protocol: TCP

Т.к. у нас Spring Boot приложение, то нужно ещё каким-то образом указать профиль при запуске приложения. Сделать это можно, например, указав переменную окружения SPRING_PROFILES_ACTIVE. Но в сгенерериванном чарте по умолчанию такой возможности нет. Добавить её можно в deployment.yaml в конфигурацию контейнера - spec.template.spec.containers. Сделаем так, чтобы можно было задавать переменные окружения через файл valus.yaml:

          {{- with .Values.env }}
          env:
            {{- toYaml . | nindent 12 }}
          {{- end }}

Здесь, с помощью with меняется текущая область видимости на .Values.env. А с помощью toYaml копируются значения текущей области видимости . с отступом, который задаётся с помощью nindent, в 12 пробелов. Т.е., если мы зададим в valus.yaml блок env , значения из него будут скопированы в deployment.yaml в блок spec.template.spec.containers.env.

Казалось бы всё, но нет. Сейчас приложение не сможет подняться (а точнее оно будет постоянно перезапускаться) из-за livenessProbe и readinessProbe в конфигурации контейнера. Kubernetes используетlivenessProbe для определения жив или или мёртв контейнер. Если проверка не проходит - значит контейнер мёртв. readinessProbe используется для определения готовности контейнера принимать сетевой трафик. Если не проходит - значит kubernetes не будет отправлять сетевой трафик в этот контейнер. Сгенерирована была следующая конфигурация:

          livenessProbe:
            httpGet:
              path: /
              port: http
          readinessProbe:
            httpGet:
              path: /
              port: http

Проблема в том, что в рассматриваемом нами контейнере нет эндпоинта /, поэтому livenessProbe и readinessProbe никогда не пройдут. Что же делать и как дальше жить?! Самим добавлять этот энпоинт?..

На самом деле, добавлять ничего не нужно. Есть уже готовый проект Spring Boot Actuator, который сам добавит нужные эндпоинты для мониторинга работоспособности вашего приложения. Это не единственная его функция, он может ещё много чего. Подробности про этот проект можно почитать в документации.

Для подключения Spring Boot Actuator в проект нужно добавить следующую зависимость:

implementation 'org.springframework.boot:spring-boot-starter-actuator

После этого в приложении, задеплоенном в kubernetes, станут доступны эндпоинты /actuator/health/liveness и /actuator/health/readiness для livenessProbe и readinessProbe соответственно. При запуске вне среды kubernetes данные эндпоинты будут недоступны. Будет доступен только глобальный эндпоинт проверки состояния вашего приложения /actuator/health. В приложении, задеплоенном в kubernetes, этот эндпоинт так же будет доступен и будет показывать, по сути, совокупность данных обоих эндпоинтов /actuator/health/liveness и /actuator/health/readiness. Итоговая конфигурация для livenessProbe и readinessProbe будет выглядеть так:

          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: http
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: http

Зададим свои values для данного сабчарта. Сделать это можно как просто поместив их в values.yaml самого сабчарта, т.е., в данном случае, в list-keep-chart\charts\list-keep\values.yaml, так и задав их в values.yaml родительского чарта, т.е. в list-keep-chart\values.yaml. Во тором варианте values , на самом деле, будут переопределяться, т.е. можно задать какие-то values в сабчарте, и переопределить какие-то из них в родительском чарте (но, например, не все). Второй вариант считается best practice, поэтому выберем его. Чтобы указать, что values относятся к конкретному сабчарту, нужно их указать в разделе с именем данного сабчарта. В нашем случае раздел будет list-keep.

list-keep:
  image:
    repository: ghcr.io/vanbv/list-keep
    tag: latest
    pullPolicy: Always
  enabled: true
  service:
    port: 8082

Как может заметить внимательный читатель, помимо repository и tag, о которых я писал выше, я здесь ещё и изменил pullPolicy. В сгенерированном файле оно было IfNotPresent- стягивает докер-образ, если его нет в локальном registry. Я заменил его на Always - стягивает образ всегда из указанного repository. Сделал это, т.к. для проекта всегда собираю образ latest , и если не поменять, то стянется один раз и потом будет всегда браться из локального registry, что не совсем здесь подходит (не делайте так в продакшене!!!).

Что ж, с чартами для Spring Boot приложения мы закончили, давайте двинемся дальше.

Чарты для Vue.js приложения

Сабчарты для Vue.js приложения list-keep-front создаём аналогично тому, как мы это делали для Spring Boot приложения. В list-keep-chart\Chart.yaml добавялем:

  - name: list-keep-front
    condition: list-keep-front.enabled
    version: "*"

Здесь нам понадобиться указать докер образ и порт, как это делать мы уже знаем. Ещё нам требуется как-то обращаться к нашему приложению, т.е. нужен его адрес. Обычно для этого используется объект kubernetes - Ingress. Он позволяет сконфигурировать маршруты HTTP и HTTPS из внешней среды для объектов Service. Но в песочнице OpenShift Sandbox он не доступен. Вместо него предлагается использовать Route (наверное, можно этот объект назвать аналогом Ingress в OpenShift). Итак, используя документацию, а также пример, который генерится, если создать Route руками из веб-админки OpenShift, пример Ingress, который генерится при создании чартов, и капельку здравого смысла мне удалось написать вот такой list-keep-chart\charts\list-keep-front\templates\route.yaml:

{{- if .Values.route.enabled -}}
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: {{ include "list-keep-front.fullname" . }}
spec:
  host: {{ .Values.route.host }}
  port:
    targetPort: {{ .Values.service.port }}
  to:
    kind: Service
    name: {{ include "list-keep-front.fullname" . }}
{{- end }}

Давайте подробнее рассмотрим, что тут происходит:

  • {{- if .Values.route.enabled -}} - включает/выключает route в зависимости от route.enabled в values.yaml (аналогично тому, как сделано в ingress.yaml). Чтобы по умолчанию route был выключен, добавим в values.yaml сабчарта следующее:

    route:
      enabled: false
  • apiVersion: route.openshift.io/v1 - версия API Kubernetes, используемая для создания объекта Route.

  • kind: Route - тип объекта kubernetes.

  • metadata - данные, по которым можно идентифицировать объект. В данном случае это name: {{ include "list-keep-front.fullname" . }} название объекта (объект назван так же, как и другие объекты kubernetes в сгенерированном сабчарте).

  • spec - требуемая конфигурация объекта. host нужно будет указать в values.yaml. В качестве port будет браться тот, что указан для Service. В блоке to указывается, что данный route нужно применить к объекту с типом сервис - kind: Service и имя данного сервиса - name: {{ include "list-keep-front.fullname" . }}.

Итого в list-keep-chart\values.yaml получается следующее:

list-keep-front:
  image:
    repository: ghcr.io/vanbv/list-keep-front
    tag: v0.0.11
  enabled: true
  service:
    port: 8080
  route:
    enabled: true

Внимательный читатель может заметить, что я куда-то потерял route.host. В принципе его можно указать и здесь, но т.к. OpenShift Sandbox после пересоздания песочницы (напоминаю, это придётся сделать после 30 дневного периода) может сгенерировать новый адрес, то я решил для удобства вынести все такие адреса в переменные sh скрипта, который покажу в дальнейшем.

И ещё один нюанс. Данное приложение использует nginx. OpenShift Sandbox не позволит запуститься nginx под привилегированным пользователем. Поэтому требуется сконфигурировать nginx таким образом, чтобы он не запускался под привилегированным пользователем. В принципе, можно вообще воспользоваться уж готовым для таких нужд образом nginxinc/nginx-unprivileged. Пример можно посмотреть тут.

Чарты keycloak

Для полноценной работы приведённых выше приложений требуется keycloak. Если коротко, то это Authorization Server, если интересно подробнее, то welcome. Для сторонних приложений обычно уже используются готовые сторонние чарты. Найти их можно на Artifact Hub. Возьмём вот эти чарты от Bitnami. Добавить их в родительские чарты можно, как и сабчарты, написанные нами. В родительский Chart.yaml добавляем следующее:

  - name: keycloak
    repository: https://charts.bitnami.com/bitnami
    condition: keycloak.enabled
    version: 9.8.1

В данных чартах отсутствуют шаблоны для route, которые нам понадобятся, поэтому напишем их сами. Нам понадобится 2 routes. Один для http, второй для https. В принципе можно было бы обойтись каким-то одним, но с двумя будет проще. route с http нам нужен для общения Spring Boot приложения с keycloak, если использовать https, то потребуется валидный сертификат. Но с route http не получится зайти в админку keycloak, по умолчанию она доступна по https. В принципе, можно зайти в базу keycloak и проапдейтить там данные для того чтобы админка стала доступна по http, правда, для этого нужно разобраться что где хранится (но технически такое возможно, по крайней мере на момент написания статьи).

Давайте создадим 2 routes. Ранее мы создавали объекты kubernetes для своих чартов. Как же создать для чужих? Да, в общем-то, так же. Создаём сабчарт с именем стороннего чарта, в нашем случае это keycloak, и удаляем оттуда всё кроме Chart.yaml, и values.yaml, содержимое values.yaml нужно также удалить. Ну и теперь создаём в папке templates наши routes: route-http.yaml и route-https.yaml. Содержимое route-http.yaml будет совпадать с рассмотренным нами ранее содержимым route.yaml для list-keep-front, поэтому не будем на нём останавливаться. А вот содержимое route-https.yaml будет немного отличаться:

{{- if .Values.route.https.enabled -}}
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: {{ include "keycloak.fullname" . }}-https
spec:
  host: {{ .Values.route.https.host }}
  port:
    targetPort: https
  tls:
    termination: passthrough
  to:
    kind: Service
    name: {{ include "keycloak.fullname" . }}
{{- end }}

Здесь в блоке spec добавляется блок tls с termination: passthrough. Эта конфигурация указывает, что нужно, чтобы зашифрованный tls-трафик отправлялся прямо в наш сервис.

В values.yaml сабчарта добавим:

route:
  http:
    enabled: false
  https:
    enabled: false

Теперь идём в родительский values.yaml и добавим туда следующее:

keycloak:
  enabled: true
  podSecurityContext:
    enabled: false
  containerSecurityContext:
    enabled: false
  auth:
    tls:
      enabled: true
      autoGenerated: true
  postgresql:
    primary:
      podSecurityContext:
        enabled: false
      containerSecurityContext:
        enabled: false
  route:
    http:
      enabled: true
    https:
      enabled: true
  • Итак, здесь зачем-то отключены podSecurityContext и containerSecurityContext. Причина, в общем-то, та же, что описана для nginx. В чартах для podSecurityContext и containerSecurityContext описан запуск контейнеров/подов под привилегированным пользователем, но OpenShift Sandbox не позволит этого сделать, поэтому просто отключим podSecurityContext и containerSecurityContext.

  • auth.tls.enabled - включает TLS трафик, напоминаю, требуется для того чтобы попасть в админку. Если его включить, то потребуется соответствующий сертификат, но можно включить автогенерацию сертификата с помощью auth.tls.autoGenerated.

  • Для keycloak требуется база данных. По умолчанию с чартами от Bitnami используется PostgreSQL. Чтобы поды/контейнеры не запускались под привилегированным пользователем, необходимо отключить podSecurityContext и containerSecurityContex, аналогично тому, как это было сделано для самого keycloak.

С чартами keycloak в общем-то всё. Теперь нужно добавить адрес keycloak в наши приложения. В Spring Boot приложение можно добавить url напрямую в конфиг приложения, но тогда придётся пересобирать докер-образ при изменении url каждый раз, когда пересоздаётся песочница. Поэтому добавим url через переменные окружения с помощью чартов (аналогично тому, как мы добавили профиль). Конфигурация application-prod.yml будет выглядеть так:

server:
  port: 8082

keycloak:
  # Адрес keycloak берётся из соответствующей переменной окружения. Он должен
  # совпадать с тем адресом, который будет указан во фронте, т.к. в
  # Spring Boot Adapter есть проверка на то что адреса keycloak указанный здесь
  # совпадает с адресом из токена
  auth-server-url: ${KEYCLOAK_URL}
  realm: "list-keep"
  resource: "list-keep"
  bearer-only: true
  security-constraints:
    - authRoles:
        - uma_authorization
      securityCollections:
        - patterns:
            - /api/*
  # Выключаем ssl, чтобы обращаться в keycloak по http
  ssl-required: none

Адрес keycloak во Vue.js приложении для production среды указывается в файле .env.production. И в данном случае с помощью переменных окружения так просто добавить адрес keycloak не получится. Способ есть, но он показался мне не сильно тривиальным, подробнее о нём можно почитать тут. В принципе, можно просто создать этот файл в репозитории проекта, и каждый раз менять адрес в репозитории, коммитить и собирать новый образ. Но всё-таки можно немного упростить и, например, генерировать данный файл при сборке. В данном случае для сборки используются GitHub Actions, поэтому, для генерации .env.production можно добавить вот такой шаг:

      - name: Create .env.production
        run: |
          touch .env.production
          echo VUE_APP_KEYCLOAK_URL = ${{ secrets.VUE_APP_KEYCLOAK_URL }} >> .env.production

Адрес keycloak лежит в секретах и здесь он просто добавляется в файл. Очевидный минус такого подхода - придётся собирать образ каждый раз, когда пересоздаётся OpenShift Sandbox. Полный файл данного GitHub Action можно посмотреть в соответствующем репозитории.

Деплой

Для установки чартов необходимо выполнить helm install. Т.к. сама команда получилась достаточно длинной, я для удобства создал скрипт helm-install.sh , с помощью которого можно её выполнить:

#!/bin/bash

export KEYCLOAK_POSTGRESQL_ADMIN_PASSWORD=postgres
export KEYCLOAK_POSTGRESQL_PASSWORD=postgres
export KEYCLOAK_ADMIN_PASSWORD=user
export CLUSTER_URL=-your-namespace.openshiftapps.com

helm install list-keep ./ --set global.postgresql.auth.postgresPassword=$KEYCLOAK_POSTGRESQL_ADMIN_PASSWORD,global.postgresql.auth.password=$KEYCLOAK_POSTGRESQL_PASSWORD,keycloak.auth.adminPassword=$KEYCLOAK_ADMIN_PASSWORD,list-keep.env[0].name=SPRING_PROFILES_ACTIVE,list-keep.env[0].value=prod,list-keep.env[1].name=KEYCLOAK_URL,list-keep.env[1].value=http://list-keep-keycloak-http$CLUSTER_URL,list-keep-front.route.host=list-keep-front$CLUSTER_URL,keycloak.route.http.host=list-keep-keycloak-http$CLUSTER_URL,keycloak.route.https.host=list-keep-keycloak-https$CLUSTER_URL

Здесь выполняется команда helm install list-keep ./ , и с помощью --set ей задаются values, которые в силу соображений безопасности и удобства не были установлены в самих чартах:

  • global.postgresql.auth.postgresPassword - пароль пользователя-админа PostgreSQL.

  • global.postgresql.auth.password - пароль пользователя PostgreSQL БД keycloak.

  • keycloak.auth.adminPassword - пароль пользователя user от админки keycloak.

  • С помощью list-keep.env[1].name=KEYCLOAK_URL,list-keep.env[1].value=http://list-keep-keycloak-http$CLUSTER_URL задаётся переменная окружения с адресом keycloak для Spring Boot приложения. Также пришлось здесь с помощью list-keep.env[0].name=SPRING_PROFILES_ACTIVE,list-keep.env[0].value=prod задать переменную окружения для профиля, т.к. не смотря на то что данные values присутствуют в values.yaml, install чартов приводил к ошибке.

  • list-keep-front.route.host=list-keep-front$CLUSTER_URL - адрес route для Vue.js приложения.

  • keycloak.route.http.host=list-keep-keycloak-http$CLUSTER_URL - http route для keycloak.

  • keycloak.route.https.host=list-keep-keycloak-https$CLUSTER_URL - https route для keycloak.

После успешного выполнения скрипта, перейдя в раздел Topology в OpenShift Sandbox, вы увидите следующую картину:

В общем-то всё, осталось сконфигурировать keycloak, и можно будет приступить к эксплуатации данных приложений. Для настройки keycloak можно воспользоваться вот этой статьёй. Кроме того, в репозитории list-keep есть скрипт init.sh, который может помочь упросить данный процесс.

И ещё немного про keycloak
  • Важно, чтобы в настройках realm (в данном случае он называется list-keep), который используется для Spring Boot и Vue.js приложений, было указано Require SSL: none, иначе, если указать в приложениях http адрес, то авторизоваться не получится (при использовании init.sh для конфигурации realm используется файл realm-export.json, в котором это уже будет указано):

  • Важно: в настройках client (в данном случае он называется list-keep), который используется для Spring Boot и Vue.js приложений, нужно будет добавить значения Valid Redirect URIs и Web Origins, которые будут соответствовать адресу route приложения list-keep-front (при использовании init.sh для конфигурации realm используется файл realm-export.json, в котором можно будет добавить соответствующие значения в массивы redirectUris и webOrigins):

Проверим что приложения работают корректно. Для этого перейдём в list-keep-front. Сделать это можно прямо из раздела Topology:

Нас редиректит на страницу логина keycloak:

Можно, например, зайти через Google. Заходим, видим, что нам требуется подтвердить Email:

Подтверждаем и попадаем на страницу нашего Vue.js приложения list-keep-front. На ней выводится логин/почта, полученный из Spring Boot приложения list-keep. Что ж, все задеплоенные нами приложения корректно работают.

Заключение

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

Комментарии (0)