505 lines
13 KiB
Markdown
505 lines
13 KiB
Markdown
---
|
||
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)
|