# goapp
**Repository Path**: czllfy/goapp
## Basic Information
- **Project Name**: goapp
- **Description**: 原始代码来自
https://github.com/romapres2010/goapp.git
- **Primary Language**: Unknown
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2024-04-26
- **Last Updated**: 2024-05-13
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# [Шаблон backend сервера на Golang — часть 3 (Docker, Docker Compose, Kubernetes (kustomize)](https://habr.com/ru/post/716634/)

[Первая часть](https://habr.com/ru/post/492062/) шаблона была посвящена HTTP серверу.
[Вторая часть](https://habr.com/ru/post/500554/) шаблона была посвящена прототипированию REST API.
[Третья часть](https://habr.com/ru/post/716634/) посвящена развертыванию шаблона в Docker, Docker Compose, Kubernetes (
kustomize).
Четвертая часть будет посвящена развертыванию в Kubernetes с Helm chart и настройке Horizontal Autoscaler.
[Пятая часть](https://habr.com/ru/post/720286/) посвящена оптимизации Worker pool и особенностям его работы в составе
микросервиса, развернутого в Kubernetes.
Для корректного развертывания в Kubernetes, в шаблон пришлось внести изменения:
- способа конфигурирования - YAML, ENV, [Kustomize](https://kustomize.io/)
- подхода к логированию - переход на [zap](https://github.com/uber-go/zap)
- способа развертывания схемы БД - переход на [liquibase](https://www.liquibase.org/)
- добавление метрик [prometheus](https://prometheus.io/)
Ссылка на новый [репозиторий](https://github.com/romapres2010/goapp).
Шаблон goapp в репозитории полностью готов к развертыванию в Docker, Docker Compose, Kubernetes (kustomize),
Kubernetes (helm).
_Настоящая статья не содержит детального описание используемых технологий_
## Содержание
1. Изменение подхода к конфигурированию
2. Добавление метрик [prometheus](https://prometheus.io/)
3. Изменение подхода к логированию
4. Развертывание схемы БД
5. Сборка Docker image
6. Сборка Docker-Compose
7. Схема развертывания в Kubernetes
8. Подготовка YAML для Kubernetes
9. Kustomization YAML для Kubernetes
10. Тестирование Kubernetes с kustomize
## 1. Изменение подхода к конфигурированию
Первоначальный вариант запуска приложения с передачей параметров через командную строку не очень удобен для Docker, так
как усложняет работу с ENTRYPOINT.
Но для совместимости этот вариант приходится поддерживать.
Идеальным способом конфигурирования для Docker и Kubernetes являются переменные окружения ENV. Их проще всего подменять
при переключении между средами DEV-TEST-PROD.
В нашем случае количество конфигурационных параметров приложения перевалило за 1000 - поэтому конфиг был разделен на
части:
- условно постоянная часть, которая меняется редко - вынесена в YAML,
- все настройки, связанные со средами порты, пути, БД - вынесены в ENV,
- чувствительные настройки безопасности (пользователи, пароли) - вынесены в отдельное зашифрованное хранилище и
передаются либо через ENV, либо через Secret.
### Условно постоянная часть конфига
Пример "базового" YAML
конфига [app.global.yaml](https://github.com/romapres2010/goapp/blob/master/deploy/config/app.global.yaml).
При использовании YAML конфигов в Kubernetes, нужно решить пару задач:
- как сформировать YAML конфиг в зависимости от среды на которой будет развернуто приложение (DEV-TEST-PROD)
- как внедрить YAML конфиг в Docker контейнер, развернутый в Pod Kubernetes
Самый простой вариант с формированием YAML конфиг - это держать "базовую" версию и делать от нее "ручные клоны" для
каждой из сред развертывания.
Самый простой вариант внедрения - это добавить YAML конфиги (всех сред) в сборку Docker образа и переключаться между
ними через ENV переменные, в зависимости от среды на которой осуществляется запуск.
Более правильный вариант - при сборке использовать Kustomize и overlays для формирования из базового, YAML конфиг для
конкретной среды и деплоить их в репозиторий артефактов, откуда он потом будет выложен в примонтированный каталог к
Docker контейнер.
### Динамическое конфигурирование REST API entry point
Столкнулись с тем, что в разных средах нужно по-разному настраивать REST API entry point.
Можно было возложить эту функцию на nginx proxy, но решили задачу через YAML конфиг.
``` yaml
handlers:
HealthHandler: # Сервис health - проверка активности HEALTH
enabled: true # Признак включен ли сервис
application: "app" # Приложение к которому относится сервис
module: "system" # Модуль к которому относится сервис
service: "health" # Имя сервиса
version: "v1.0.0" # Версия сервиса
full_path: "/app/system/health" # URI сервиса /Application.Module.Service.APIVersion или /Application/APIVersion/Module/Service
params: "" # Параметры сервиса с виде {id:[0-9]+}
method: "GET" # HTTP метод: GET, POST, ...
handler_name: "HealthHandler" # Имя функции обработчика
ReadyHandler: # Сервис ready - handle to test readinessProbe
enabled: true # Признак включен ли сервис
application: "app" # Приложение к которому относится сервис
module: "system" # Модуль к которому относится сервис
service: "ready" # Имя сервиса
version: "v1.0.0" # Версия сервиса
full_path: "/app/system/ready" # URI сервиса /Application.Module.Service.APIVersion или /Application/APIVersion/Module/Service
params: "" # Параметры сервиса с виде {id:[0-9]+}
method: "GET" # HTTP метод: GET, POST, ...
handler_name: "ReadyHandler" # Имя функции обработчика
```
Назначение функции обработчика для REST API entry point
выполнено [с использованием reflect](https://github.com/romapres2010/goapp/blob/master/pkg/common/httpservice/httpservice.go#L193).
Весьма удобный побочный эффект такого подхода - возможность переключать альтернативные обработчики без пересборки кода,
только меняя конфиг.
``` go
var dummyHandlerFunc func(http.ResponseWriter, *http.Request)
for handlerName, handlerCfg := range s.cfg.Handlers {
if handlerCfg.Enabled { // сервис включен
handler := Handler{}
if handlerCfg.Params != "" {
handler.Path = handlerCfg.FullPath + "/" + handlerCfg.Params
} else {
handler.Path = handlerCfg.FullPath
}
handler.Method = handlerCfg.Method
// 确定处理器的方法
method := reflect.ValueOf(s.httpHandler).MethodByName(handlerCfg.HandlerName)
// 方法已找到
if method.IsValid() {
methodInterface := method.Interface() // получил метод в виде интерфейса, для дальнейшего преобразования к нужному типу
handlerFunc, ok := methodInterface.(func(http.ResponseWriter, *http.Request)) // преобразуем к нужному типу
if ok {
handler.HandlerFunc = s.recoverWrap(handlerFunc) // Оборачиваем от паники
_log.Info("Register HTTP handler: HandlerName, Method, FullPath", handlerCfg.HandlerName, handlerCfg.Method, handlerCfg.FullPath)
} else {
return _err.NewTyped(_err.ERR_INCORRECT_TYPE_ERROR, requestID, "New", "func(http.ResponseWriter, *http.Request)", reflect.ValueOf(methodInterface).Type().String(), reflect.ValueOf(dummyHandlerFunc).Type().String()).PrintfError()
}
} else {
return _err.NewTyped(_err.ERR_HTTP_HANDLER_METHOD_NOT_FOUND, requestID, handlerCfg.HandlerName).PrintfError()
}
s.Handlers[handlerName] = handler
}
}
```
## 2 Добавление метрик [prometheus](https://prometheus.io/)
В шаблон встроена сборка следующих метрик для [prometheus](https://prometheus.io/):
- Метрики DB
- **db_total** - The total number of processed DB by sql statement (CounterVec)
- **db_duration_ms** - The duration histogram of DB operation in ms by sql statement (HistogramVec)
- Метрики HTTP request
- **http_requests_total_by_resource** - How many HTTP requests processed, partitioned by resource (CounterVec)
- **http_requests_error_total_by_resource** - How many HTTP requests was ERRORED, partitioned by resource (
CounterVec)
- **http_request_duration_ms_by_resource** - The duration histogram of HTTP requests in ms by resource (
HistogramVec)
- **http_active_requests_count** - The total number of active HTTP requests (Gauge)
- **http_request_duration_ms** - The duration histogram of HTTP requests in ms (Histogram)
- Метрики HTTP client call
- **http_client_call_total_by_resource** - How many HTTP client call processed, partitioned by resource (CounterVec)
- **http_client_call_duration_ms_by_resource** - The duration histogram of HTTP client call in ms by resource (
HistogramVec)
- Метрики WorkerPool
- **wp_task_queue_buffer_len_vec** - The len of the worker pool buffer (GaugeVec)
- **wp_add_task_wait_count_vec** - The number of the task waiting to add to worker pool queue (GaugeVec)
- **wp_worker_process_count_vec** - The number of the working worker (GaugeVec)
Доступ к метрикам настроен по стандартному пути HTTP GET [/metrics](http://127.0.0.1:3000/metrics).
Включение / выключение метрик настраивается через YAML конфиг
``` yaml
# конфигурация сбора метрик
metrics:
metrics_namespace: com
metrics_subsystem: go_app
collect_db_count_vec: true
collect_db_duration_vec: false
collect_http_requests_count_vec: true
collect_http_error_requests_count_vec: true
collect_http_requests_duration_vec: true
collect_http_active_requests_count: true
collect_http_requests_duration: true
collect_http_client_call_count_vec: true
collect_http_client_call_duration_vec: true
collect_wp_task_queue_buffer_len_vec: true
collect_wp_add_task_wait_count_vec: true
collect_wp_worker_process_count_vec: true
```
## 3. Изменение подхода к логированию
Логирование приложения в Kubernetes можно реализовать несколькими способами:
- вывод в stdout, stderr - этот способ является стандартным, но нужно учитывать, что в Kubernetes stdout, stderr Docker
контейнеров перенаправляются в файлы, которые имеют ограниченный размер и могут перезаписываться.
- вывод в файл на примонтированный к Docker контейнеру внешний ресурс.
- вывод в структурном виде во внешний сервис, например, Kafka.
Всем этим требованиям отлично удовлетворяет библиотека [zap](https://github.com/uber-go/zap). Она также позволяет
настроить несколько параллельных логгеров (ZapCore), которые будут писать в различные приемники.
В пакете [logger](https://github.com/romapres2010/goapp/tree/master/pkg/common/logger) реализована возможность настройки
нескольких ZapCore через простой YAML конфиг.
Так же в пакете [logger](https://github.com/romapres2010/goapp/tree/master/pkg/common/logger) добавлена
интеграция [zap](https://github.com/uber-go/zap) с библиотекой [lumberjack](https://gopkg.in/natefinch/lumberjack.v2)
для управления циклом ротации и архивирования лог файлов.
Любой из ZapCore можно динамически включать/выключать или изменять через HTTP
POST: [/system/config/logger](https://github.com/romapres2010/goapp/blob/master/pkg/common/httpservice/handler_logger_set_config.go).
Например, в следующем конфиге объявлены 4 ZapCore.
- все сообщения от DEBUG до INFO выводятся в файл app.debug.log
- максимальный размер лог файла в 10 MB
- время хранения истории лог файлов 7 дней
- максимальное количество архивных логов - 10 без архивирования
- все сообщения от ERROR до FATAL выводятся в файл app.error.log
- все сообщения выводятся в stdout
- все сообщения от ERROR до FATAL выводятся в stderr
``` yaml
# конфигурация сервиса логирования
logger:
enable: true # состояние логирования 'true', 'false'
global_level: INFO # debug, info, warn, error, dpanic, panic, fatal - все логгеры ниже этого уровня будут отключены
global_filename: /app/log/app.log # глобальное имя файл для логирования
zap:
enable: true # состояние логирования 'true', 'false'
disable_caller: false # запретить вывод в лог информации о caller
disable_stacktrace: false # запретить вывод stacktrace
development: false # режим разработки для уровня dpanic
stacktrace_level: error # для какого уровня выводить stacktrace debug, info, warn, error, dpanic, panic, fatal
core:
- enable: true # состояние логирования 'true', 'false'
min_level: null # минимальный уровень логирования debug, info, warn, error, dpanic, panic, fatal
max_level: INFO # максимальный уровень логирования debug, info, warn, error, dpanic, panic, fatal
log_to: lumberjack # логировать в 'file', 'stderr', 'stdout', 'url', 'lumberjack'
encoding: "console" # формат вывода 'console', 'json'
file:
filename: ".debug.log" # имя файл для логирования, если не заполнено, то используется глобальное имя
max_size: 10 # максимальный размер лог файла в MB
max_age: 7 # время хранения истории лог файлов в днях
max_backups: 10 # максимальное количество архивных логов
local_time: true # использовать локальное время в имени архивных лог файлов
compress: false # сжимать архивные лог файлы в zip архив
- enable: true # состояние логирования 'true', 'false'
min_level: ERROR # минимальный уровень логирования debug, info, warn, error, dpanic, panic, fatal
max_level: null # максимальный уровень логирования debug, info, warn, error, dpanic, panic, fatal
log_to: lumberjack # логировать в 'file', 'stderr', 'stdout', 'url', 'lumberjack
encoding: "console" # формат вывода 'console', 'json'
file:
filename: ".error.log" # имя файл для логирования, если не заполнено, то используется глобальное имя
max_size: 10 # максимальный размер лог файла в MB
max_age: 7 # время хранения истории лог файлов в днях
max_backups: 10 # максимальное количество архивных логов
local_time: true # использовать локальное время в имени архивных лог файлов
compress: false # сжимать архивные лог файлы в zip архив
- enable: true # состояние логирования 'true', 'false'
min_level: null # минимальный уровень логирования debug, info, warn, error, dpanic, panic, fatal
max_level: null # максимальный уровень логирования debug, info, warn, error, dpanic, panic, fatal
log_to: stdout # логировать в 'file', 'stderr', 'stdout', 'url', 'lumberjack
encoding: "console" # формат вывода 'console', 'json'
- enable: true # состояние логирования 'true', 'false'
min_level: ERROR # минимальный уровень логирования debug, info, warn, error, dpanic, panic, fatal
max_level: null # максимальный уровень логирования debug, info, warn, error, dpanic, panic, fatal
log_to: stderr # логировать в 'file', 'stderr', 'stdout', 'url', 'lumberjack
encoding: "console" # формат вывода 'console', 'json'
```
## 4. Развертывание схемы БД
Для первоначального развертывания схемы БД в контейнере и управления изменениями DDL скриптов через Git великолепно
подходит [liquibase](https://www.liquibase.org/).
[Liquibase](https://www.liquibase.org/) позволяет самостоятельно генерировать DDL скрипты БД. Для простых случаев -
этого вполне хватает, но если требуется тонкая настройка БД (например,настройка партиций, Blob storage) то лучше
формировать DDL скрипты в отдельном приложении.
Для Postgres отлично подходит TOAD DataModeler. Для Oracle - Oracle SQL Developer Data Modeler. Например, TOAD
DataModeler умеет корректно генерировать DDL скрипты для добавления not null столбцов в уже заполненную таблицу.
Весьма полезная функция [Liquibase](https://www.liquibase.org/) - возможность задавать скрипты для отката изменений.
Если установка патча на БД прошла неуспешно (сбой добавления столбца в таблицу, нарушение PK, FK), то легко откатиться к
предыдущему состоянию.
В шаблон включены
примеры [liquibase/changelog](https://github.com/romapres2010/goapp/tree/master/db/liquibase/changelog)
и [DDL скрипты](https://github.com/romapres2010/goapp/tree/master/db/sql) для таблиц Country и Currency с минимальным
тестовым наполнением.
Так же как и с YAML конфигами, необходимо предусмотреть способ внедрения [Liquibase](https://www.liquibase.org/)
Changelog в Docker контейнер Liquibase, развернутый в Job Pod Kubernetes:
- скрипты можно встраивать в Liquibase контейнер (не очень хороший вариант, как минимум размер Docker с Liquibase более
300 Мбайт),
- скрипты можно выкладывать в примонтированный каталог к Docker контейнер и на уровне ENV переменных указывать Changelog
для запуска.
В шаблоне предусмотрены оба эти варианта.
## 5. Сборка Docker image
Docker файлы для сборки выложены в
каталоге [deploy/docker](https://github.com/romapres2010/goapp/tree/master/deploy/docker).
Для тестирования сборки можно использовать Docker Desktop - в этом случае не нужно делать push Docker image - они все
кешируются на локальной машине.
### Сборка Docker image для Go Api
Сборка Docker image для Go
Api [app-api.Dockerfile](https://github.com/romapres2010/goapp/blob/master/deploy/docker/app-api.Dockerfile) имеет
следующие особенности:
- git commit, время сборки и базовая версия приложения передаются через аргументы docker
- создаются точки монтирования для внешних и преднастроенных YAML конфигов и log файлов
- сборка ведется только на локальной копии внешних библиотек ./vendor
- преднастроенные YAML конфиги для различных сред DEV-TEST-PROD можно встроить в сборку и переключаться через ENV
переменные
- git commit, время сборки и базовая версия приложения встраиваются в пакет main
- для финальной сборки используется минимальный distroless образ
- точка запуска приложения не содержит параметров - все передается через ENV переменные
``` docker
##
## Build stage
##
FROM golang:1.19-buster AS build
# git commit, время сборки и базовая версия приложения передаются через аргументы docker
ARG APP_COMMIT
ARG APP_BUILD_TIME
ARG APP_VERSION
WORKDIR /app
# создаются точки монтирования для внешних и преднастроенных YAML конфигов и log файлов
RUN mkdir ./run && mkdir ./run/defcfg && mkdir ./run/log && mkdir ./run/cfg
COPY ./go.mod ./go.mod
COPY ./go.sum ./go.sum
COPY ./pkg ./pkg
COPY ./cmd/app/main.go ./cmd/app/main.go
# сборка ведется только на локальной копии внешних библиотек ./vendor
COPY ./vendor ./vendor
# преднастроенные YAML конфиги для различных сред DEV-TEST-PROD можно встроить в сборку и переключаться через ENV переменные
COPY ./deploy/config/. ./run/defcfg/
# git commit, время сборки и базовая версия приложения встраиваются в пакет main
RUN go build -v -mod vendor -ldflags "-X main.commit=${APP_COMMIT} -X main.buildTime=${APP_BUILD_TIME} -X main.version=${APP_VERSION}" -o ./run/main ./cmd/app/main.go
RUN echo "Based on commit: $APP_COMMIT" && echo "Build Time: $APP_BUILD_TIME" && echo "Version: $APP_VERSION"
##
## Deploy stage
##
FROM gcr.io/distroless/base-debian10
WORKDIR /app
COPY --from=build /app/run/. .
EXPOSE 8080/tcp
# точка запуска приложения не содержит параметров - все передается через ENV переменные
ENTRYPOINT [ "/app/main"]
```
Для ручной сборки Docker выложен тестовый shell
скрипт [app-api-build.sh](https://github.com/romapres2010/goapp/blob/master/deploy/docker/app-api-build.sh):
- сборка и запуск всех скриптов всегда осуществляется от корня репозитория
- логирование вывода всех скриптов идет в файлы
- управление версией, именем приложения и docker tag представлено в упрощенном виде через локальные файлы в
каталоге [deploy](https://github.com/romapres2010/goapp/tree/master/deploy)
- базовая версия приложения ведется в текстовом
файле [version](https://github.com/romapres2010/goapp/blob/master/deploy/version). Для CI/CD через github action
дополнительно используется сквозная нумерация, которая добавляется к базовой версии.
- имя приложения ведется в текстовом
файле [app_api_app_name](https://github.com/romapres2010/goapp/blob/master/deploy/app_api_app_name)ведется в
текстовом файле [app_api_app_name](https://github.com/romapres2010/goapp/blob/master/deploy/app_api_app_name)
- имя репозитория для публикации ведется в текстовом
файле [default_repository](https://github.com/romapres2010/goapp/blob/master/deploy/default_repository)
``` shell
echo "Current directory:"
pwd
export LOG_FILE=$(pwd)/app-api-build.log
echo "Go to working directory:"
pushd ./../../
export APP_VERSION=$(cat ./deploy/version)
export APP_API_APP_NAME=$(cat ./deploy/app_api_app_name)
export APP_REPOSITORY=$(cat ./deploy/default_repository)
export APP_BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S')
export APP_COMMIT=$(git rev-parse --short HEAD)
docker build --build-arg APP_VERSION=$APP_VERSION --build-arg APP_BUILD_TIME=$APP_BUILD_TIME --build-arg APP_COMMIT=$APP_COMMIT -t $APP_REPOSITORY/$APP_API_APP_NAME:$APP_VERSION -f ./deploy/docker/app-api.Dockerfile . 1>$LOG_FILE 2>&1
echo "Go to current directory:"
popd
```
### Сборка Docker image для liquibase
Сборка Docker image для
Liquibase [liquibase.Dockerfile](https://github.com/romapres2010/goapp/blob/master/deploy/docker/app-liquibase.Dockerfile)
имеет следующие особенности:
- Changelog для различных сред DEV-TEST-PROD можно встроить в сборку и переключаться через ENV переменные
- DDL и DML скрипты можно встроить в сборку либо использовать внешнюю точку монтирования
``` dockerfile
##
## Deploy stage
##
FROM liquibase/liquibase:4.9.1
# Changelog для различных сред DEV-TEST-PROD можно встроить в сборку и переключаться через ENV переменные
COPY ./db/liquibase/changelog/. /liquibase/changelog/
# DDL и DML скрипты можно встроить в сборку
COPY ./db/sql /sql
```
## 6. Сборка Docker Compose
Для локальной сборки удобно использовать Docker Compose. Скрипт
сборки [compose-app.yaml](https://github.com/romapres2010/goapp/blob/master/deploy/compose/compose-app.yaml).
В
каталоге [/deploy/run/local.compose.global.config](https://github.com/romapres2010/goapp/tree/master/deploy/run/local.compose.global.config)
выложены скрипты для локального тестирования с использованием Docker Compose.
- все ENV переменные приложений передаются через
внешний [.env файл](https://github.com/romapres2010/goapp/blob/master/deploy/run/local.compose.global.config/.env)
- каждый компонент системы (API, БД, UI) можно поднимать независимо, например:
- [/deploy/run/local.compose.global.config/compose-app-api-db-reload-up.sh](https://github.com/romapres2010/goapp/blob/master/deploy/run/local.compose.global.config/compose-app-api-db-reload-up.sh) -
полностью чистит БД, и пересобирает API
- [/deploy/run/local.compose.global.config/compose-app-ui-up.sh](https://github.com/romapres2010/goapp/blob/master/deploy/run/local.compose.global.config/compose-app-ui-up.sh) -
переустанавливает только UI
### Docker Compose для Go Api
Особенности:
- при каждом запуске выполняется build
- аргументы в Dockerfile (APP_COMMIT, APP_BUILD_TIME, APP_VERSION) пробрасываются через ENV переменные окружения
- ENV переменные приложения берутся из одноименных ENV переменных окружения (дефолтные значения указаны для примера)
- в Docker монтируются volumеs для конфиг и логов (/app/cfg:ro, /app/log:rw)
- зависимости (depends_on) от БД не выставляются - вместо этого приложение написано так, чтобы оно могло
перестартовывать без последствий и указано restart_policy: on-failure
- настроен healthcheck на GET: /app/system/health
``` dockerfile
app-api:
build:
context: ./../../
dockerfile: ./deploy/docker/app-api.Dockerfile
tags:
- $APP_REPOSITORY/$APP_API_APP_NAME:$APP_VERSION
args:
- APP_COMMIT=${APP_COMMIT:-unset}
- APP_BUILD_TIME=${APP_BUILD_TIME:-unset}
- APP_VERSION=${APP_VERSION:-unset}
container_name: app-api
hostname: app-api-host
networks:
- app-net
ports:
- $APP_HTTP_OUT_PORT:$APP_HTTP_PORT
- 8001:$APP_HTTP_PORT
environment:
- TZ="Europe/Moscow"
- APP_CONFIG_FILE=${APP_CONFIG_FILE:-/app/defcfg/app.global.yaml}
- APP_HTTP_LISTEN_SPEC=${APP_HTTP_LISTEN_SPEC:-0.0.0.0:8080}
- APP_LOG_LEVEL=${APP_LOG_LEVEL:-ERROR}
- APP_LOG_FILE=${APP_LOG_FILE:-/app/log/app.log}
- APP_PG_USER=${APP_PG_USER:-postgres}
- APP_PG_PASS=${APP_PG_PASS:?database password not set}
- APP_PG_HOST=${APP_PG_HOST:-app-db-host}
- APP_PG_PORT=${APP_PG_PORT:-5432}
- APP_PG_DBNAME=${APP_PG_DBNAME:-postgres}
volumes:
- "./../../../app_volumes/cfg:/app/cfg:ro"
- "./../../../app_volumes/log:/app/log:rw"
deploy:
restart_policy:
condition: on-failure
healthcheck:
test: ["curl -f 0.0.0.0:8080/app/system/health"]
interval: 10s
timeout: 5s
retries: 5
```
### Docker Compose для liquibase
Особенности:
- при необходимости тестирования сборки можно примонтировать дополнительный Volume для логов (/liquibase/mylog:rw)
- устанавливается зависимость от БД (depends_on condition: service_healthy)
- при необходимости в Docker монтируются volumеs для changelog и sql (/liquibase/sql:rw, /liquibase/changelog:rw)
- changelog для запуска передается через ENV переменные
``` dockerfile
app-liquibase:
build:
context: ./../../
dockerfile: ./deploy/docker/app-liquibase.Dockerfile
tags:
- $APP_REPOSITORY/$APP_LUQUIBASE_APP_NAME:$APP_VERSION
container_name: app-liquibase
depends_on:
app-db:
condition: service_healthy
networks:
- app-net
# volumes:
# - "./../../../app_volumes/log:/liquibase/mylog:rw"
# - "./../../../app_volumes/sql:/liquibase/sql:rw"
# - "./../../../app_volumes/log:/liquibase/changelog:rw"
# command: --changelog-file=./changelog/$APP_PG_CHANGELOG --url="jdbc:postgresql://$APP_PG_HOST:$APP_PG_PORT/$APP_PG_DBNAME" --username=$APP_PG_USER --password=$APP_PG_PASS --logFile="mylog/liquibase.log" --logLevel=info update
command: --changelog-file=./changelog/$APP_PG_CHANGELOG --url="jdbc:postgresql://$APP_PG_HOST:$APP_PG_PORT/$APP_PG_DBNAME" --username=$APP_PG_USER --password=$APP_PG_PASS --logLevel=info update
```
### Docker Compose для БД
Особенности:
- в Docker монтируются volumе для БД (/var/lib/postgresql/data:rw)
- настроен healthcheck на "CMD-SHELL", "pg_isready"
``` dockerfile
app-db:
image: postgres:14.5-alpine
container_name: app-db
hostname: app-db-host
environment:
- POSTGRES_PASSWORD=${APP_PG_PASS:?database password not set}
- PGUSER=${APP_PG_USER:?database user not set}
networks:
- app-net
ports:
- $APP_PG_OUT_PORT:$APP_PG_PORT
volumes:
- "./../../../app_volumes/db:/var/lib/postgresql/data:rw"
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: unless-stopped
```
## 7. Схема развертывания в Kubernetes
Схема развертывания в [Kubernetes](https://kubernetes.io/ru/docs/tutorials/kubernetes-basics/) приведена в сокращенном
виде, исключены компоненты, связанные с UI (Jmix) и распределенным кэшем (hazelcast).

Основным элементом развертывания в Kubernetes является [Pod](https://kubernetes.io/docs/concepts/workloads/pods/).
Для простоты - это группа контейнеров с общими ресурсами, которая изолирована от остальных Pod.
- Обычно, в Pod размещается один app контейнер, и, при необходимости, init контейнер.
- Остановить app контейнер в Pod нельзя, либо он завершит работу сам, либо нужно удалить Pod (руками или через
Deployment уменьшив количество replicas дo 0).
- Pod размещается на определенном узле кластера Kubernetes
- Для Pod задается политика автоматического рестарта в рамках одного
Node [restartPolicy](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#restart-policy): Always,
OnFailure, Never.
- Остановкой и запуском Pod управляет Kubernetes. В общем случае, Pod может быть автоматически остановлен если:
- все контейнеры в Pod завершили работу (успешно или ошибочно)
- нарушены liveness probe для контейнера
- нарушены limits.memory для контейнера
- Pod больше не нужен, например, если уменьшилась нагрузка и Horizontal Autoscaler уменьшил количество replicas
- При [остановке Pod](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination):
- в контейнеры будет отправлен STOPSIGNAL
- по прошествии terminationGracePeriodSeconds процессы контейнеров будут удалены
- при удалении Pod и следующем создании, он может быть создан на другом узле кластера, с другими IP адресами и
портами и к нему могут быть примонтированы новые Volume
При разработке stateless приложений для Kubernetes нужно учитывать эту специфику:
- приложение может быть остановлено в любой момент
- необходимо предусмотреть возможность "чистой" остановки в течении grace period (закрыть подключения к БД,
завершить (или нет) текущие задачи, закрыть HTTP соединения, ...)
- необходимо предусмотреть возможность "жесткой" остановки, если приложение взаимодействие со stateful сервисами, то
предусмотреть компенсирующие воздействия при повторном запуске
- желательно контролировать лимит по памяти для приложения
- при запуске/перезапуске приложения все данные в локальной файловой системе контейнера потенциально могут быть потеряны
- при запуске/перезапуске приложения данные на примонтированных Volume потенциально могут быть так же потеряны
- при остановке приложения доступ к stdout и stderr получить можно, но есть ограничения - критические логи желательно
сразу отправлять во внешний сервис или на Volume, который точно не будет удален
- в Kubernetes нет возможности явно задать последовательность старта различных Pod
- приложение может потенциально запущено раньше других связанных сервисов
- желательно предусмотреть вариант постоянного (или лимитированного по количеству раз) перезапуска приложения в
ожидании готовности внешних сервисов
- желательно предусмотреть в связанных приложениях liveness/readiness probe, чтобы понять, когда связанное
приложение готово к работе
## 8. Подготовка YAML для Kubernetes
### Конфигурирование
В Kubernetes предусмотрено для основных способа
конфигурирования [ConfigMaps](https://kubernetes.io/docs/concepts/configuration/configmap/) для основных настроек
и [Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) для настроек, чувствительных с точки зрения
безопасности.
В простейшем
случае [/deploy/kubernates/base/app-configmap.yaml](https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/base/app-configmap.yaml),
содержит переменные со строковыми значениями. В более сложных вариантах, можно считывать и разбирать конфиги из внешнего
файла или использовать внешний файл без "разбора".
Все артефакты Kubernetes желательно сразу создавать в отдельном
namespace [/deploy/kubernates/base/app-namespase.yaml](https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/base/app-namespase.yaml).
Для каждого артефакта нужно указывать метки для дальнейшего поиска и фильтрации. В примере приведена одна метка с
именем 'app' и значением 'app'.
``` yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
labels:
app: app
namespace: go-app
data:
APP_CONFIG_FILE: /app/defcfg/app.global.yaml
APP_HTTP_PORT: "8080"
APP_HTTP_LISTEN_SPEC: 0.0.0.0:8080
APP_LOG_LEVEL: ERROR
APP_LOG_FILE: /app/log/app.log
APP_PG_HOST: dev-app-db
APP_PG_PORT: "5432"
APP_PG_DBNAME: postgres
APP_PG_CHANGELOG: db.changelog-1.0_recreate_testdatamdg.xml
```
"Секретные" конфигурационные данные определяются
в [/deploy/kubernates/base/app-secret.yaml](https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/base/app-secret.yaml).
Данный вариант максимально упрощенный - правильно использовать внешнее зашифрованное хранилище.
``` yaml
apiVersion: v1
kind: Secret
metadata:
name: app-secret
labels:
app: app
namespace: go-app
type: Opaque
stringData:
APP_PG_USER: postgres
APP_PG_PASS: postgres
```
### БД
Развертывание БД в шаблоне приведено в упрощенном stateless варианте (может быть использовано только для dev).
Правильный вариант для промышленной эксплуатации - развертывание через оператор с поддержкой кластеризации,
например, [zalando](https://github.com/zalando/postgres-operator).
Прежде всего, нужно запросить ресурсы
для [Persistent Volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) на котором будет располагаться
файлы с БД.
Если PersistentVolumeClaim удаляется, то выделенный Persistent Volumes в зависимости от persistentVolumeReclaimPolicy,
будет удален, очищен или сохранен.
_Хорошая статья с
описанием [хранилищ данных (Persistent Volumes) в Kubernetes](https://serveradmin.ru/hranilishha-dannyh-persistent-volumes-v-kubernetes/)_
``` yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-db-claim
labels:
app: app
namespace: go-app
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Mi
```
Управлять созданием Pod удобнее
через [Deployments](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/), который задает шаблон по
которому будут создаваться Pod.
Особенности
развертывания [/deploy/kubernates/base/app-db-deployment.yaml](https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/base/app-db-deployment.yaml):
- количество replicas: 1 - создавать несколько экземпляров БД таким образом можно, но физически у каждой БД будут свои
файлы.
- template.metadata.labels задана дополнительная метка tier: app-db, чтобы можно было легко найти Pod БД
- ENV переменные заполняются из ранее созданных ConfigMap и Secret
- определены readinessProbe и livenessProbe
- resources.limits для БД не указываются
- к /var/lib/postgresql/data примонтирован volume, полученный через ранее определенный persistentVolumeClaim.claimName:
app-db-claim
``` yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-db
labels:
tier: app-db
namespace: go-app
spec:
replicas: 1
selector:
matchLabels:
tier: app-db
strategy:
type: Recreate
template:
metadata:
labels:
app_net: "true"
tier: app-db
spec:
containers:
- name: app-db
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: app-secret
key: APP_PG_PASS
- name: POSTGRES_DB
valueFrom:
configMapKeyRef:
name: app-config
key: APP_PG_DBNAME
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: app-secret
key: APP_PG_USER
- name: PGUSER
valueFrom:
secretKeyRef:
name: app-secret
key: APP_PG_USER
image: postgres:14.5-alpine
imagePullPolicy: IfNotPresent
readinessProbe:
exec:
command:
- pg_isready
initialDelaySeconds: 30 # Time to create a new DB
failureThreshold: 5
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
exec:
command:
- pg_isready
failureThreshold: 5
periodSeconds: 10
timeoutSeconds: 5
ports:
- containerPort: 5432
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: app-db-volume
hostname: app-db-host
restartPolicy: Always
volumes:
- name: app-db-volume
persistentVolumeClaim:
claimName: app-db-claim
```
Если доступ к БД нужен вне кластера, то можно
определить [Service](https://kubernetes.io/docs/concepts/services-networking/service/) с типом
NodePort - [/deploy/kubernates/base/app-db-service.yaml](https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/base/app-db-service.yaml).
- в этом случае на каждом узле кластера, где развернут Pod с БД будет открыт "рандомный" порт, на который будет смаплен
порт 5432 из Docker контейнера с БД
- по этому "рандомному" порту и IP адресу узла кластера можно получить доступ к БД.
- назначить БД конкретный порт нельзя. При пересоздании Pod через stateless Deployment, порт может быть уже другим.
- для того, чтобы Service был смаплен с созданным через Deployment Pod БД, должны соответствовать
Service.spec.selector.tier: app-db и Deployment.spec.template.metadata.labels.tier: app-db
``` yaml
apiVersion: v1
kind: Service
metadata:
name: app-db
labels:
tier: app-db
namespace: go-app
spec:
type: NodePort # A port is opened on each node in your cluster via Kube proxy.
ports:
- port: 5432
targetPort: 5432
selector:
tier: app-db
```
### Liquibase
Liquibase со скриптами должен запускаться только один раз, сразу после старта БД:
- нужно задержать запуск, пока БД не поднимется
- второй раз Liquibase не должен запускаться
- нужно различать запуски для первичной инсталляции и установки изменений DDL и DML на существующую БД.
В простом случае, без использования Helm, запуск реализован в виде однократно запускаемого
Pod - [/deploy/kubernates/base/app-liquibase-pod.yaml](https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/base/app-liquibase-pod.yaml)
- ENV переменные заполняются из ранее созданных ConfigMap и Secret
- template.metadata.labels задана дополнительная метка tier: app-liquibase, чтобы можно было легко найти Pod Liquibase
- задан отдельный initContainers с image: busybox:1.28, задача которой - бесконечный цикл (лучше его ограничить, иначе
придется руками удалять Pod) - ожидания готовности порта 5432 postgres в Pod c БД.
- для прослушивания порта используется команда nc -w
- здесь нам пригодился созданный
ранее [Service]((https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/base/app-db-service.yaml))
для БД.
- В рамках Kubernetes кластера в качестве краткого DNS имени используется именно Service.metadata.name, которое
мы определили как 'app-db'.
- Это же значения мы записали в ConfigMap.data.APP_PG_HOST
- ConfigMap.data.APP_PG_HOST смаплен на ENV переменную initContainers.env.APP_PG_HOST
- ENV переменную initContainers.env.APP_PG_HOST используем в команде nc -w
- в command определена собственно строка запуска Liquibase с передачей через ENV переменную
initContainers.env.APP_PG_CHANGELOG корневого скрипта для запуска '--changelog-file=./changelog/$(APP_PG_CHANGELOG)'
- чтобы исключить повторный запуск Liquibase Pod указано spec.restartPolicy: Never
- tag для Docker образа будет определен в kustomize через подстановку image: app-liquibase
``` yaml
apiVersion: v1
kind: Pod
metadata:
name: app-liquibase
labels:
tier: app-liquibase
namespace: go-app
spec:
initContainers:
- name: init-app-liquibase
env:
- name: APP_PG_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: APP_PG_HOST
- name: APP_PG_PORT
valueFrom:
configMapKeyRef:
name: app-config
key: APP_PG_PORT
image: busybox:1.28
command: ['sh', '-c', "until nc -w 2 $(APP_PG_HOST) $(APP_PG_PORT); do echo Waiting for $(APP_PG_HOST):$(APP_PG_PORT) to be ready; sleep 5; done"]
containers:
- name: app-liquibase
env:
- name: APP_PG_USER
valueFrom:
secretKeyRef:
name: app-secret
key: APP_PG_USER
- name: APP_PG_PASS
valueFrom:
secretKeyRef:
name: app-secret
key: APP_PG_PASS
- name: APP_PG_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: APP_PG_HOST
- name: APP_PG_PORT
valueFrom:
configMapKeyRef:
name: app-config
key: APP_PG_PORT
- name: APP_PG_DBNAME
valueFrom:
configMapKeyRef:
name: app-config
key: APP_PG_DBNAME
- name: APP_PG_CHANGELOG
valueFrom:
configMapKeyRef:
name: app-config
key: APP_PG_CHANGELOG
image: app-liquibase # image: romapres2010/app-liquibase:2.0.0
command: ['sh', '-c', "docker-entrypoint.sh --changelog-file=./changelog/$(APP_PG_CHANGELOG) --url=jdbc:postgresql://$(APP_PG_HOST):$(APP_PG_PORT)/$(APP_PG_DBNAME) --username=$(APP_PG_USER) --password=$(APP_PG_PASS) --logLevel=info update"]
imagePullPolicy: IfNotPresent
restartPolicy: Never
```
### Go Api
Особенности
развертывания [/deploy/kubernates/base/app-api-deployment.yaml](https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/base/app-api-deployment.yaml):
- начальное количество replicas: 1, остальные будет автоматически создаваться Horizontal Autoscaler
- template.metadata.labels задана дополнительная метка tier: app-api, чтобы можно было легко найти Pod Go App
- ENV переменные заполняются из ранее созданных ConfigMap и Secret
- по аналогии с Liquibase задан отдельный initContainers для ожидания готовности порта 5432 postgres в Pod c БД
- так как все Pod (БД, Liquibase, Go Api) будут запущены одновременно, то возникнет ситуация, когда БД уже
стартовала, но Liquibase еще не применил DDL и DML скрипты. Или применил их только частично.
- в это время контейнер с Go Api (в текущем шаблоне) будет падать с ошибкой и пересоздаваться.
- поэтому нужно правильно настроить readinessProbe для Go Api, которая определяет с какого момента приложение готово
к работе
- альтернативный вариант иметь в БД метку или сигнал, об успешной установке патчей на БД по которому Go Api будет
готова к работе
- определены readinessProbe и livenessProbe на основе httpGet
- определена период мягкой остановки - terminationGracePeriodSeconds: 45
- заданы resources.requests - это определяет минимальные ресурсы, для старта приложения. В зависимости от этого будет
выбран узел кластера со свободными ресурсами.
- заданы resources.limits
- если превышен limits.memory, то Pod будет удален и пересоздан
- limits.cpu контролируется собственно кластером - больше процессорного времени не будет выделено.
- tag для Docker образа будет определен в kustomize через подстановку image: app-api
``` yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-api
labels:
tier: app-api
namespace: go-app
spec:
replicas: 1
selector:
matchLabels:
tier: app-api
strategy:
type: Recreate
template:
metadata:
labels:
app_net: "true"
tier: app-api
spec:
initContainers:
- name: init-app-api
env:
- name: APP_PG_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: APP_PG_HOST
- name: APP_PG_PORT
valueFrom:
configMapKeyRef:
name: app-config
key: APP_PG_PORT
image: busybox:1.28
command: ['sh', '-c', "until nc -w 2 $(APP_PG_HOST) $(APP_PG_PORT); do echo Waiting for $(APP_PG_HOST):$(APP_PG_PORT) to be ready; sleep 5; done"]
terminationGracePeriodSeconds: 45
containers:
- name: app-api
env:
- name: APP_CONFIG_FILE
valueFrom:
configMapKeyRef:
name: app-config
key: APP_CONFIG_FILE
- name: APP_HTTP_LISTEN_SPEC
valueFrom:
configMapKeyRef:
name: app-config
key: APP_HTTP_LISTEN_SPEC
- name: APP_LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: APP_LOG_LEVEL
- name: APP_LOG_FILE
valueFrom:
configMapKeyRef:
name: app-config
key: APP_LOG_FILE
- name: APP_PG_USER
valueFrom:
secretKeyRef:
name: app-secret
key: APP_PG_USER
- name: APP_PG_PASS
valueFrom:
secretKeyRef:
name: app-secret
key: APP_PG_PASS
- name: APP_PG_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: APP_PG_HOST
- name: APP_PG_PORT
valueFrom:
configMapKeyRef:
name: app-config
key: APP_PG_PORT
- name: APP_PG_DBNAME
valueFrom:
configMapKeyRef:
name: app-config
key: APP_PG_DBNAME
image: app-api # image: romapres2010/app-api:2.0.0
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
path: /app/system/health
port: 8080
scheme: HTTP
initialDelaySeconds: 30 # Time to start
failureThreshold: 5
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
httpGet:
path: /app/system/health
port: 8080
scheme: HTTP
failureThreshold: 5
periodSeconds: 10
timeoutSeconds: 5
ports:
- containerPort: 8080
resources:
requests:
cpu: 500m
memory: 256Mi
limits:
cpu: 2000m
memory: 2000Mi
hostname: app-api-host
restartPolicy: Always
```
Доступ к Go Api нужен вне кластера, причем так как Pod может быть несколько, то нужен
LoadBalancer - [/deploy/kubernates/base/app-api-service.yaml](https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/base/app-api-service.yaml).
- в нашем случае для кластера настраивается внешний порт 3000 на IP адрес собственно кластера
- для того, чтобы Service был смаплен с созданным через Deployment Pod Go APi (при масштабировании нагрузки их будет
несколько), должны соответствовать Service.spec.selector.tier: app-api и
Deployment.spec.template.metadata.labels.tier: app-api
``` yaml
apiVersion: v1
kind: Service
metadata:
name: app-api
labels:
tier: app-api
namespace: go-app
spec:
type: LoadBalancer
ports:
- port: 3000
targetPort: 8080
selector:
tier: app-api
```
## 9. Kustomization YAML для Kubernetes
Kubernetes не предоставляет стандартной возможности использовать внешние ENV переменные - каждый раз нужно менять YAML и
заново "накатывать конфигурацию".
Самый простой автоматически вносить изменения в YAML файлы в зависимости от сред развертывания DEV-TEST-PROD -
это [Kustomize](https://kustomize.io/).
- создается "базовая версия" (не содержит специфику сред) YAML для Kubernetes - она выложена в
каталоге [/deploy/kubernates/base](https://github.com/romapres2010/goapp/tree/master/deploy/kubernates/base)
- для каждой из сред (вариантов развертывания) создает отдельный каталог, содержащий изменения в YAML, которые должны
быть применены поверх "базовой версии".
Например, [/deploy/kubernates/overlays/dev](https://github.com/romapres2010/goapp/tree/master/deploy/kubernates/overlays/dev).
Состав YAML "базовой версии" определен
в [/deploy/kubernates/base/kustomization.yaml](https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/base/kustomization.yaml).
``` yaml
commonLabels:
app: app
variant: base
resources:
- app-namespase.yaml
- app-net-networkpolicy.yaml
- app-configmap.yaml
- app-secret.yaml
- app-db-persistentvolumeclaim.yaml
- app-db-deployment.yaml
- app-db-service.yaml
- app-api-deployment.yaml
- app-api-service.yaml
- app-liquibase-pod.yaml
```
Состав изменений, которые нужно применить для среды DEV к "базовой версии" определен
в [/deploy/kubernates/overlays/dev/kustomization.yaml](https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/overlays/dev/kustomization.yaml).
- в имена всех артефактов Kubernetes добавлен дополнительный префикс namePrefix: dev-
- определена новая метка, для фильтрации всех артефактов среды - commonLabels.variant: dev
- определены подстановки реальных Docker образов для app-api и app-liquibase
- определены патчи, которые нужно применить поверх "базовой версии"
``` yaml
namePrefix: dev-
commonLabels:
variant: dev
commonAnnotations:
note: This is development
resources:
- ../../base
images:
- name: app-api
newName: romapres2010/app-api
newTag: 2.0.0
- name: app-liquibase
newName: romapres2010/app-liquibase
newTag: 2.0.0
patches:
- app-configmap.yaml
- app-secret.yaml
- app-api-deployment.yaml
```
### Патчи поверх базовой версии
Включают изменения, специфичные для сред: IP, порты, имена схем БД, пароли, ресурсы, лимиты.
Изменение Secret
``` yaml
apiVersion: v1
kind: Secret
metadata:
name: app-secret
labels:
app: app
namespace: go-app
type: Opaque
stringData:
APP_PG_USER: postgres
APP_PG_PASS: postgres
```
Изменение ConfigMap
``` yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
labels:
app: app
namespace: go-app
data:
APP_CONFIG_FILE: /app/defcfg/app.global.yaml
APP_HTTP_PORT: "8080"
APP_HTTP_LISTEN_SPEC: 0.0.0.0:8080
APP_LOG_LEVEL: ERROR
APP_LOG_FILE: /app/log/app.log
APP_PG_HOST: dev-app-db
APP_PG_PORT: "5432"
APP_PG_DBNAME: postgres
APP_PG_CHANGELOG: db.changelog-1.0_recreate_testdatamdg.xml
```
Изменение Deployment Go Api - заданы другие лимиты, количество реплик и время "чистой" остановки
``` yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-api
labels:
tier: app-api
namespace: go-app
spec:
replicas: 2
template:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app-api
resources:
limits:
cpu: 4000m
memory: 4000Mi
restartPolicy: Always
```
## 10. Тестирование Kubernetes с kustomize
Для тестирования подготовлено несколько скриптов в
каталоге [/deploy/kubernates](https://github.com/romapres2010/goapp/tree/master/deploy/kubernates).
Для тестирования под Windows достаточно поставить Docker Desktop и включить в нем опцию Enable Kubernetes.
_скрипты представлены только для ознакомительных целей_
### Сборка Docker образов
В файле [/deploy/default_repository](https://github.com/romapres2010/goapp/blob/master/deploy/default_repository) нужно
подменить константу на свой Docker репозиторий. Без этого тоже будет работать, но не получится сделать docker push.
Собрать Docker образы:
- Go
App [/deploy/docker/app-api-build.sh](https://github.com/romapres2010/goapp/blob/master/deploy/docker/app-api-build.sh)
-
Liquibase [/deploy/docker/app-liquibase-build.sh](https://github.com/romapres2010/goapp/blob/master/deploy/docker/app-liquibase-build.sh)
Выполнять docker push не обязательно, так как после сборки Docker образы кешируются на локальной машине.
### Развертывание артефактов Kubernetes
Запускаем
скрипт [/deploy/kubernates/kube-build.sh](https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/kube-build.sh)
и передаем ему в качестве параметров среду dev, которую нужно развернуть.
``` shell
./kube-build.sh dev
```
Все результаты логируются в
каталог [/deploy/kubernates/log](https://github.com/romapres2010/goapp/tree/master/deploy/kubernates/log)
- первым шагом выполняется kubectl kustomize и формируется итоговый YAML для среды
DEV [/kubernates/log/build-dev-kustomize.yaml](https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/log/build-dev-kustomize.yaml) -
можно посмотреть как применилась dev конфигурация
- вторым шагом выполняется kubectl apply - запускается одновременное создание всех ресурсов.
- стартуют сразу все Pod, но dev-app-api и dev-app-liquibase уходит в ожидание через initContainers готовности порта
5432 postgres в Pod c БД
- вставлена задержка 120 сек. и после этого собираются логи всех контейнеров
### Kubernetes工件摘要
如果运行[/master/deploy/kubernates/kube-descibe.sh](https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/kube-descibe.sh)
脚本,则可以一目了然地看到 Kubernetes 工件的状态.
- Pod Liquibase 工作成功并停止。它不是通过 Deployment 创建的,因此它有一个“正常名称"dev-app-liquibase
- 两个 Pod Go Api 已启动并运行,它们是通过 Deployment 创建的,因此名称具有“正常”前缀和自动生成的部分
автоматически сгенерированную часть.
- DB Pod 已启动并运行.
- dev-app-api 服务的类型为 LoadBalancer
- 它在集群外部的端口 3000 上可用 - 例如,您可以调用[/metrics](http://127.0.0.1:3000/metrics).
- 标准 LoadBalancer
根据[round robin](https://ru.wikipedia.org/wiki/Round-robin_(%D0%B0%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC))
算法工作,因此在不保存会话的情况下,请求将依次路由到不同的 Pod
- 如果客户端请求 HTTP keep-alive,则请求将转到同一个 Pod(这在使用 Horizontal Autoscaler 时很重要)
- dev-app-db 服务的类型为 NodePort,并分配了端口 30906
- 为了访问集群外部的数据库,您首先需要确定此 Pod 在集群的哪个节点(Node)上引发,并且可以通过节点的 IP 地址和端口 30906
访问数据库
- Pod 可以通过短 DNS 名称服务相互通信
- Liquibase 和 Go API 可以通过 host name = service:dev-app-db 访问数据库
``` shell
$ ./kube-get.sh dev go-app
Kube namespace: go-app
Kube variant: dev
kubectl get pods
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
dev-app-api-59b6ff97b4-2kmfm 1/1 Running 0 13m 10.1.1.182 docker-desktop
dev-app-api-59b6ff97b4-plmz6 1/1 Running 0 13m 10.1.1.183 docker-desktop
dev-app-db-58bbb867d8-c96bz 1/1 Running 0 13m 10.1.1.184 docker-desktop
dev-app-liquibase 0/1 Completed 0 13m 10.1.1.185 docker-desktop
kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
dev-app-api 2/2 2 2 13m app-api romapres2010/app-api:2.0.0 app=app,tier=app-api,variant=dev
dev-app-db 1/1 1 1 13m app-db postgres:14.5-alpine app=app,tier=app-db,variant=dev
kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
dev-app-api LoadBalancer 10.106.114.183 localhost 3000:30894/TCP 13m app=app,tier=app-api,variant=dev
dev-app-db NodePort 10.111.201.17 5432:30906/TCP 13m app=app,tier=app-db,variant=dev
kubectl get configmap
NAME DATA AGE
dev-app-config 9 13m
kubectl get secret
NAME TYPE DATA AGE
dev-app-secret Opaque 2 13m
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE VOLUMEMODE
dev-app-db-claim Bound pvc-996244d5-c5fd-4496-abfd-b6d9301549af 100Mi RWO hostpath 13m Filesystem
kubectl get hpa
No resources found in go-app namespace.
```
### Kubernetes 工件的部署状态
您可以通过运行 [/master/deploy/kubernates/kube-descibe.sh](https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/kube-descibe.sh)
脚本来查看 Kubernetes 工件的已部署状态。
``` shell
$ ./kube-descibe.sh dev go-app
```
请参阅[/deploy/kubernates/log/describe-dev-kube.log](https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/log/describe-dev-kube.log)
中的示例结果。
### 检查 Docker 容器日志
您可以通过运行 [/deploy/kubernates/kube-log.sh](https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/kube-log.sh)
脚本来查看 Kubernetes 容器日志.
``` shell
$ ./kube-log.sh dev go-app
```
我们有 2 个 Pod Go Api 在运行 - 在这个脚本中,它们将被随机播放并写入一个文件。.
### 删除 Kubernetes 项目
您可以使用脚本[/deploy/kubernates/kube-delete.sh](https://github.com/romapres2010/goapp/blob/master/deploy/kubernates/kube-delete.sh)
删除所有项目.
``` shell
$ ./kube-deelte.sh dev go-app
```