DevOps Tutorials — Kubernetes: разворачиваем веб-приложение в облачном кластере

от автора

Привет, друзья!

В этой серии статей я делюсь с вами своим опытом решения различных задач из области веб-разработки и не только.

Другие статьи серии:

В предыдущих статьях мы рассмотрели настройку сети и создание виртуального сервера 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 требуются

  • по умолчанию приложение будет доступно по внешнему 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!


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


Комментарии

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

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