oakazanin/content/posts/blog-part-3-dev-environment/index.md

13 KiB
Raw Blame History

title date draft description tags categories series series_order
Блог на Hugo в K3s: часть 3 - development окружение 2026-01-15 false Dev окружение для Hugo в K3s: отдельный Hugo Builder, Nginx и Basic Auth через Traefik. Проверяем статьи до публикации в production.
hugo
k3s
kubernetes
traefik
basic-auth
Веб-разработка
DevOps практики
Блог на Hugo в K3s
3

В части 2 мы развернули production окружение для ветки main. Каждый пуш в main автоматически обновляет публичный сайт.

Проблема: нельзя проверить как выглядит статья до публикации. Локальный hugo server показывает одно, а production может выглядеть по-другому из-за версий Hugo, конфигов, CSS.

Нужен второй пайплайн - тестовый контур где можно проверить изменения перед мержем в main.


Архитектура dev окружения

Git Push (dev branch)
  ↓
Gitea
  ↓ webhook
Hugo Builder Dev
  ├→ Clone dev branch
  ├→ hugo --minify
  └→ Output → NFS (dev)
       ↓
  /export/blog-public-dev/
       ↓
    Nginx Dev (1 реплика)
       ↓
  Traefik Ingress
       ├→ Basic Auth Middleware
       └→ dev.blog.ru (SSL)

Отличия от production:

  • Отдельный Hugo Builder (переменная BRANCH=dev)
  • Отдельный NFS volume (blog-public-dev)
  • Отдельный Nginx (одна реплика вместо двух)
  • Basic Auth - доступ только по логину и паролю
  • Отдельный домен (dev.blog.ru)

Всё это живёт в том же namespace что и production. Два независимых пайплайна, нулевое пересечение.


Шаг 1: Hugo Builder для dev

Используем тот же Docker образ что и для production. Разница - в переменной окружения BRANCH.

Файл: 01-hugo-builder-dev.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hugo-builder-dev
  namespace: blog
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hugo-builder-dev
  template:
    metadata:
      labels:
        app: hugo-builder-dev
    spec:
      containers:
      - name: hugo-builder
        image: hugo-builder:latest
        imagePullPolicy: Never
        env:
        - name: BRANCH
          value: "dev"  # Главное отличие - используем dev ветку
        volumeMounts:
        - name: public
          mountPath: /mnt/blog-public
        resources:
          requests:
            cpu: 200m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi
      volumes:
      - name: public
        persistentVolumeClaim:
          claimName: blog-public-dev-pvc  # Отдельный PVC

---
apiVersion: v1
kind: Service
metadata:
  name: hugo-builder-dev
  namespace: blog
spec:
  selector:
    app: hugo-builder-dev
  ports:
  - port: 8080
    targetPort: 8080
    name: webhook
# Применяем манифест
kubectl apply -f 01-hugo-builder-dev.yaml

# Проверяем что под запустился
kubectl get pods -n blog | grep hugo-builder-dev

# Смотрим логи
kubectl logs -n blog deployment/hugo-builder-dev
# Starting webhook listener on port 8080...

Шаг 2: Nginx для dev

Одна реплика вместо двух - для тестового окружения высокая доступность не критична.

Файл: 03-nginx-dev-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-dev
  namespace: blog
spec:
  replicas: 1  # Тестовому окружению достаточно одной реплики
  selector:
    matchLabels:
      app: nginx-dev
  template:
    metadata:
      labels:
        app: nginx-dev
    spec:
      containers:
      - name: nginx
        image: nginx:1.25-alpine
        ports:
        - containerPort: 80
        volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
          readOnly: true
        - name: config
          mountPath: /etc/nginx/nginx.conf
          subPath: nginx.conf
        resources:
          requests:
            cpu: 50m
            memory: 64Mi
      volumes:
      - name: html
        persistentVolumeClaim:
          claimName: blog-public-dev-pvc  # Отдельный PVC
      - name: config
        configMap:
          name: nginx-dev-config

---
apiVersion: v1
kind: Service
metadata:
  name: nginx-dev
  namespace: blog
spec:
  selector:
    app: nginx-dev
  ports:
  - port: 80
    targetPort: 80
    name: http
# Применяем манифест
kubectl apply -f 03-nginx-dev-deployment.yaml

# Проверяем
kubectl get pods -n blog | grep nginx-dev

Шаг 3: Basic Auth через Traefik

Dev окружение должно быть закрыто от посторонних. Traefik поддерживает Basic Auth через Middleware.

Создаём пароль

# Генерируем htpasswd (логин: dev, пароль: ваш пароль)
htpasswd -nb dev your-password
# dev:$apr1$...хеш...

# Кодируем в base64 для Kubernetes Secret
echo -n "dev:$apr1$...хеш..." | base64
# ZGV2OiRhcHIxJC4uLg==

Secret с паролем

Файл: 07-basic-auth-secret.yaml

---
apiVersion: v1
kind: Secret
metadata:
  name: dev-basic-auth
  namespace: blog
type: Opaque
data:
  users: ZGV2OiRhcHIxJC4uLg==  # ваш base64 хеш
# Применяем секрет
kubectl apply -f 07-basic-auth-secret.yaml

# Проверяем что секрет создался
kubectl get secret -n blog | grep dev-basic-auth

Middleware для Basic Auth

Файл: 08-basic-auth-middleware.yaml

---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: dev-basic-auth
  namespace: blog
spec:
  basicAuth:
    secret: dev-basic-auth
    removeHeader: true  # Убираем заголовок Authorization после проверки
# Применяем middleware
kubectl apply -f 08-basic-auth-middleware.yaml

# Проверяем
kubectl get middleware -n blog
# NAME              AGE
# dev-basic-auth    5s

Шаг 4: IngressRoute с Basic Auth

Связываем всё вместе: домен → middleware → nginx-dev.

Файл: 06-ingressroute-dev.yaml

---
# HTTP (без SSL)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: blog-dev-http
  namespace: blog
spec:
  entryPoints:
  - web
  routes:
  - match: Host(`dev.blog.ru`)
    kind: Rule
    services:
    - name: nginx-dev
      port: 80

---
# HTTPS с Basic Auth
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: blog-dev-https
  namespace: blog
spec:
  entryPoints:
  - websecure
  routes:
  - match: Host(`dev.blog.ru`)
    kind: Rule
    middlewares:
    - name: dev-basic-auth  # Добавляем Basic Auth
    services:
    - name: nginx-dev
      port: 80
  tls:
    secretName: blog-dev-tls
# Применяем IngressRoute
kubectl apply -f 06-ingressroute-dev.yaml

# Проверяем
kubectl get ingressroute -n blog | grep dev

Шаг 5: SSL сертификат для dev

cert-manager выпустит отдельный сертификат для dev.blog.ru.

Не забудьте: Добавить A-запись в DNS:

dev.blog.ru    A    77.37.XXX.XXX

Файл: 04-certificate-dev.yaml

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: blog-dev-tls
  namespace: blog
spec:
  secretName: blog-dev-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
  - dev.blog.ru
# Применяем манифест
kubectl apply -f 04-certificate-dev.yaml

# Ждём получения сертификата (30-60 секунд)
kubectl get certificate -n blog

# Должно быть READY=True
# NAME           READY   SECRET          AGE
# blog-dev-tls   True    blog-dev-tls    45s

Шаг 6: Webhook в Gitea для dev

Создаём второй webhook который триггерится на пуши в ветку dev.

Gitea → ваш репозиторий → Settings → Webhooks → Add Webhook → Gitea

  • URL: http://hugo-builder-dev.blog.svc.cluster.local:8080
  • HTTP Method: POST
  • Content Type: application/json
  • Trigger On: Push events
  • Branch filter: dev ← Главное отличие от prod

Нажимаем "Test Delivery" → должен вернуть 200 OK.

Проверяем логи:

# Смотрим логи Hugo Builder Dev
kubectl logs -n blog deployment/hugo-builder-dev -f

# Должно появиться:
# Webhook triggered, starting build...
# Cloning repository (branch: dev)...
# Build successful!

Проверка работы

# Создаём тестовую статью в dev
cd ~/projects/blog
git checkout dev

# Создаём статью
hugo new content posts/test-dev/index.md
echo "Тестовая статья в dev окружении" >> content/posts/test-dev/index.md

# Коммитим и пушим
git add .
git commit -m "test: проверка dev окружения"
git push origin dev

# Следим за логами Hugo Builder Dev
kubectl logs -n blog deployment/hugo-builder-dev -f

# Через 5-7 секунд сборка завершится

Открываем https://dev.blog.ru в браузере:

  1. Браузер запросит логин и пароль (Basic Auth)
  2. Вводим: логин dev, пароль который задали
  3. Видим тестовую статью

Критично: Статья появилась на dev.blog.ru, но её нет на blog.ru - окружения изолированы.


Workflow: dev → prod

Типичный процесс работы:

1. Пишу статью в dev:

cd ~/projects/blog
git checkout dev

# Создаю статью
hugo new content posts/kubernetes-intro/index.md

# Пишу контент, коммичу
git add .
git commit -m "feat: статья про Kubernetes"
git push origin dev
# → автосборка → dev.blog.ru

2. Проверяю на dev.blog.ru:

Открываю https://dev.blog.ru (вводя логин/пароль), читаю статью, проверяю форматирование, ссылки, изображения.

Нахожу опечатку - исправляю локально, пушу в dev снова. Повторяю пока не доволен результатом.

3. Публикую в production:

# Всё отлично на dev - мержу в main
git checkout main
git merge dev
git push origin main
# → автосборка → blog.ru

Статья появляется на публичном сайте.


Итоговая архитектура

Два полностью независимых пайплайна в одном namespace:

Production:
  main branch → hugo-builder-prod → blog-public-pvc → nginx (x2) → blog.ru

Development:
  dev branch → hugo-builder-dev → blog-public-dev-pvc → nginx-dev (x1) → dev.blog.ru + Basic Auth

Общее:

  • Namespace: blog
  • Gitea репозиторий
  • Docker образ Hugo Builder
  • Traefik IngressRoute
  • cert-manager

Отдельное:

  • Deployments
  • Services
  • PersistentVolumes
  • SSL сертификаты
  • Домены

Что дальше

Два окружения работают. Можно писать статьи, проверять на dev, публиковать в production.

Но есть проблема: я долго работал в двух папках - ~/projects/blog (main) и ~/projects/blog-dev (dev). Это создавало конфликты при merge, рассинхронизацию веток и головную боль.

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


Стек этой части:

  • Hugo Builder Dev (та же версия Hugo)
  • Nginx Dev (одна реплика)
  • Traefik Basic Auth Middleware
  • cert-manager (отдельный сертификат)
  • NFS (отдельный volume)