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

```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
```

```bash
# Применяем манифест
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`

```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
```

```bash
# Применяем манифест
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.

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

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

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

### Secret с паролем

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

```yaml
---
apiVersion: v1
kind: Secret
metadata:
  name: dev-basic-auth
  namespace: blog
type: Opaque
data:
  users: ZGV2OiRhcHIxJC4uLg==  # ваш base64 хеш
```

```bash
# Применяем секрет
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`

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

```bash
# Применяем 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`

```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
```

```bash
# Применяем 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`

```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
```

```bash
# Применяем манифест
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`.

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

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

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

---

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

```bash
# Создаём тестовую статью в 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:**

```bash
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:**

```bash
# Всё отлично на 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)
