
Привет, друзья!
В этой серии статей я делюсь с вами своим опытом решения различных задач из области веб-разработки и не только.
Другие статьи серии:
DevOps Tutorials — Ansible: разворачиваем веб-приложение на виртуальном сервере
DevOps Tutorials — Terraform: создаем виртуальный сервер в облаке
В предыдущих статьях мы рассмотрели настройку сети и создание виртуального сервера Ubuntu Linux в Yandex Cloud с помощью Terraform и деплой Angular+Java веб-приложения на этом сервере с помощью Ansible. В этой статье мы научимся разворачивать JavaScript+Go веб-приложение в кластере Kubernetes.
Интересно? Тогда прошу под кат.
Итак, наша задача - развернуть JavaScript+Go веб-приложение (особенности приложения описаны ниже) в кластере Kubernetes.
Предварительные условия:
на вашей машине должен быть установлен Kubernetes (инструкция для Linux)
в вашем распоряжении должен быть кластер Kubernetes с Ingress (я буду использовать Yandex Managed Service for Kubernetes)
-
для работы с Yandex Cloud требуются
OAuth-токен (
yc config set token <OAuth-токен>
)
по умолчанию приложение будет доступно по внешнему IP Ingress, но это неудобно и некрасиво, поэтому мы "прикрутим" свой домен (купить домен можно, например, здесь)
Пара слов о Kubernetes
Kubernetes (k8s) — это платформа-оркестратор для запуска контейнеризованных приложений в кластере (cluster) серверов. Она позволяет описать желаемое состояние приложения (количество копий, конфигурации, тома (volumes) и т.д.), а система сама обеспечивает достижение и поддержание этого состояния: запускает контейнеры, перезапускает их при сбое, масштабирует, балансирует трафик и т.п.
Допустим, у нас имеется приложение, которое состоит из нескольких частей - фронтенд, бэкенд, база данных, вспомогательные сервисы. Каждая часть упакована в контейнер (например, Docker). Необходимо, чтобы эти контейнеры автоматически запускались, перезапускались при сбое, масштабировались под нагрузкой и могли обновляться без простоя.
K8s берет это на себя:
оркестрация контейнеров — распределяет их по серверам (узлам - nodes) в кластере
масштабирование — увеличивает или уменьшает количество копий контейнера в зависимости от нагрузки
автовосстановление — перезапускает упавшие контейнеры
сетевое взаимодействие — обеспечивает контейнерам доступ друг к другу и к внешнему миру
развертывания без простоя — обновляет версии приложения постепенно (rolling update)
Архитектура
Кластер разделен на контрольную плоскость (control plane/master) и воркеры (worker nodes).
Control plane
API Server — точка входа в кластер (REST API). Вся работа клиентов и компонентов выполняется через него
etcd — распределенное хранилище "ключ-значение", где k8s хранит свое состояние (конфигурации, объектный статус)
kube-scheduler (планировщик) — решает, на каких узлах запускать новые поды (pods)
kube-controller-manager — набор контроллеров (для узлов, реплик (replication), конечных точек (endpoints) и др.), которые следят за желаемым состоянием и приводят кластер в соответствие с ним
Worker nodes
kubelet — агент на каждом узле; получает от API Server манифесты подов и запускает контейнеры через runtime
container runtime (среда выполнения контейнеров, например, containerd, CRI-O) — собственно запускает контейнеры
kube-proxy — реализует сетевой доступ к сервисам (может проксировать или использовать iptables/iptables-replacement)
CNI-плагины (Calico, Flannel, WeaveNet и др.) обеспечивают сетевой уровень между подами
Основные понятия и объекты
Container (контейнер) — как в Docker: приложение + зависимости
Pod (под) — минимальная единица в k8s: один или несколько контейнеров, запущенных вместе и разделяющих сеть и тома. Обычно в Pod — 1 основной контейнер + опциональные вспомогательные контейнеры (sidecars). Поды эфемерны
ReplicaSet (набор реплик) — гарантирует заданное число копий подов
Deployment (деплой, развертывание) — декларация обновлений/управления ReplicaSet (постепенные обновления, откаты). Наиболее часто используемый абстрактный объект для сервисов без состояния (stateless)
StatefulSet (набор состояний) — для приложений с состоянием (stateful) (базы данных, очереди) — сохраняет порядок запуска, стабильные идентификаторы, привязку томов
DaemonSet (набор демонов) — запускает копию пода на каждом (или выбранных) узле — удобно для логирования/мониторинга
Job/CronJob — разовые (Job) или периодические (CronJob) задачи
Service (сервис) — абстракция доступа к подам (виртуальный IP + балансировка). Типы: ClusterIP (по умолчанию, внутри кластера), NodePort, LoadBalancer
Ingress — правила маршрутизации (HTTP) и точка входа в кластер; требует Ingress Controller (nginx-ingress, Traefik и др.)
ConfigMap/Secret — конфигурация и секреты, доступные в подах. Secret хранит данные более защищенно (базовая защита — base64; в проде требуются дополнительные меры, например, Vault)
Volume, PersistentVolume (PV), PersistentVolumeClaim (PVC), StorageClass — абстракции хранения; PVC запрашивает PV, StorageClass описывает политики динамического обеспечения
Namespace (пространство имен) — виртуальное разделение ресурсов (multi-tenant, окружения)
CRD (Custom Resource Definition)/Operator — расширение API k8s для определения собственных типов и автоматизации сложных приложений
Жизненный цикл
Мы пишем манифесты (в формате YAML) — декларативно описываем Deployment, Service и т.д.
kubectl apply
отправляет эти манифесты (объекты) в API Serverконтроллеры и планировщик решают, где и как создать поды; kubelet запускает контейнеры через runtime
если под падает, контроллер перезапускает/заменяет его в соответствии с установленной политикой
при обновлении Deployment происходит rolling update, состояние можно откатить
Сеть
В k8s действует модель "каждому поду — свой IP, поды общаются напрямую". Это требует CNI-плагин
Service скрывает набор подов и предоставляет стабильную конечную точку
Ingress управляет внешним HTTP(S)-трафиком на основе установленных правил
NetworkPolicy контролирует, кто с кем может общаться (фильтрация на уровне пода)
Хранилище данных
Поды по умолчанию эпизодичны (их состояние исчезает вместе с ними) — для постоянных данных используются PersistentVolumes
StorageClass позволяет динамически выделять тома (например, облачные диски в GCE/AWS)
для сервисов с состоянием (БД) используют StatefulSet + PVC
Масштабирование и ресурсы
Requests/Limits (запросы/лимиты) — указывают минимальные/максимальные ресурсы (ЦП, память) для контейнера; влияют на планирование и QoS
Horizontal Pod Autoscaler (HPA) — масштабирует количество подов по метрикам
Vertical Pod Autoscaler (VPA) — корректирует запросы/лимиты
Cluster Autoscaler — добавляет/удаляет узлы в облаке под нужды кластера
Безопасность
RBAC (Role-Based Access Control) — управление доступом к API
NetworkPolicy — сетевые ограничения между подами
Secrets — управление конфиденциальной информацией
Pod Security — политики безопасности подов (раньше была PodSecurityPolicy, в новых версиях была заменена на Pod Security Admission; также есть инструменты вроде Gatekeeper/OPA)
runtime - запуск контейнеров с непривилегированными пользователями, ограничение capabilities, seccomp, SELinux/AppArmor и т.д.
Логирование, метрики, трассировка
Отчеты (логи): обычно собирают агенты (fluentd/fluent-bit, Logstash) и хранят в Elasticsearch/Loki
метрики: Prometheus + Grafana — фактически стандарт мониторинга
трассировка: Jaeger, OpenTelemetry для распределенной трассировки
ключевые компоненты: метрики пода/узла, система оповещений (alerting), панели мониторинга (dashboards)
Инструменты
kubectl — CLI для управления кластером
kubeadm, kops, kubespray — инструменты установки кластера
minikube, kind — локальные кластеры для разработки
Helm — менеджер пакетов (мы поговорим о нем в следующей статье)
Operators (операторы) — автоматизация управления сложными приложениями (CRD + контроллер)
Managed Kubernetes: GKE (Google), EKS (AWS), AKS (Azure), Yandex Cloud — облачные сервисы, упрощающие управление control plane.
Пример минимального Deployment + Service
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-deploy
spec:
replicas: 3
selector:
matchLabels:
app: hello
template:
metadata:
labels:
app: hello
spec:
containers:
- name: hello
image: nginx:stable
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "200m"
memory: "256Mi"
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: hello-svc
spec:
type: ClusterIP
selector:
app: hello
ports:
- port: 80
targetPort: 80
Мы подробно разберем большинство этих полей в дальнейшем.
Материалы для дополнительного изучения:
Пара слов о приложении для деплоя
Наше приложение состоит из фронтенда на Vue.js и бэкенда на Go. Для простоты мы не будем поднимать БД, сервисы будут взаимодействовать напрямую, без сохранения состояния. Рассмотрим Dockerfile сервисов.
Dockerfile бэкенда:
FROM golang:1.21 as builder
WORKDIR /app
# Устанавливаем необходимые зависимости
COPY go.mod go.sum ./
RUN go mod download
# Копируем исходный код приложения
COPY . .
# Собираем приложение для Linux
# Используем CGO_ENABLED=0 для создания статически связанного бинарного файла,
# что позволяет запускать его в контейнере без библиотек C
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/api
# Загружаем сертификат Yandex Internal Root CA
RUN curl https://storage.yandexcloud.net/cloud-certs/CA.pem -o YandexInternalRootCA.crt
FROM alpine:latest
# Устанавливаем ca-certificates для работы с HTTPS
RUN apk --no-cache add ca-certificates
WORKDIR /root
# Копируем сертификат из builder
COPY --from=builder /app/YandexInternalRootCA.crt /usr/local/share/ca-certificates/YandexInternalRootCA.crt
# Обновляем список сертификатов
RUN update-ca-certificates
# Копируем собранное приложение из builder
COPY --from=builder /app/main .
EXPOSE 8081
# Запускаем приложение
CMD ["./main"]
Dockerfile для фронтенда:
FROM node:16.20.0-alpine3.18 as builder
WORKDIR /app
# Устанавливаем необходимые зависимости
COPY package*.json ./
RUN npm install
# Копируем исходный код приложения
COPY . .
# Собираем приложение
RUN npm run build
FROM nginx:1.25-alpine AS production
# Копируем собранное приложение из builder
COPY --from=builder /app/dist /usr/share/nginx/html
# Копируем конфигурацию Nginx
COPY default.conf /etc/nginx/conf.d/default.conf
EXPOSE 8080
Для раздачи статики используется Nginx. Его конфигурация (default.conf
) выглядит так:
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# Проксирование запросов к серверу
location /api/ {
# my-store-backend - название контейнера бэкенда
proxy_pass http://my-store-backend:8081/;
}
}
Для сборки и запуска сервисов используется Docker Compose. docker-compose.yaml
выглядит так:
version: '3.8'
services:
# Сервис бэкенда
backend:
# Образ
image: ${CI_REGISTRY_IMAGE}/my-store-backend:${VERSION}
# Название контейнера
container_name: my-store-backend
restart: always
# Сеть
networks:
- my-store
# Проверка состояния
healthcheck:
test: wget -qO- http://localhost:8081/health
interval: 10s
timeout: 5s
start_period: 10s
retries: 5
# Сервис фронтенда
frontend:
# Образ
image: ${CI_REGISTRY_IMAGE}/my-store-frontend:${VERSION}
# Название контейнера
container_name: my-store-frontend
restart: always
# Порты
ports:
- '80:80'
# Сеть
networks:
- my-store
networks:
my-store:
Сборка и запуск сервисов выполняются в Gitlab CI с помощью Docker Compose на удаленном сервере. Образы фронтенда и бэкенда хранятся в Gitlab Registry. Например, стадии релиза (отправки образа в Gitlab Registry) и деплоя бэкенда в gitlab-ci.yaml
выглядят следующим образом:
# Стадия релиза
release-backend:
stage: release
image:
name: gcr.io/go-containerregistry/crane:debug
entrypoint: ['']
cache: []
before_script:
- crane auth login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- crane tag $CI_REGISTRY_IMAGE/my-store-backend:$CI_COMMIT_SHA $VERSION
# Стадия деплоя (запускается вручную при необходимости)
deploy-backend:
stage: deploy
image: docker:24.0.7-alpine3.19
before_script:
- apk add docker-cli-compose openssh-client bash
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY"| tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 600 ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
- docker context create remote --description "remote ssh" --docker "host=ssh://${DEV_USER}@${DEV_HOST}"
script:
- echo "VERSION=${VERSION}" >> deploy.env
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker --context remote compose --env-file deploy.env up backend -d --pull "always" --force-recreate
environment:
name: review/$CI_COMMIT_REF_SLUG
url: http://${DEV_HOST}:8080
auto_stop_in: 1h
when: manual
Настройка Kubernetes
Для работы с кластером k8s Yandex Cloud необходимо выполнить команду yc managed-kubernetes cluster get-credentials <название кластера> --external
. Это добавит конфигурацию k8s в $HOME/.kube/config
. Не забудьте изменить дефолтный каталог с помощью команды yc config set folder-id <идентификатор каталога>
.
Кратко обсудим структуру объектов k8s, требующихся для деплоя нашего приложения. Каждому сервису требуются объекты Deployment и Service. Кроме этого, сервисам требуется доступ к Gitlab Registry. Для этого мы создадим объект Secrets в сервисе бэкенда.
Забавы ради мы также настроим для бэкенда Vertical Pod Autoscaler.
Что касается фронтенда, то ему также потребуются объекты Configmap с конфигурацией Nginx, Ingress для балансировки нагрузки и обеспечения доступа к приложению извне по домену, а также ClusterIssuer для работы с сертификатами.
Создаем основную директорию и в ней 2 поддиректории:
mkdir kubernetes
cd kubernetes
mkdir frontend backend
Объекты бэкенда
Работаем в директории backend
.
Начнем с Deployment:
deployment.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
# Название деплоя
name: backend
# Метки
labels:
app: backend
spec:
# Количество реплик (копий) приложения
replicas: 1
# Лимит историй релизов.
# Это позволяет откатиться к предыдущим версиям приложения
# в случае необходимости
revisionHistoryLimit: 12
# Стратегия обновления приложения
strategy:
# Поэтапное обновление, без остановки приложения
type: RollingUpdate
rollingUpdate:
# Максимальное количество недоступных реплик во время обновления
maxUnavailable: 0
# Максимальное количество новых реплик, которые могут быть созданы во время обновления
maxSurge: 20%
# Селектор для выбора подов, к которым будет применяться этот деплой
selector:
matchLabels:
app: backend
# Шаблон для создания подов
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
# Образ приложения
image: gitlab.my-services.ru:5050/my-store/my-store-backend:1.0.2213512
imagePullPolicy: IfNotPresent
# Ресурсы, которые будут выделены для контейнера
resources:
requests:
cpu: '0.5'
memory: '256Mi'
limits:
cpu: '1'
memory: '512Mi'
# Порты
ports:
- name: backend
containerPort: 8081
# Проверка состояния приложения
livenessProbe:
httpGet:
path: /health
port: 8081
initialDelaySeconds: 15
periodSeconds: 30
timeoutSeconds: 2
failureThreshold: 6
# Секреты для доступа к Gitlab Registry
imagePullSecrets:
- name: docker-config-secret
Теперь Service:
service.yaml
---
apiVersion: v1
kind: Service
metadata:
name: backend
labels:
app: backend
spec:
# Дефолтный тип сервиса - сервис доступен только внутри кластера
type: ClusterIP
ports:
- protocol: TCP
# Порт, на котором сервис будет доступен внутри кластера
port: 8081
# Порт, на который будет направляться трафик
# в контейнере приложения.
# В данном случае это порт, на котором запускается бэкенд
targetPort: 8081
# Селектор для выбора подов, к которым будет направляться трафик
selector:
app: backend
Теперь Secrets:
secrets.yaml
---
apiVersion: v1
kind: Secret
metadata:
name: docker-config-secret
data:
.dockerconfigjson: <dockerconfigjson>
type: kubernetes.io/dockerconfigjson
О том, как сформировать dockerconfigjson
, можно почитать здесь. Вкратце:
docker login
cat ~/.docker/config.json | base64 --encode
И, наконец, Vertical Pod Autoscaler:
vpa.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: backend-vpa
spec:
# Цель VPA - Deployment с именем "backend"
targetRef:
apiVersion: apps/v1
kind: Deployment
name: backend
# Политика обновления
updatePolicy:
# Режим обновления - "Off" означает, что VPA не будет автоматически изменять ресурсы
# контейнеров, но будет предоставлять рекомендации
# для ручного применения.
# Это позволяет избежать неожиданных изменений в работе приложения.
# "Auto" означает автоматическое применение изменений
updateMode: 'Off'
resourcePolicy:
# Политики ресурсов для контейнеров в Deployment
containerPolicies:
# Применяем политику ко всем контейнерам
- containerName: '*'
# Ресурсы, которые будут контролироваться VPA
controlledResources: ['cpu', 'memory']
VPA автоматически рекомендует и может изменять ресурсы (ЦП, память) для контейнеров в подах, чтобы они работали оптимально.
VPA анализирует нагрузку на приложение и рекомендует оптимальные значения ресурсов. Это помогает избежать недостатка или перерасхода ресурсов.
k8s предоставляет также Horizontal Pod Autoscaler (HPA) для горизонтального масштабирования - увеличения количества реплик приложения.
Объекты фронтенда
Работаем в директории frontend
.
Начнем с Deployment:
deployment.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
# Название деплоя
name: frontend
# Метки
labels:
app: frontend
spec:
# Количество реплик (копий) приложения
replicas: 1
# Лимит историй релизов.
# Это позволяет откатиться к предыдущим версиям приложения
# в случае необходимости
revisionHistoryLimit: 12
# Селектор для выбора подов, к которым будет применяться этот деплой
selector:
matchLabels:
app: frontend
# Шаблон для создания подов
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
# Образ приложения
image: gitlab.my-services.ru:5050/my-store/my-store-frontend:1.0.2213511
imagePullPolicy: IfNotPresent
# Ресурсы, которые будут выделены для контейнера
resources:
requests:
cpu: '0.5'
memory: '256Mi'
limits:
cpu: '1'
memory: '512Mi'
# Порты
ports:
- containerPort: 8080
# Какие тома и куда (внутри контейнера) монтировать
volumeMounts:
- name: nginx-config-volume
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
volumes:
- name: nginx-config-volume
# Ссылка на ConfigMap, который содержит конфигурацию Nginx
configMap:
name: frontend-nginx-config
# Секреты для доступа к Gitlab Registry.
# Secrets из бэкенда
imagePullSecrets:
- name: docker-config-secret
Теперь Service:
service.yaml
---
apiVersion: v1
kind: Service
metadata:
name: frontend
spec:
# По умолчанию сервис имеет тип ClusterIP - доступен только внутри кластера
ports:
- protocol: TCP
# Порт, на котором сервис будет доступен внутри кластера
port: 80
# Порт, на который будет направляться трафик
# в контейнере приложения
targetPort: 8080
# Селектор для выбора подов, к которым будет направляться трафик
selector:
app: frontend
Теперь ConfigMap с настройками Nginx (аналогично рассмотренному ранее default.conf
, кроме названия контейнера бэкенда):
configmap.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: frontend-nginx-config
data:
default.conf: |
server {
listen 8080;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:8081/;
}
}
Теперь Ingress:
ingress.yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: frontend
annotations:
# Какой ClusterIssuer использовать для автоматической выдачи TLS-сертификата через cert-manager
cert-manager.io/cluster-issuer: 'http01-clusterissuer'
spec:
# Использовать контроллер ingress-nginx
ingressClassName: 'nginx'
# Настройка HTTPS
tls:
# Домены, для которых нужен сертификат
- hosts:
# Заменить на свой
- 'mysuperduper.host'
# Имя секрета, где будет храниться TLS-сертификат.
# Заменить на свой
secretName: mysuperduper.host-tls
# Правила маршрутизации трафика
rules:
# Домен, по которому будет доступно приложение.
# Заменить на свой
- host: 'mysuperduper.host'
http:
# Список путей и сервисов
paths:
# Все запросы к корню сайта
- path: /
pathType: Prefix
backend:
service:
# Трафик перенаправляется на сервис "frontend"
name: frontend
port:
number: 80
Ingress управляет входящим HTTP(S)-трафиком и маршрутизирует его к сервисам внутри кластера. Обратите внимание, что он не устанавливается вместе с кластером k8s, его нужно устанавливать отдельно либо через графический интерфейс облака, либо с помощью Terraform.
Что касается домена, то в настройках его DNS необходимо указать запись типа A со значением внешнего IP Ingress. Получить этот адрес можно с помощью команды kubectl get svc -n <namespace> | grep ingress
(столбец EXTERNAL_IP
).
И, наконец, ClusterIssuer:
cert-manager.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: http01-clusterissuer
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
# Заменить на свой
email: myname@mail.com
privateKeySecretRef:
name: http01-clusterissuer-key
solvers:
- http01:
ingress:
class: nginx
Итого
Финальная структура проекта:
kubernetes
| backend
| deployment.yaml
| secrets.yaml
| service.yaml
| vpa.yml
| frontend
| cert-manager.yaml
| configmap.yml
| deployment.yaml
| ingress.yaml
| service.yaml
Команды:
cd kubernetes/
# Заменяем <dockerconfigjson> в backend/secrets.yaml
# Запуск
kubectl apply -R -f .
# Удаление
kubectl delete -R -f .
Мы рассмотрели далеко не все возможности, предоставляемые Kubernetes, но думаю вы получили неплохое представление о том, что и как позволяет делать этот замечательный инструмент. Наряду с другими популярными решениями для автоматизации ИТ-процессов (Ansible, Terraform, Docker и т.д.), Kubernetes на сегодняшний день является важной частью арсенала DevOps-инженера.
Happy devopsing!