Привет! Меня зовут Максим Базуев, я мобильный разработчик. Последние 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.
Читайте также в нашем блоге: