Привет! Меня зовут Максим Базуев, я мобильный разработчик. Последние 6 лет занимаюсь разработкой гибридных мобильных приложений с использованием веб-технологий.

Сейчас я работаю над стартапом — мобильным приложением в тематике повышения продуктивности. Пока оно не в релизе. У меня маленькая команда, одна крошечная виртуалка на 1 ГБ RAM, 1 CPU и 20 ГБ диска (и там у меня Kubernetes). 

В этой статье я решил поделиться своим кейсом:

  • с каким стеком я работаю;

  • какую роль занимает бэкенд и какие сервисы висят на виртуалке;

  • как я познакомился с werf и почему выбрал его;

  • зачем использую werf, с какими проблемами столкнулся при развёртывании и чем помогла утилита.

Мой кейс будет полезен тем, кто ещё не знаком с werf — утилитой для доставки приложений в Kubernetes, — а также спецам из мира разработки, которые смотрят на K8s и ищут подходящие инструменты для работы с ним.

Стек разработки

Моё мобильное приложение — гибридное. Оно использует платформу CapacitorJS (для обёртки веба в мобильное приложение от экс-разработчиков Cordova) и поддерживает офлайн-режим как основную функциональность. При появлении подключения к интернету происходит отправка пакета мутаций на бэкенд, чтобы синхронизировать состояние между устройствами пользователя.

На стороне бэкенда реализовано решение tRPC, представляющее собой type-safe API уровня end-to-end, поскольку клиент и сервер разработаны на TypeScript и находятся в одном монорепозитории. Это позволяет мне обеспечить надёжность взаимодействия между компонентами и ускорить разработку. Для валидации и парсинга данных использую библиотеки Zod и SuperJSON. Они позволяют передавать различные типы данных между клиентом и сервером и не вспоминать, где какой тип в структуре данных, что экономит много времени.

Бэкенд в данной архитектуре выполняет роль синхронизации данных между клиентскими устройствами. У нас небольшой набор сервисов: приложение бэкенда и PostgreSQL, развёрнутый с использованием OpenEBS на компактной виртуальной машине с интеграцией Traefik Ingress. Благодаря OpenEBS получается минимальная задержка при работе с диском, что полностью меня устраивает. Ещё планирую добавить сервис для сбора логов с бэкенда, но пока не определился, какой именно инструмент выбрать, так как не сильно разбираюсь в этой области, смотрю в сторону Fluent Bit.

Внедрение в проект werf и мои ошибки

Примерно 2–3 года назад я начал изучать видеоролики по Kubernetes на YouTube.  Два месяца назад я решил установить оркестратор и понял, что ему нужно много ресурсов. Тогда я начал искать его реализацию на GitHub и наткнулся на k0s, который полностью совместим с Kubernetes и не требует огромного количества ресурсов. 

А ещё во время своего YouTube-исследования благодаря докладам компании «Флант» я познакомился с Open Source-решением werf. Я сразу установил утилиту, а затем развернул свой бэкенд вместе с базой данных, добавленной в зависимости чарта. В итоге werf используетсяу меня как для staging, так и для production.

В процессе внедрения я столкнулся с несколькими ошибками при деплойменте. Например, забыл добавить один из секретов в нужное пространство имён. Однако werf предоставил мне статус и логи работы в режиме реального времени. Утилита уведомила о возникшей 401-й ошибке и подсказала, что чего-то не хватает. Я погуглил и нашёл ответ: необходимо добавить секреты к образу. В итоге ошибки быстро исправились, и мой бэкенд с базой данных был готов к работе всего за несколько минут.

# Пример добавления секрета для Docker Registry (с которым у меня была ошибка).
kubectl create secret docker-registry NAME --docker-username=user --docker-password=password

Работа с секретами в werf

Важными факторами в выборе werf для меня стали возможность работы с секретами и их хранение в репозитории с помощью команды werf helm secret values edit .helm/secret-values.yaml. В дополнение к этой идее я установил SOPS от Mozilla, что решило проблемы с совместным использованием секретов между членами команды и CI-системой. Честно говоря, работа с секретами заслуживает отдельной статьи.

# Пример конфигурации SOPS.
creation_rules:
   - age: >-
       age1j25…

Команду werf converge используем для деплоя и развёртывания сервиса backend и admin (админ-панель):

# backend.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: backend
spec:
 replicas: {{ index $.Values.backend.replicas $.Values.werf.env  }}
 selector:
   matchLabels:
     app: backend
 template:
   metadata:
     labels:
       app: backend
     annotations:
       checksum/secretmap-app-envs: '{{ include (print $.Template.BasePath "/secret-app-envs.yaml") . | sha256sum }}' # Если поменяли секреты, то и под обновляет секреты.
   spec:
     imagePullSecrets:
       - name: {{ $.Release.Namespace }}-regcred
     containers:
     - image: {{ $.Values.werf.image.backend }}
       name: backend
       ports:
       - containerPort: 3000
       envFrom:
       - secretRef:
           name: app-envs

