Добавил серию Blog 1-5 части. Подправил настройки и внешний вид сайта
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -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)
|
||||
Reference in New Issue
Block a user