Добавил серию Blog 1-5 части. Подправил настройки и внешний вид сайта

This commit is contained in:
2026-02-19 19:49:44 +00:00
parent 3ecd02c4ab
commit b05195a735
238 changed files with 53664 additions and 1026 deletions
@@ -0,0 +1,504 @@
---
title: "Блог на Hugo в K3s: часть 3 - development окружение"
date: 2026-01-15
draft: false
description: "Второй пайплайн для ветки dev с отдельным Hugo Builder, Nginx и защитой через Basic Auth. Тестируем изменения перед публикацией в production."
tags: ["hugo", "k3s", "kubernetes", "traefik", "basic-auth"]
categories: ["infrastructure"]
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)