Урок 33: Kubernetes — deployment, probes, HPA, ConfigMaps

Урок 33. Kubernetes — deployment, probes, HPA, ConfigMaps

🔄 Node.js → Go (ключевые особенности в K8s):

📋 Что изучаем

📦 Инициализация проекта

mkdir go-k8s && cd go-k8s
go mod init go-k8s

# Создаём простое приложение с health-чеками
mkdir -p cmd/server
mkdir -p k8s

💻 Файл: cmd/server/main.go

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

var (
    startTime = time.Now()
    isReady   = false // Меняем на true когда готовы принимать трафик
)

func main() {
    log.SetFlags(log.LstdFlags | log.Lmicroseconds)
    log.Println("🚀 Запуск Go-приложения в Kubernetes...")

    // Имитация инициализации (подключение к БД, прогрев кэша)
    go func() {
        log.Println("⏳ Инициализация (2 секунды)...")
        time.Sleep(2 * time.Second)
        isReady = true
        log.Println("✅ Приложение готово к работе")
    }()

    mux := http.NewServeMux()

    // Liveness probe — жив ли процесс?
    mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("OK"))
    })

    // Readiness probe — готов ли принимать запросы?
    mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
        if isReady {
            w.WriteHeader(http.StatusOK)
            w.Write([]byte("Ready"))
        } else {
            w.WriteHeader(http.StatusServiceUnavailable)
            w.Write([]byte("Not Ready"))
        }
    })

    // Startup probe — запустился ли? (для медленных приложений)
    mux.HandleFunc("GET /startupz", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("Started"))
    })

    // Информация о поде
    mux.HandleFunc("GET /info", func(w http.ResponseWriter, r *http.Request) {
        hostname, _ := os.Hostname()
        info := map[string]any{
            "hostname":   hostname,
            "version":    "1.0.0",
            "uptime":     time.Since(startTime).String(),
            "go_version": runtime.Version(),
            "env":        os.Getenv("APP_ENV"),
        }
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(info)
    })

    // Имитация нагрузки (для тестирования HPA)
    mux.HandleFunc("GET /cpu-load", func(w http.ResponseWriter, r *http.Request) {
        // Нагружаем CPU на 100ms
        done := make(chan struct{})
        go func() {
            for i := 0; i < 10000000; i++ {
                _ = i * i
            }
            close(done)
        }()
        <-done
        w.Write([]byte("CPU load completed"))
    })

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    server := &http.Server{
        Addr:    ":" + port,
        Handler: mux,
    }

    // Graceful shutdown (важно для K8s!)
    go func() {
        log.Printf("✅ Сервер на :%s", port)
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Сервер: %v", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    sig := <-quit
    log.Printf("📡 Сигнал %v — выключаемся...", sig)

    // K8s даёт terminationGracePeriodSeconds на завершение
    isReady = false // Сразу перестаём принимать новый трафик
    log.Println("⏳ Ждём завершения активных запросов (5 секунд)...")
    time.Sleep(5 * time.Second) // Имитация drain

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    server.Shutdown(ctx)
    log.Println("👋 Завершено")
}

import "runtime"

☸️ Файл: Dockerfile

FROM golang:1.22-alpine AS builder
RUN apk add --no-cache git ca-certificates
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /server ./cmd/server

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
USER nonroot
ENTRYPOINT ["/server"]

☸️ Файл: k8s/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-app
  labels:
    app: go-app
