Golang backend server template Kubernates Helm ready
Шаблон backend сервера на Golang — часть 3 (Docker, Docker Compose, Kubernetes (kustomize)

Первая часть шаблона была посвящена HTTP серверу.
Вторая часть шаблона была посвящена прототипированию REST API.
Третья часть посвящена развертыванию шаблона в Docker, Docker Compose, Kubernetes (kustomize).
Четвертая часть будет посвящена развертыванию в Kubernetes с Helm chart и настройке Horizontal Autoscaler.
Пятая часть посвящена оптимизации Worker pool и особенностям его работы в составе микросервиса, развернутого в Kubernetes.
Для корректного развертывания в Kubernetes, в шаблон пришлось внести изменения:
- способа конфигурирования - YAML, ENV, Kustomize
- подхода к логированию - переход на zap
- способа развертывания схемы БД - переход на liquibase
- добавление метрик prometheus
Шаблон goapp в репозитории полностью готов к развертыванию в Docker, Docker Compose, Kubernetes (kustomize), Kubernetes (helm).
Настоящая статья не содержит детального описание используемых технологий
Содержание
- Изменение подхода к конфигурированию
- Добавление метрик prometheus
- Изменение подхода к логированию
- Развертывание схемы БД
- Сборка Docker image
- Сборка Docker-Compose
- Схема развертывания в Kubernetes
- Подготовка YAML для Kubernetes
- Kustomization YAML для Kubernetes
- Тестирование Kubernetes с kustomize
1. Изменение подхода к конфигурированию
Первоначальный вариант запуска приложения с передачей параметров через командную строку не очень удобен для Docker, так как усложняет работу с ENTRYPOINT. Но для совместимости этот вариант приходится поддерживать.Идеальным способом конфигурирования для Docker и Kubernetes являются переменные окружения ENV. Их проще всего подменять при переключении между средами DEV-TEST-PROD. В нашем случае количество конфигурационных параметров приложения перевалило за 1000 - поэтому конфиг был разделен на части:
- условно постоянная часть, которая меняется редко - вынесена в YAML,
- все настройки, связанные со средами порты, пути, БД - вынесены в ENV,
- чувствительные настройки безопасности (пользователи, пароли) - вынесены в отдельное зашифрованное хранилище и передаются либо через ENV, либо через Secret.
Условно постоянная часть конфига
Пример "базового" YAML конфига app.global.yaml.При использовании YAML конфигов в Kubernetes, нужно решить пару задач:
- как сформировать YAML конфиг в зависимости от среды на которой будет развернуто приложение (DEV-TEST-PROD)
- как внедрить YAML конфиг в Docker контейнер, развернутый в Pod Kubernetes
Более правильный вариант - при сборке использовать 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. Весьма удобный побочный эффект такого подхода - возможность переключать альтернативные обработчики без пересборки кода, только меняя конфиг.
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.ERRINCORRECTTYPE_ERROR, requestID, "New", "func(http.ResponseWriter, *http.Request)", reflect.ValueOf(methodInterface).Type().String(), reflect.ValueOf(dummyHandlerFunc).Type().String()).PrintfError() } } else { return err.NewTyped(err.ERRHTTPHANDLERMETHODNOT_FOUND, requestID, handlerCfg.HandlerName).PrintfError() }
s.Handlers[handlerName] = handler } }
2 Добавление метрик prometheus
В шаблон встроена сборка следующих метрик для prometheus:
- Метрики DB
- Метрики HTTP request
- Метрики HTTP client call
- Метрики WorkerPool
Доступ к метрикам настроен по стандартному пути HTTP GET /metrics.
Включение / выключение метрик настраивается через YAML конфиг
yaml конфигурация сбора метрик
metrics: metrics_namespace: com metricssubsystem: goapp collectdbcount_vec: true collectdbduration_vec: false collecthttprequestscountvec: true collecthttperrorrequestscount_vec: true collecthttprequestsdurationvec: true collecthttpactiverequestscount: true collecthttprequests_duration: true collecthttpclientcallcount_vec: true collecthttpclientcallduration_vec: true collectwptaskqueuebufferlenvec: true collectwpaddtaskwaitcountvec: true collectwpworkerprocesscount_vec: true
3. Изменение подхода к логированию
Логирование приложения в Kubernetes можно реализовать несколькими способами:- вывод в stdout, stderr - этот способ является стандартным, но нужно учитывать, что в Kubernetes stdout, stderr Docker контейнеров перенаправляются в файлы, которые имеют ограниченный размер и могут перезаписываться.
- вывод в файл на примонтированный к Docker контейнеру внешний ресурс.
- вывод в структурном виде во внешний сервис, например, Kafka.
В пакете logger реализована возможность настройки нескольких ZapCore через простой YAML конфиг.
Так же в пакете logger добавлена интеграция zap с библиотекой lumberjack для управления циклом ротации и архивирования лог файлов.
Любой из ZapCore можно динамически включать/выключать или изменять через HTTP POST: /system/config/logger.
Например, в следующем конфиге объявлены 4 ZapCore.
- все сообщения от DEBUG до INFO выводятся в файл app.debug.log
- все сообщения от 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.Liquibase позволяет самостоятельно генерировать DDL скрипты БД. Для простых случаев - этого вполне хватает, но если требуется тонкая настройка БД (например,настройка партиций, Blob storage) то лучше формировать DDL скрипты в отдельном приложении. Для Postgres отлично подходит TOAD DataModeler. Для Oracle - Oracle SQL Developer Data Modeler. Например, TOAD DataModeler умеет корректно генерировать DDL скрипты для добавления not null столбцов в уже заполненную таблицу.
Весьма полезная функция Liquibase - возможность задавать скрипты для отката изменений. Если установка патча на БД прошла неуспешно (сбой добавления столбца в таблицу, нарушение PK, FK), то легко откатиться к предыдущему состоянию.
В шаблон включены примеры liquibase/changelog и DDL скрипты для таблиц Country и Currency с минимальным тестовым наполнением.
Так же как и с YAML конфигами, необходимо предусмотреть способ внедрения Liquibase Changelog в Docker контейнер Liquibase, развернутый в Job Pod Kubernetes:
- скрипты можно встраивать в Liquibase контейнер (не очень хороший вариант, как минимум размер Docker с Liquibase более 300 Мбайт),
- скрипты можно выкладывать в примонтированный каталог к Docker контейнер и на уровне ENV переменных указывать Changelog для запуска.
5. Сборка Docker image
Docker файлы для сборки выложены в каталоге deploy/docker.Для тестирования сборки можно использовать Docker Desktop - в этом случае не нужно делать push Docker image - они все кешируются на локальной машине.
Сборка Docker image для Go Api
Сборка Docker image для Go Api 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 APPBUILDTIME
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=${APPCOMMIT} -X main.buildTime=${APPBUILDTIME} -X main.version=${APPVERSION}" -o ./run/main ./cmd/app/main.go
RUN echo "Based on commit: $APPCOMMIT" && echo "Build Time: $APPBUILDTIME" && echo "Version: $APPVERSION"
##
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:
- сборка и запуск всех скриптов всегда осуществляется от корня репозитория
- логирование вывода всех скриптов идет в файлы
- управление версией, именем приложения и docker tag представлено в упрощенном виде через локальные файлы в каталоге deploy
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 APPAPIAPPNAME=$(cat ./deploy/appapiappname) export APPREPOSITORY=$(cat ./deploy/defaultrepository) export APPBUILDTIME=$(date -u '+%Y-%m-%d_%H:%M:%S') export APP_COMMIT=$(git rev-parse --short HEAD)
docker build --build-arg APPVERSION=$APPVERSION --build-arg APPBUILDTIME=$APPBUILDTIME --build-arg APPCOMMIT=$APPCOMMIT -t $APPREPOSITORY/$APPAPIAPPNAME:$APPVERSION -f ./deploy/docker/app-api.Dockerfile . 1>$LOGFILE 2>&1
echo "Go to current directory:" popd
Сборка Docker image для liquibase
Сборка Docker image для Liquibase 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.В каталоге /deploy/run/local.compose.global.config выложены скрипты для локального тестирования с использованием Docker Compose.
- все ENV переменные приложений передаются через внешний .env файл
- каждый компонент системы (API, БД, UI) можно поднимать независимо, например:
Docker Compose для Go Api
Особенности:- при каждом запуске выполняется build
- аргументы в Dockerfile (APPCOMMIT, APPBUILDTIME, APPVERSION) пробрасываются через ENV переменные окружения
- ENV переменные приложения берутся из одноименных ENV переменных окружения (дефолтные значения указаны для примера)
- в Docker монтируются volumеs для конфиг и логов (/app/cfg:ro, /app/log:rw)
- зависимости (dependson) от БД не выставляются - вместо этого приложение написано так, чтобы оно могло перестартовывать без последствий и указано restartpolicy: on-failure
- настроен healthcheck на GET: /app/system/health
dockerfile
app-api:
build:
context: ./../../
dockerfile: ./deploy/docker/app-api.Dockerfile
tags:
- $APPREPOSITORY/$APPAPIAPPNAME:$APP_VERSION
args:
- APPCOMMIT=${APPCOMMIT:-unset}
- APPBUILDTIME=${APPBUILDTIME:-unset}
- APPVERSION=${APPVERSION:-unset}
container_name: app-api
hostname: app-api-host
networks:
- app-net
ports:
- $APPHTTPOUTPORT:$APPHTTP_PORT
- 8001:$APPHTTPPORT
environment:
- TZ="Europe/Moscow"
- APPCONFIGFILE=${APPCONFIGFILE:-/app/defcfg/app.global.yaml}
- APPHTTPLISTENSPEC=${APPHTTPLISTENSPEC:-0.0.0.0:8080}
- APPLOGLEVEL=${APPLOGLEVEL:-ERROR}
- APPLOGFILE=${APPLOGFILE:-/app/log/app.log}
- APPPGUSER=${APPPGUSER:-postgres}
- APPPGPASS=${APPPGPASS:?database password not set}
- APPPGHOST=${APPPGHOST:-app-db-host}
- APPPGPORT=${APPPGPORT:-5432}
- APPPGDBNAME=${APPPGDBNAME:-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)
- устанавливается зависимость от БД (dependson condition: servicehealthy)
- при необходимости в 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:
- $APPREPOSITORY/$APPLUQUIBASEAPPNAME:$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/$APPPGCHANGELOG --url="jdbc:postgresql://$APPPGHOST:$APPPGPORT/$APPPGDBNAME" --username=$APPPGUSER --password=$APPPGPASS --logFile="mylog/liquibase.log" --logLevel=info update
command: --changelog-file=./changelog/$APPPGCHANGELOG --url="jdbc:postgresql://$APPPGHOST:$APPPGPORT/$APPPGDBNAME" --username=$APPPGUSER --password=$APPPGPASS --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:
- POSTGRESPASSWORD=${APPPG_PASS:?database password not set}
- PGUSER=${APPPGUSER:?database user not set}
networks:
- app-net
ports:
- $APPPGOUTPORT:$APPPG_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 приведена в сокращенном виде, исключены компоненты, связанные с UI (Jmix) и распределенным кэшем (hazelcast).
Основным элементом развертывания в Kubernetes является Pod. Для простоты - это группа контейнеров с общими ресурсами, которая изолирована от остальных Pod.
- Обычно, в Pod размещается один app контейнер, и, при необходимости, init контейнер.
- Остановить app контейнер в Pod нельзя, либо он завершит работу сам, либо нужно удалить Pod (руками или через Deployment уменьшив количество replicas дo 0).
- Pod размещается на определенном узле кластера Kubernetes
- Для Pod задается политика автоматического рестарта в рамках одного Node restartPolicy: Always, OnFailure, Never.
- Остановкой и запуском Pod управляет Kubernetes. В общем случае, Pod может быть автоматически остановлен если:
- При остановке Pod:
При разработке stateless приложений для Kubernetes нужно учитывать эту специфику:
- приложение может быть остановлено в любой момент
- при запуске/перезапуске приложения все данные в локальной файловой системе контейнера потенциально могут быть потеряны
- при запуске/перезапуске приложения данные на примонтированных Volume потенциально могут быть так же потеряны
- при остановке приложения доступ к stdout и stderr получить можно, но есть ограничения - критические логи желательно сразу отправлять во внешний сервис или на Volume, который точно не будет удален
- в Kubernetes нет возможности явно задать последовательность старта различных Pod
8. Подготовка YAML для Kubernetes
Конфигурирование
В Kubernetes предусмотрено для основных способа конфигурирования ConfigMaps для основных настроек и Secrets для настроек, чувствительных с точки зрения безопасности.В простейшем случае /deploy/kubernates/base/app-configmap.yaml, содержит переменные со строковыми значениями. В более сложных вариантах, можно считывать и разбирать конфиги из внешнего файла или использовать внешний файл без "разбора".
Все артефакты Kubernetes желательно сразу создавать в отдельном namespace /deploy/kubernates/base/app-namespase.yaml.
Для каждого артефакта нужно указывать метки для дальнейшего поиска и фильтрации. В примере приведена одна метка с именем 'app' и значением 'app'.
yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
labels:
app: app
namespace: go-app
data:
APPCONFIGFILE: /app/defcfg/app.global.yaml
APPHTTPPORT: "8080"
APPHTTPLISTEN_SPEC: 0.0.0.0:8080
APPLOGLEVEL: ERROR
APPLOGFILE: /app/log/app.log
APPPGHOST: dev-app-db
APPPGPORT: "5432"
APPPGDBNAME: postgres
APPPGCHANGELOG: db.changelog-1.0recreatetestdatamdg.xml
"Секретные" конфигурационные данные определяются в /deploy/kubernates/base/app-secret.yaml.
Данный вариант максимально упрощенный - правильно использовать внешнее зашифрованное хранилище.
yaml
apiVersion: v1
kind: Secret
metadata:
name: app-secret
labels:
app: app
namespace: go-app
type: Opaque
stringData:
APPPGUSER: postgres
APPPGPASS: postgres
БД
Развертывание БД в шаблоне приведено в упрощенном stateless варианте (может быть использовано только для dev). Правильный вариант для промышленной эксплуатации - развертывание через оператор с поддержкой кластеризации, например, zalando.Прежде всего, нужно запросить ресурсы для Persistent Volumes на котором будет располагаться файлы с БД.
Если PersistentVolumeClaim удаляется, то выделенный Persistent Volumes в зависимости от persistentVolumeReclaimPolicy, будет удален, очищен или сохранен.
Хорошая статья с описанием хранилищ данных (Persistent Volumes) в 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, который задает шаблон по которому будут создаваться Pod. Особенности развертывания /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: APPPGPASS - name: POSTGRES_DB valueFrom: configMapKeyRef: name: app-config key: APPPGDBNAME - name: POSTGRES_USER valueFrom: secretKeyRef: name: app-secret key: APPPGUSER - name: PGUSER valueFrom: secretKeyRef: name: app-secret key: APPPGUSER 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 с типом NodePort - /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 на существующую БД.
- ENV переменные заполняются из ранее созданных ConfigMap и Secret
- template.metadata.labels задана дополнительная метка tier: app-liquibase, чтобы можно было легко найти Pod Liquibase
- задан отдельный initContainers с image: busybox:1.28, задача которой - бесконечный цикл (лучше его ограничить, иначе придется руками удалять Pod) - ожидания готовности порта 5432 postgres в Pod c БД.
- в command определена собственно строка запуска Liquibase с передачей через ENV переменную initContainers.env.APPPGCHANGELOG корневого скрипта для запуска '--changelog-file=./changelog/$(APPPGCHANGELOG)'
- чтобы исключить повторный запуск 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: APPPGHOST
valueFrom:
configMapKeyRef:
name: app-config
key: APPPGHOST
- name: APPPGPORT
valueFrom:
configMapKeyRef:
name: app-config
key: APPPGPORT
image: busybox:1.28
command: ['sh', '-c', "until nc -w 2 $(APPPGHOST) $(APPPGPORT); do echo Waiting for $(APPPGHOST):$(APPPGPORT) to be ready; sleep 5; done"]
containers:
- name: app-liquibase
env:
- name: APPPGUSER
valueFrom:
secretKeyRef:
name: app-secret
key: APPPGUSER
- name: APPPGPASS
valueFrom:
secretKeyRef:
name: app-secret
key: APPPGPASS
- name: APPPGHOST
valueFrom:
configMapKeyRef:
name: app-config
key: APPPGHOST
- name: APPPGPORT
valueFrom:
configMapKeyRef:
name: app-config
key: APPPGPORT
- name: APPPGDBNAME
valueFrom:
configMapKeyRef:
name: app-config
key: APPPGDBNAME
- name: APPPGCHANGELOG
valueFrom:
configMapKeyRef:
name: app-config
key: APPPGCHANGELOG
image: app-liquibase # image: romapres2010/app-liquibase:2.0.0
command: ['sh', '-c', "docker-entrypoint.sh --changelog-file=./changelog/$(APPPGCHANGELOG) --url=jdbc:postgresql://$(APPPGHOST):$(APPPGPORT)/$(APPPGDBNAME) --username=$(APPPGUSER) --password=$(APPPGPASS) --logLevel=info update"]
imagePullPolicy: IfNotPresent
restartPolicy: Never
Go Api
Особенности развертывания /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 БД
- определены readinessProbe и livenessProbe на основе httpGet
- определена период мягкой остановки - terminationGracePeriodSeconds: 45
- заданы resources.requests - это определяет минимальные ресурсы, для старта приложения. В зависимости от этого будет выбран узел кластера со свободными ресурсами.
- заданы resources.limits
- 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: APPPGHOST valueFrom: configMapKeyRef: name: app-config key: APPPGHOST - name: APPPGPORT valueFrom: configMapKeyRef: name: app-config key: APPPGPORT image: busybox:1.28 command: ['sh', '-c', "until nc -w 2 $(APPPGHOST) $(APPPGPORT); do echo Waiting for $(APPPGHOST):$(APPPGPORT) to be ready; sleep 5; done"] terminationGracePeriodSeconds: 45 containers: - name: app-api env: - name: APPCONFIGFILE valueFrom: configMapKeyRef: name: app-config key: APPCONFIGFILE - name: APPHTTPLISTEN_SPEC valueFrom: configMapKeyRef: name: app-config key: APPHTTPLISTEN_SPEC - name: APPLOGLEVEL valueFrom: configMapKeyRef: name: app-config key: APPLOGLEVEL - name: APPLOGFILE valueFrom: configMapKeyRef: name: app-config key: APPLOGFILE - name: APPPGUSER valueFrom: secretKeyRef: name: app-secret key: APPPGUSER - name: APPPGPASS valueFrom: secretKeyRef: name: app-secret key: APPPGPASS - name: APPPGHOST valueFrom: configMapKeyRef: name: app-config key: APPPGHOST - name: APPPGPORT valueFrom: configMapKeyRef: name: app-config key: APPPGPORT - name: APPPGDBNAME valueFrom: configMapKeyRef: name: app-config key: APPPGDBNAME 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.
- в нашем случае для кластера настраивается внешний порт 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.
- создается "базовая версия" (не содержит специфику сред) YAML для Kubernetes - она выложена в каталоге /deploy/kubernates/base
- для каждой из сред (вариантов развертывания) создает отдельный каталог, содержащий изменения в YAML, которые должны быть применены поверх "базовой версии". Например, /deploy/kubernates/overlays/dev.
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.
- в имена всех артефактов 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: APPPGUSER: postgres APPPGPASS: postgres
Изменение ConfigMap
yaml apiVersion: v1 kind: ConfigMap metadata: name: app-config labels: app: app namespace: go-app data: APPCONFIGFILE: /app/defcfg/app.global.yaml APPHTTPPORT: "8080" APPHTTPLISTEN_SPEC: 0.0.0.0:8080 APPLOGLEVEL: ERROR APPLOGFILE: /app/log/app.log APPPGHOST: dev-app-db APPPGPORT: "5432" APPPGDBNAME: postgres APPPGCHANGELOG: db.changelog-1.0recreatetestdatamdg.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.Для тестирования под Windows достаточно поставить Docker Desktop и включить в нем опцию Enable Kubernetes.
скрипты представлены только для ознакомительных целей
Сборка Docker образов
В файле /deploy/default_repository нужно подменить константу на свой Docker репозиторий. Без этого тоже будет работать, но не получится сделать docker push.
Собрать Docker образы:
- Go App /deploy/docker/app-api-build.sh
- Liquibase /deploy/docker/app-liquibase-build.sh
Развертывание артефактов Kubernetes
Запускаем скрипт /deploy/kubernates/kube-build.sh и передаем ему в качестве параметров среду dev, которую нужно развернуть.
shell
./kube-build.sh dev
Все результаты логируются в каталог /deploy/kubernates/log
- первым шагом выполняется kubectl kustomize и формируется итоговый YAML для среды DEV /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, то можно посмотреть краткий статус артефактов Kubernetes.
- Pod Liquibase успешно отработал и остановился. Его создавали не через Deployment, поэтому у него "нормальное имя" dev-app-liquibase.
- Запущены и работают два Pod Go Api, они создавались через Deployment, поэтому имена имеют "нормальный" префикс и автоматически сгенерированную часть.
- Запущен и работает Pod БД.
- Сервис dev-app-api имеет тип LoadBalancer
- Сервис dev-app-db имеет тип NodePort и указан назначенный ему порт 30906
- Между собой Pod могут коммуницировать через краткое DNS имя service.
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 <none> <none> dev-app-api-59b6ff97b4-plmz6 1/1 Running 0 13m 10.1.1.183 docker-desktop <none> <none> dev-app-db-58bbb867d8-c96bz 1/1 Running 0 13m 10.1.1.184 docker-desktop <none> <none> dev-app-liquibase 0/1 Completed 0 13m 10.1.1.185 docker-desktop <none> <none>
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 <none> 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
Развернутый статус артефактов Kubernetes можно посмотреть, запустив скрипт /master/deploy/kubernates/kube-descibe.sh.
shell
$ ./kube-descibe.sh dev go-app
Пример результатов в файле /deploy/kubernates/log/describe-dev-kube.log.
Проверка логов Docker контейнеров
Логи контейнеров Kubernetes можно посмотреть, запустив скрипт /deploy/kubernates/kube-log.sh.
shell
$ ./kube-log.sh dev go-app
У нас запущено 2 Pod Go Api - в этом скрипте они будут перемешаны и записаны в один файл.
Удаление артефактов Kubernetes
Удалить все артефакты можно скриптом /deploy/kubernates/kube-delete.sh.
shell
$ ./kube-deelte.sh dev go-app