Что нужно знать про использование werf при развёртывании гибридного приложения: личный опыт

от автора

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/914060/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *