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

505 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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