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