Вносить изменения в код приложения и тут же автоматически получать задеплоенные изменения, чтобы быстро тестировать его, — мечта разработчика. В этой статье мы посмотрим, как реализовать такой подход для небольшого приложения с фронтендом и бэкендом: организуем два варианта локального стенда на базе minikube или Docker с автоматическим развертыванием всех изменений или только закоммиченых в Git. Бэкенд приложения напишем на Go, а фронтенд — на Vue.js. Все это позволит быстро запускать проект для тестирования прямо во время разработки, что, несомненно, повысит удобство работы с приложением.
На старте вам понадобится установленная утилита werf (инструкция по установке есть на официальном сайте) — с ее помощью мы и будем максимально удобно организовывать автоматический деплой всех изменений. Всё остальное поставим в процессе.
В предыдущей статье мы рассмотрели, как собрать и развернуть приложение в кластере под управлением платформы Deckhouse с помощью утилиты werf. Сегодня в качестве следующего шага мы организуем локальный стенд для разработки, позволяющий с помощью werf настроить автоматическую сборку приложения на машине разработчика. Вам необязательно читать и выполнять инструкции из предыдущей статьи — мы всё построили так, чтобы эта статья была самодостаточной.
Разработка приложения
У нас уже есть шаблон небольшого приложения — это простой веб-сервис, в котором можно написать сообщение, сохранить его в базу данных и отобразить сохраненные ранее сообщения. Пока оно представляет из себя монолитный бэкенд на Go с подготовленными для деплоя в кластер Kubernetes Helm-чартами. В приложении реализована как логика работы с БД для сохранения и получения собщений, так и шаблонизация HTML-страниц и их отображение пользователю по соответствующим эндпоинтам — то есть нет жесткого разделения на бэкенд и фронтенд.
Далее мы немного доработаем приложение, разбив его на отдельные сервисы фронтенда и бэкенда: вынесем весь интерфейс во фронтенд, а в бэкенде оставим только возможность отвечать на REST-запросы. Кроме того, мы добавим чарты для новых сервисов и настроим запуск приложения в Docker Compose на локальной машине разработчика. Это позволит сделать наше приложение более интерактивным с точки зрения пользователя и более «сложным» для развертывания — ведь фронтенд и бэкенд теперь будут развертываться как отдельные сервисы, работающие параллельно и отвечающие за запросы по одному доменному имени. В итоге наша задача по развертыванию получится максимально приближенной к реальной жизни.
Реорганизация проекта
Создадим новый каталог, в котором будем разрабатывать приложение. Для начала скопируем содержимое оригинального проекта в подкаталог backend
нового проекта. В нем будет храниться только то, что отвечает за работу с БД и организацию REST API.
Фронтенд приложения расположится в подкаталоге frontend
корневого каталога, но создавать его пока не нужно: мы сделаем это на стадии подготовки нового приложения на Vue.js.
В итоге у приложения должна получиться следующая файловая структура:
$ tree -a .
.
└── backend
├── .helm
│ └── templates
│ ├── database.yaml
│ ├── deployment.yaml
│ ├── ingress.yaml
│ ├── job-db-setup-and-migrate.yaml
│ └── service.yaml
├── Dockerfile
├── cmd
│ └── main.go
├── db
│ └── migrations
│ ├── 000001_create_talkers_table.down.sql
│ └── 000001_create_talkers_table.up.sql
├── go.mod
├── go.sum
├── internal
│ ├── app
│ │ └── app.go
│ ├── common
│ │ └── json_logger_filter.go
│ ├── controllers
│ │ └── db_controllers.go
│ └── services
│ └── db_service.go
├── templates
│ ├── index.html
│ ├── remember.html
│ └── say.html
└── werf.yaml
Разработка бэкенда приложения
В нашем исходном приложении реализованы генерация страниц и передача их браузеру пользователя. В новой версии за веб-представление приложения будет отвечать отдельный сервис на Vue.js, поэтому давайте уберем из бэкенда все, что связано с обработкой шаблонов, а ответы будем передавать не в HTML, а в формате JSON — как взрослые программисты :)
Для начала удалим в файле internal/app/app.go
эндпоинт /
, отвечающий за главную страницу приложения, так как за него теперь будет отвечать фронтенд. К эндпоинотам remember
и say
, которые отвечают за сохранение и отображение сообщений из БД, добавим подпуть /api
(должно получиться вот так: remember/api
, say/api
), который пригодится для настройки распределения запросов по нужным путям при развертывании в кластере и Docker Compose в процессе организации локального стенда. После этого у нас получится следующий код функции инициализации сервера:
func Run() {
route := gin.New()
route.Use(gin.Recovery())
route.Use(common.JsonLogger())
route.GET("/api/remember", controllers.RememberController)
route.GET("/api/say", controllers.SayController)
err := route.Run()
if err != nil {
return
}
}
Примечание
Лучше заранее продумать, как будут группироваться эндпоинты приложения. Такая предусмотрительность позволит сразу организовать версионность и обратную совместимость API. Например, при изменении мажорной версии приложения все новые методы API будут доступны по пути /api/v2, а старые останутся на /api/v1.
Заменим в контроллерах тип ответа с HTML на JSON, оставив его содержимое в неизменном виде.
Ниже представлен контроллер сохранения данных, расположенный в файле internal/controllers/db_controllers.go
:
func RememberController(c *gin.Context) {
dbType, dbPath := services.GetDBCredentials()
db, err := sql.Open(dbType, dbPath)
if err != nil {
panic(err)
}
message := c.Query("message")
name := c.Query("name")
_, err = db.Exec("INSERT INTO talkers (message, name) VALUES (?, ?)",
message, name)
if err != nil {
panic(err)
}
c.JSON(http.StatusOK, gin.H{
"Name": name,
"Message": message,
})
defer db.Close()
}
В нем все осталось как прежде: получение данных от формы на сайте, переданных по GET-запросу, и их сохранение в БД. Изменен только ответ с вызова шаблона HTML на генерацию JSON (второй блок кода, если считать снизу, который теперь начинается с c.JSON
).
Код контроллера чтения данных из БД расположен в том же файле и выглядит теперь так:
func SayController(c *gin.Context) {
dbType, dbPath := services.GetDBCredentials()
db, err := sql.Open(dbType, dbPath)
if err != nil {
panic(err)
}
result, err := db.Query("SELECT * FROM talkers")
if err != nil {
panic(err)
}
count := 0
var data []map[string]string
for result.Next() {
count++
var id int
var message string
var name string
err = result.Scan(&id, &message, &name)
if err != nil {
panic(err)
}
data = append(data, map[string]string{
"Name": name,
"Message": message})
}
if count == 0 {
c.JSON(http.StatusOK, gin.H{
"Error": "There are no messages from talkers!",
})
} else {
c.JSON(http.StatusOK, gin.H{
"Messages": data,
})
}
}
Здесь все также осталось практически без изменений — мы лишь заменили HTML на JSON (два последних блока кода, теперь они начинаются с c.JSON
).
Ура! Отделение бэкенда завершено.
Можно проверить, что все работает, перейдя в терминале в каталог backend
и развернув приложение в кластере:
werf converge --repo <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/habr-app
После развертывания проверим, что по эндпоинту /say
данные возвращаются из БД в виде JSON-файла следующего содержания:
$ curl https://habrapp.example.com/say | jq
{
"Messages": [
{
"Message": "Привет, Хабр!",
"Name": "Zhbert"
}
]
}
Если вы разворачивали кластер с этим приложением по инструкции из предыдущей статьи этого цикла, то данные уже должны быть в базе — после тестирования первой версии приложения. В противном случае вам также вернется JSON, но с ошибкой, которая будет ругаться на отсутствие данных — это нормально, не пугайтесь.
Заметим, что, так как мы удалили из приложения генерацию главной страницы, при запросе на /
возвращается 404-я ошибка, как в примере ниже:
$ curl https://habrapp.example.com
404 page not found
В дальнейшем мы это исправим, так что не обращайте внимания.
Перенесем из каталога backend
файл werf.yaml
на уровень выше — в корневой каталог проекта, так как теперь он будет отвечать за сборку сразу двух контейнеров вместо одного.
Ну что ж, можно приступать к созданию фронтенда.
Разработка фронтенда приложения
В этой части мы разработаем веб-приложение на Vue.js, которое будет отвечать за фронтенд нашего проекта: отображать страницы, а также отправлять и получать данные от бэкенда.
Установите Vue CLI на вашу машину. Лучше сразу делать это через npm, так как в дальнейшем он нам еще понадобится.
Теперь инициализируем новое Vue-приложение в корневом каталоге нашего проекта, выполнив соответствующую команду:
vue create frontend
В ответ на предложение определиться с версией фреймворка выберем вариант по умолчанию:
> Default ([Vue 3] babel, eslint
Процесс подготовки займет некоторое время в зависимости от скорости интернет-соединения. В результате отобразятся статус созданного проекта и подсказка о том, как его запустить локально:
Vue CLI v5.0.8
✨ Creating project in /Users/zhbert/Programming/habr-werf-local-stend/frontend.
⚙️ Installing CLI plugins. This might take a while...
added 862 packages, and audited 863 packages in 24s
packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities