
Привет, друзья!
В этой серии статей я делюсь с вами своим опытом решения различных задач из области веб-разработки и не только.
Другие статьи серии:
-
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!
ссылка на оригинал статьи https://habr.com/ru/articles/936584/
Добавить комментарий