Для развёртывания мобильных приложений я использую систему Fastlane, и в этом процессе очень удобно работать с SOPS для управления секретами. В SOPS я храню все свои секреты, включая секретный ключ werf, который также шифруется, ключи от кластера Kubernetes (dev, prod), keystore для подписи Android-сборки, ключ App Store Connect API, аккаунт для доступа к облаку.

В процессе CI/CD я активно использую секреты werf, так как они необходимы для сборки моих продуктов. Их можно шифровать с помощью SOPS. У меня есть только один ключ SOPS — Age key, и этого достаточно для расшифровки секретов. А с помощью .sops.yaml можно указать права для конкретных ключей, например по Regex для /.*\.dev\..*/ указать ключи для разработчиков, а доступ к проду будет иметь ограниченный круг лиц. Если я не буду хранить секретный ключ werf в SOPS, то мне придётся следить за его сохранностью отдельно.

# Пример конфигурации SOPS с разграничением прав на dev и prod.
creation_rules:
   - path_regex: .*\.dev\..*
     age: >-
       age1j25…,
      age1r…
   - age: >-
       age1j25…
# Пример команды для шифрования секрета.
sops -e secrets/terraform.sa.key.dev.json > secrets/terraform.sa.key.dev.enc.json 

# Пример команды для расшифровки секрета.
sops -d secrets/terraform.sa.key.dev.enc.json > secrets/terraform.sa.key.dev.json 

# Пример sh-файлика для расшифровки секретов в папке.
#!/bin/sh
find . -name '*.enc.*' | xargs -n 1 -I '{}' echo sops -d {} \> {} | sed -E 's/\.enc//2' | bash

Гитерминизм

Мне особенно понравился гитерминизм в werf. Я активно использую его в коммуникации между бэкенд- и фронтенд-разработкой. Мобильное приложение, админ-панель и бэкенд живут в одном репозитории. Линтинг изменившихся ручек на бэкенде показывает сразу ошибки на мобильном приложении и админ-панели и не даст замержить их в основную ветку, пока ошибки не будут исправлены. Так я могу не беспокоиться об ошибках несовместимости бэкенд- и фронтенд-кода в окружении prod и staging.

# Пример конфигурации файла гитерминизма.
# https://werf.io/docs/v2/reference/werf_giterminism_yaml.html
giterminismConfigVersion: 1
config:
 goTemplateRendering:
   allowEnvVariables:
   - SENTRY_AUTH_TOKEN
   - VITE_APP_API_BASE

Вся инфраструктура хранится в Terraform-модулях внутри этого же репозитория, что позволяет отслеживать в Git состояние инфраструктуры проекта, включая информацию о том, в каком облаке что размещено. Такой подход, где всё представлено в виде кода (as a code), упрощает процесс адаптации для нового разработчика — достаточно выдать доступ по его Age-ключу к staging-окружению, и он сможет начать экспериментировать.

# Пример развертки кластера K0s в Terraform.

terraform {
 required_providers {
   k0s = {
     source  = "alessiodionisi/k0s"
     version = "0.2.2"
   }
 }
}

variable "vm_address" {
 type = string
}

variable "cluster_name" {
 type = string
}

variable "ssh_key_path" {
 type = string
}
variable "vm_user" {
 type    = string
 default = "bazumax"
}

variable "kubeconfig_path" {
 type = string
}


resource "k0s_cluster" "k0s" {
 name    = "main"
 version = "1.28.7+k0s.0"

 hosts = [
   {
     role      = "controller+worker"
     no_taints = true
     install_flags = [
       "--enable-k0s-cloud-provider=true",
       "--enable-cloud-provider=true"
     ]
     ssh = {
       address  = var.vm_address
       port     = 22
       user     = var.vm_user
       key_path = var.ssh_key_path
     }
   },
 ]

 config = file("${path.module}/k0s.config.yaml")
}

resource "local_file" "private_key" {
 content  = k0s_cluster.k0s.kubeconfig
 filename = var.kubeconfig_path

 depends_on = [k0s_cluster.k0s]
}

output "kubeconfig" {
 sensitive = true
 value     = k0s_cluster.k0s.kubeconfig
}

output "kubeconfig_file" {
 sensitive  = true
 value      = var.kubeconfig_path
 depends_on = [local_file.private_key]
}
# Пример развёртки собственного легкого registry для образов приложения. 
# https://artifacthub.io/packages/helm/twuni/docker-registry
# values.yaml - https://artifacthub.io/packages/helm/twuni/docker-registry?modal=values
resource "helm_release" "registry" {
 name = "twuni-registry"

 repository       = "https://helm.twun.io"
 chart            = "docker-registry"
 create_namespace = true

 values = [
   templatefile("${path.module}/registry/values.yaml",
     {
       htpasswd = "${var.registry.htpasswd}"
       host     = "registry.${var.domain}"
   })
 ]
}

Вместо заключения

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

Если у вас будут вопросы по гибридной разработке, можете написать мне в Telegram, постараюсь ответить и помочь.

P. S.

Читайте также в нашем блоге:

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