spec:
  replicas: 3  # 3 пода для отказоустойчивости
  selector:
    matchLabels:
      app: go-app
  # Стратегия обновления
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # Максимум дополнительных подов при обновлении
      maxUnavailable: 0  # Минимум 0 недоступных (всегда доступны!)
  template:
    metadata:
      labels:
        app: go-app
        version: "1.0.0"
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
        prometheus.io/path: "/metrics"
    spec:
      # Graceful shutdown: даём 15 секунд на завершение
      terminationGracePeriodSeconds: 15
      containers:
      - name: go-app
        image: go-k8s-app:1.0.0
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8080
          protocol: TCP
        env:
        # Конфигурация из ConfigMap
        - name: APP_ENV
          valueFrom:
            configMapKeyRef:
              name: go-app-config
              key: app.env
        - name: LOG_LEVEL
          valueFrom:
            configMapKeyRef:
              name: go-app-config
              key: log.level
        # Секреты из Secret
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: go-app-secrets
              key: database-url
        - name: REDIS_URL
          valueFrom:
            secretKeyRef:
              name: go-app-secrets
              key: redis-url

        # ╔══════════════════════════════════════════════════╗
        # ║  PROBES (проверки)                              ║
        # ╚══════════════════════════════════════════════════╝
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 1  # Go стартует быстро
          periodSeconds: 10
          timeoutSeconds: 2
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /readyz
            port: 8080
          initialDelaySeconds: 0  # Проверяем сразу
          periodSeconds: 5
          timeoutSeconds: 2
          failureThreshold: 2
        startupProbe:
          httpGet:
            path: /startupz
            port: 8080
          initialDelaySeconds: 0
          periodSeconds: 1
          failureThreshold: 30  # До 30 секунд на старт

        # ╔══════════════════════════════════════════════════╗
        # ║  RESOURCES (Go потребляет МАЛО!)                ║
        # ╚══════════════════════════════════════════════════╝
        resources:
          requests:
            cpu: "50m"      # 0.05 ядра
            memory: "32Mi"  # 32 MB
          limits:
            cpu: "200m"     # 0.2 ядра
            memory: "128Mi" # 128 MB

☸️ Файл: k8s/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: go-app-service
  labels:
    app: go-app
spec:
  type: ClusterIP  # Внутренний доступ
  selector:
    app: go-app
  ports:
  - port: 80         # Порт сервиса
    targetPort: 8080  # Порт контейнера
    protocol: TCP
    name: http
  sessionAffinity: None

☸️ Файл: k8s/configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: go-app-config
data:
  app.env: "production"
  log.level: "info"
  app.name: "go-k8s-demo"
  # Можно хранить целые файлы
  config.yaml: |
    server:
      port: 8080
      read_timeout: 5s
    features:
      enable_cache: true
      enable_metrics: true

☸️ Файл: k8s/secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: go-app-secrets
type: Opaque
stringData:  # stringData автоматически кодируется в base64
  database-url: "postgres://user:password@postgres:5432/appdb?sslmode=disable"
  redis-url: "redis://redis:6379/0"
  jwt-secret: "your-super-secret-key-here"

☸️ Файл: k8s/hpa.yaml

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: go-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: go-app
  minReplicas: 2   # Минимум 2 пода
  maxReplicas: 10  # Максимум 10 подов
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70  # Масштабируем при 70% CPU
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80  # Масштабируем при 80% памяти
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300  # Ждём 5 минут перед уменьшением
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0    # Увеличиваем мгновенно
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15

☸️ Файл: k8s/ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: go-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: go-app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: go-app-service
            port:
              number: 80

🚀 Запуск в Kubernetes

# Собираем Docker-образ
docker build -t go-k8s-app:1.0.0 .

# Если используете minikube
minikube start
minikube image load go-k8s-app:1.0.0

# Применяем манифесты
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/secret.yaml
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
kubectl apply -f k8s/hpa.yaml

# Проверяем
kubectl get pods
kubectl get svc
kubectl get hpa

# Логи
kubectl logs -f deployment/go-app

# Проброс порта (если нет Ingress)
kubectl port-forward svc/go-app-service 8080:80

# Тестирование
curl http://localhost:8080/healthz
curl http://localhost:8080/info

# Нагрузочное тестирование HPA
kubectl run -it --rm load-generator --image=busybox -- /bin/sh
# Внутри контейнера:
while true; do wget -q -O- http://go-app-service/cpu-load; done

📊 K8s Probes

ProbeНазначениеПри провалеGo-особенности
LivenessЖив ли процесс?Перезапуск контейнераinitialDelaySeconds: 1 (быстрый старт)
ReadinessГотов принимать трафик?Убрать из ServiceinitialDelaySeconds: 0 (можно сразу)
StartupЗапустился ли?Блокирует Liveness/ReadinessНужен если есть долгая инициализация
⚠️ Типичные ошибки:
💡 Практический совет:

💡 Best practices от сеньоров:

🔑 Ключевые концепции

Что нужно запомнить из этого урока:

💡 Для Node.js разработчика:

← Предыдущий урок Следующий урок →