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

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

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

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

Комментарии (0)