diff --git a/config/_default/params.toml b/config/_default/params.toml index 65edf5b..934be0d 100644 --- a/config/_default/params.toml +++ b/config/_default/params.toml @@ -11,7 +11,7 @@ autoSwitchAppearance = false enableA11y = false enableSearch = true -enableCodeCopy = true +enableCodeCopy = false enableStructuredBreadcrumbs = false # enableStyledScrollbar = true # disable to use native scrollbar style (defaults to true) @@ -70,8 +70,8 @@ forgejoDefaultServer = "https://v11.next.forgejo.org" showDateUpdated = false showAuthor = true # showAuthorBottom = false - showHero = false - # heroStyle = "basic" # valid options: basic, big, background, thumbAndBackground + showHero = true + heroStyle = "background" # valid options: basic, big, background, thumbAndBackground layoutBackgroundBlur = true # only used when heroStyle equals background or thumbAndBackground layoutBackgroundHeaderSpace = true # only used when heroStyle equals background showBreadcrumbs = false @@ -85,8 +85,8 @@ forgejoDefaultServer = "https://v11.next.forgejo.org" invertPagination = false showReadingTime = true showTableOfContents = true - # showRelatedContent = false - # relatedContentLimit = 3 + showRelatedContent = true + relatedContentLimit = 6 showTaxonomies = true # use showTaxonomies OR showCategoryOnly, not both showCategoryOnly = false # use showTaxonomies OR showCategoryOnly, not both showAuthorsBadges = false @@ -96,8 +96,8 @@ forgejoDefaultServer = "https://v11.next.forgejo.org" # externalLinkForceNewTab = false # disable to allow external links in the same tab (defaults to true) [list] - showHero = false - # heroStyle = "background" # valid options: basic, big, background, thumbAndBackground + showHero = true + heroStyle = "background" # valid options: basic, big, background, thumbAndBackground layoutBackgroundBlur = true # only used when heroStyle equals background or thumbAndBackground layoutBackgroundHeaderSpace = true # only used when heroStyle equals background showBreadcrumbs = false @@ -107,7 +107,7 @@ forgejoDefaultServer = "https://v11.next.forgejo.org" showTableOfContents = false showCards = false orderByWeight = false - groupByYear = true + groupByYear = false cardView = false cardViewScreenWidth = false constrainItemsWidth = false @@ -117,8 +117,8 @@ forgejoDefaultServer = "https://v11.next.forgejo.org" [taxonomy] showTermCount = true - showHero = false - # heroStyle = "background" # valid options: basic, big, background, thumbAndBackground + showHero = true + heroStyle = "background" # valid options: basic, big, background, thumbAndBackground showBreadcrumbs = false showViews = false showLikes = false @@ -126,8 +126,8 @@ forgejoDefaultServer = "https://v11.next.forgejo.org" cardView = false [term] - showHero = false - # heroStyle = "background" # valid options: basic, big, background, thumbAndBackground + showHero = true + heroStyle = "background" # valid options: basic, big, background, thumbAndBackground showBreadcrumbs = false showViews = false showLikes = false diff --git a/content/posts/blog-part-1-architecture/featured.png b/content/posts/blog-part-1-architecture/featured.png new file mode 100644 index 0000000..1a37bfc Binary files /dev/null and b/content/posts/blog-part-1-architecture/featured.png differ diff --git a/content/posts/blog-part-1-architecture/index.md b/content/posts/blog-part-1-architecture/index.md new file mode 100644 index 0000000..6faf7ee --- /dev/null +++ b/content/posts/blog-part-1-architecture/index.md @@ -0,0 +1,306 @@ +--- +title: "Блог на Hugo в K3s: часть 1 - архитектура и первый запуск" +date: 2026-01-03 +draft: false +description: "Как я переехал с Jekyll на Hugo, почему выбрал тему Blowfish и как настроил два окружения с нуля. Начало цикла о том как я строил oakazanin.ru." +tags: ["hugo", "blowfish", "gitea", "homelab", "devops"] +categories: ["infrastructure"] +series: ["Блог на Hugo в K3s"] +series_order: 1 +--- + +Предыдущий блог жил на Jekyll. Жил - громко сказано. Скорее существовал, периодически ломаясь при обновлениях и требуя ритуальных танцев с Ruby каждый раз когда я садился за новую статью. + +Однажды я решил что хватит. + +--- + +## Почему не Jekyll + +Jekyll - зрелый инструмент с большим сообществом. Но у него есть фундаментальная проблема: он написан на Ruby. + +Звучит невинно. На практике это означает: + +**Версионный ад.** Ruby, Bundler, Gems - у каждого своя версия, и они регулярно конфликтуют друг с другом. Клонируешь репозиторий на новую машину - час уходит на то чтобы собрать рабочее окружение. Обновляешь Jekyll - ломаются плагины. Обновляешь плагины - ломается что-то ещё. + +**Зависимости ради зависимостей.** Простой блог тянет 1000+ gems, половина из которых устаревшая или находится в состоянии "поддерживается постольку-поскольку". Каждый `bundle install` - это лотерея. + +**Лишний мусор.** Jekyll генерирует страницы без ссылок, которые непонятно зачем существуют. Приходится явно прописывать что не генерировать. + +Hugo решает все эти проблемы радикально: это один бинарник на Go. Никаких зависимостей, никаких конфликтов версий, никакого `bundle install`. Скачал - работает. На любой машине, всегда. + +Бинарник весит ~50MB. Собирает сайт из 40+ страниц за полторы секунды. Jekyll на том же контенте думал заметно дольше. + +--- + +## Почему Blowfish + +Все просто: понравилась. + +Смотрел темы несколько часов. Blowfish выглядела именно так как я хотел - чисто, без лишнего, с хорошей типографикой. Взял её. + +Ставится как Git submodule - обновляется одной командой, не засоряет репозиторий. + +--- + +## Архитектура: два окружения + +У любого нормального инфраструктурного проекта есть тестовый и production контур. Блог - не исключение: обновления Hugo, изменения темы, новые статьи - всё это нужно проверять до того как это увидят читатели. + +``` + ┌────────────────────────────────────┐ + │ Gitea (внутренний) │ + │ репозиторий: blog │ + │ │ + │ ветка: main ветка: dev │ + └───────┬────────────────────┬───────┘ + │ │ + ▼ ▼ + ┌───────────────┐ ┌───────────────┐ + │ hugo-builder │ │ hugo-builder │ + │ prod │ │ dev │ + └───────┬───────┘ └───────┬───────┘ + │ │ + ▼ ▼ + ┌───────────────┐ ┌───────────────┐ + │ nginx │ │ nginx-dev │ + │ blog.ru │ │ dev.blog.ru │ + │ (публичный) │ │ (Basic Auth) │ + └───────────────┘ └───────────────┘ +``` + +**Production** (`blog.ru`) - публичный. Ветка `main`. + +**Development** (`dev.blog.ru`) - тестовый контур. Ветка `dev`. Закрыт Basic Auth через Traefik Middleware - без логина и пароля не войти. + +Оба окружения в одном K3s namespace. Один репозиторий, две ветки, два независимых пайплайна. + +--- + +## Шаг 1: Создаём Hugo проект + +```bash +# Создаём новый Hugo сайт +cd ~/projects +hugo new site blog +cd blog + +# Инициализируем Git репозиторий +git init +git remote add origin https://git.example.com/user/blog.git + +# Добавляем тему Blowfish как Git submodule +git submodule add -b main https://github.com/nunocoracao/blowfish.git themes/blowfish + +# Подтягиваем файлы submodule +git submodule update --init --recursive + +# Удаляем дефолтный конфиг Hugo +rm -f hugo.toml + +# Создаём структуру для конфигов +mkdir -p config/_default + +# Копируем примеры конфигов из темы +cp themes/blowfish/config/_default/*.toml config/_default/ +``` + +Структура после инициализации: + +``` +blog/ +├── content/ ← статьи в Markdown +├── static/ ← изображения, favicon +├── themes/ +│ └── blowfish/ ← тема (Git submodule) +└── config/ + └── _default/ + ├── hugo.toml + ├── languages.ru.toml + ├── menus.ru.toml + ├── markup.toml + └── params.toml +``` + +--- + +## Шаг 2: Минимальная конфигурация + +### hugo.toml + +```toml +baseURL = "https://blog.ru/" +theme = "blowfish" +defaultContentLanguage = "ru" +``` + +### languages.ru.toml + +```bash +# Переименовываем файл языка для русского +mv config/_default/languages.en.toml config/_default/languages.ru.toml +``` + +```toml +languageCode = "ru" +languageName = "Русский" +weight = 1 +title = "Мой Блог" + +[params] +displayName = "RU" +isoCode = "ru" +rtl = false +dateFormat = "2 January 2006" + +[params.author] +name = "Ваше Имя" +headline = "DevOps Engineer | Kubernetes | Homelab" +``` + +### menus.ru.toml + +```bash +# Переименовываем файл меню +mv config/_default/menus.en.toml config/_default/menus.ru.toml +``` + +```toml +[[main]] + name = "Блог" + pageRef = "posts" + weight = 10 + +[[main]] + name = "О сайте" + pageRef = "about" + weight = 20 +``` + +--- + +## Шаг 3: Первая статья + +```bash +# Создаём новую статью +hugo new content posts/hello-world/index.md +``` + +```markdown +--- +title: "Hello World" +date: 2026-02-13 +draft: false +description: "Первый пост на новом сайте" +tags: ["test"] +--- + +Это первый пост на blog.ru. +``` + +--- + +## Шаг 4: Проверяем локально + +```bash +# Запускаем локальный сервер Hugo +# -D: показывать черновики (draft: true) +# --bind 0.0.0.0: доступен с любого IP в локальной сети +hugo server -D --bind 0.0.0.0 +``` + +Открываем `http://192.168.11.10:1313/` (ваш IP) - сайт с Blowfish темой и первой статьёй. + +Hugo автоматически пересобирает сайт при сохранении файлов - изменения видны сразу после обновления страницы в браузере. + +--- + +## Шаг 5: Пушим в Gitea + +```bash +# Добавляем все файлы в Git +git add . + +# Создаём первый коммит +git commit -m "Initial commit: Hugo + Blowfish v2.98.0" + +# Переименовываем ветку в main (если по умолчанию master) +git branch -M main + +# Пушим в Gitea +git push -u origin main + +# Создаём ветку dev для development окружения +git checkout -b dev +git push -u origin dev + +# Возвращаемся на main +git checkout main +``` + +--- + +## Workflow: как я пишу статьи + +**Пишу (ветка dev):** + +```bash +# Переходим в папку проекта +cd ~/projects/blog + +# Переключаемся на ветку dev +git checkout dev + +# Запускаем локальный предпросмотр +hugo server -D --bind 0.0.0.0 + +# Создаём новую статью +hugo new content posts/название-статьи/index.md + +# Редактируем в любом редакторе, сохраняем, смотрим в браузере +# Когда готово - коммитим +git add . +git commit -m "feat: новая статья про X" + +# Пушим в dev ветку +git push origin dev +# → webhook срабатывает → автосборка → dev.blog.ru +``` + +**Проверяю:** + +Открываю `https://dev.blog.ru` (вводя логин/пароль Basic Auth) - вижу статью как её увидят читатели. + +**Публикую:** + +```bash +# Переключаемся на main +git checkout main + +# Мержим изменения из dev +git merge dev + +# Пушим в production +git push origin main +# → webhook срабатывает → автосборка → blog.ru +``` + +Золотое правило: **все изменения только через ветку `dev`**. В `main` - только через merge. Никогда не редактировать файлы находясь на `main`. + +Почему это важно - расскажу отдельно, когда дойдём до того как я нарушил это правило и что из этого вышло. + +--- + +## Что дальше + +Локально всё работает. Но `hugo server` умрёт как только закрою терминал. + +Нужно развернуть это в K3s: Hugo Builder который пересобирает сайт при каждом пуше, Nginx который раздаёт статику, SSL сертификаты, NFS хранилище. Об этом - в следующей части. + +--- + +**Стек этой части:** +- Hugo v0.155.3 extended +- Blowfish v2.98.0 +- Gitea (внутренний) +- Рабочая станция: Debian/Ubuntu diff --git a/content/posts/blog-part-2-k8s-deployment/featured.png b/content/posts/blog-part-2-k8s-deployment/featured.png new file mode 100644 index 0000000..53a9edb Binary files /dev/null and b/content/posts/blog-part-2-k8s-deployment/featured.png differ diff --git a/content/posts/blog-part-2-k8s-deployment/index.md b/content/posts/blog-part-2-k8s-deployment/index.md new file mode 100644 index 0000000..a4c5969 --- /dev/null +++ b/content/posts/blog-part-2-k8s-deployment/index.md @@ -0,0 +1,593 @@ +--- +title: "Блог на Hugo в K3s: часть 2 - деплой в кластер" +date: 2026-01-08 +draft: false +description: "NFS хранилище, Hugo Builder с webhook listener, Nginx с Prometheus exporter и автоматический SSL от Let's Encrypt. Полный деплой production окружения." +tags: ["hugo", "k3s", "kubernetes", "nfs", "traefik", "cert-manager"] +categories: ["infrastructure"] +series: ["Блог на Hugo в K3s"] +series_order: 2 +--- + +В первой части мы запустили Hugo локально. Сайт работает пока открыт терминал. Закрыл терминал - сайт умер. + +Пора переносить это в K3s. + +--- + +## Архитектура деплоя + +``` +Git Push + ↓ +Gitea (внутренний) + ↓ webhook POST +Hugo Builder + ├→ git clone + submodule + ├→ hugo --minify + └→ output → NFS + ↓ + /export/blog-public/ + ↓ + Nginx (x2 реплики) + ↓ + Traefik Ingress + ↓ + your-blog.ru (SSL) +``` + +Пять компонентов: +1. **NFS** - хранилище для статики (OpenMediaVault) +2. **Hugo Builder** - пересобирает сайт при каждом пуше +3. **Nginx** - раздаёт статику с NFS +4. **cert-manager** - автоматический SSL от Let's Encrypt +5. **Traefik IngressRoute** - маршрутизация с SSL терминацией + +--- + +## Шаг 1: NFS хранилище + +Hugo собирает статику в HTML/CSS/JS файлы. Nginx раздаёт эти файлы. Значит нужно общее хранилище куда Hugo пишет, а Nginx читает. + +NFS - самый простой вариант для homelab. У меня OpenMediaVault на отдельной машине. + +### Создаём директории на NAS + +```bash +# Подключаемся к NAS (SSH на нестандартном порту для безопасности) +ssh -p 33322 nasadmin@192.168.11.30 + +# Создаём папки для production и development окружений +sudo mkdir -p /srv/storage/blog/blog-public +sudo mkdir -p /srv/storage/blog/blog-public-dev + +# Выдаём права на запись (контейнеры пишут от root) +sudo chmod -R 775 /srv/storage/blog/ +``` + +**Почему SSH на порту 33322?** Стандартный порт 22 - первая цель сканеров и ботов. Нестандартный порт снижает шум в логах и количество brute-force попыток до нуля. Безопасность через скрытность работает для домашних серверов. + +### Настраиваем NFS через OMV Web UI + +Storage → Shared Folders → Create: +- Name: `blog-public` +- Device: основной диск +- Path: `/blog/blog-public` + +Services → NFS → Shares → Create: +- Shared folder: `blog-public` +- Client: `192.168.11.0/24` +- Privilege: Read/Write +- Extra options: `rw,sync,no_subtree_check,no_root_squash` + +То же для `blog-public-dev`. + +**Критично:** `no_root_squash` - без этого контейнеры не смогут записывать файлы (они пишут от root внутри контейнера). + +### Проверяем экспорт + +```bash +# Заходим на NAS +ssh -p 33322 nasadmin@192.168.11.30 + +# Проверяем что NFS экспортирует наши шары +sudo exportfs -v | grep blog + +# Ожидаемый вывод - две строки с настройками экспорта: +# /export/blog-public 192.168.11.0/24(rw,sync,no_root_squash,...) +# /export/blog-public-dev 192.168.11.0/24(rw,sync,no_root_squash,...) +``` + +--- + +## Шаг 2: PersistentVolumes в K3s + +K3s нужно сказать где лежат NFS шары. Создаём манифест с PersistentVolume ресурсами. + +**Файл:** `02-pv.yaml` + +```yaml +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: blog-public-pv +spec: + capacity: + storage: 5Gi + accessModes: + - ReadWriteMany + nfs: + server: 192.168.11.30 # IP вашего NAS + path: /export/blog-public + mountOptions: + - nfsvers=3 + - hard + +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: blog-public-dev-pv +spec: + capacity: + storage: 5Gi + accessModes: + - ReadWriteMany + nfs: + server: 192.168.11.30 + path: /export/blog-public-dev + mountOptions: + - nfsvers=3 + - hard +``` + +**Почему NFSv3, а не NFSv4?** Потому что NFSv4.2 в K3s не работал - поды виснут в `ContainerCreating` с ошибкой `mount.nfs: No such file or directory`. NFSv3 работает стабильно. Не надо усложнять то что работает. + +```bash +# Применяем манифест +kubectl apply -f 02-pv.yaml + +# Проверяем что PV создались и привязались +kubectl get pv | grep blog +# blog-public-pv 5Gi RWX Bound blog/blog-public-pvc +``` + +--- + +## Шаг 3: Hugo Builder + +Нужен контейнер который слушает webhook от Gitea, клонирует репозиторий и собирает Hugo. + +### Зачем нужен Hugo Builder? + +**Проблема:** Hugo генерирует статику командой `hugo`. Где её запускать? На локальной машине? Тогда нужно вручную заливать файлы на сервер после каждого изменения. Неудобно и ломает автоматизацию. + +**Решение:** Контейнер который живёт в K3s, слушает webhook от Gitea и автоматически пересобирает сайт при каждом `git push`. + +### Dockerfile + +```dockerfile +FROM alpine:3.19 + +# Устанавливаем всё что нужно Hugo и Git +RUN apk add --no-cache \ + git nodejs npm bash curl wget \ + libc6-compat libstdc++ ca-certificates + +# Скачиваем Hugo Extended v0.155.3 +WORKDIR /tmp +RUN wget https://github.com/gohugoio/hugo/releases/download/v0.155.3/hugo_extended_0.155.3_linux-amd64.tar.gz && \ + tar -xzf hugo_extended_0.155.3_linux-amd64.tar.gz && \ + cp hugo /usr/bin/hugo && \ + chmod +x /usr/bin/hugo && \ + rm -rf /tmp/* + +WORKDIR /workspace + +# Копируем скрипты +COPY webhook-listener.sh /usr/local/bin/ +COPY build.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/*.sh + +EXPOSE 8080 +CMD ["/usr/local/bin/webhook-listener.sh"] +``` + +### build.sh - скрипт сборки Hugo + +**Зачем:** Отдельный скрипт сборки нужен чтобы его можно было запускать не только из webhook listener, но и вручную для тестирования. Один скрипт - одна ответственность. + +```bash +#!/bin/bash +set -e # Остановиться при первой ошибке + +export GIT_TERMINAL_PROMPT=0 # Не запрашивать пароли интерактивно + +REPO_URL="https://git.example.com/user/blog.git" # URL вашего Gitea репозитория +BRANCH="${BRANCH:-main}" # Ветка (передаётся через env) +OUTPUT_DIR="/mnt/blog-public" # Куда складывать собранную статику (NFS) +WORK_DIR="/tmp/build" # Временная папка для клонирования + +# Чистим рабочую директорию от прошлой сборки +rm -rf ${WORK_DIR} +mkdir -p ${WORK_DIR} + +# Клонируем репозиторий (только нужную ветку, без истории) +cd ${WORK_DIR} +git clone --branch ${BRANCH} --depth 1 ${REPO_URL} site 2>&1 +cd site + +# Подтягиваем тему Blowfish как Git submodule +git submodule update --init --recursive --depth 1 2>&1 + +# Собираем сайт (минифицируем CSS/JS/HTML) +hugo --minify --destination ${OUTPUT_DIR} 2>&1 + +# Проверяем что сборка прошла успешно +if [ -f "${OUTPUT_DIR}/index.html" ]; then + echo "Build successful!" +else + echo "Build failed - index.html not found" + exit 1 +fi + +# Убираем за собой +rm -rf ${WORK_DIR} +``` + +### webhook-listener.sh - слушатель webhook + +**Зачем:** Gitea отправляет HTTP POST запрос при каждом `git push`. Нужен простой HTTP сервер который принимает этот запрос и запускает сборку. netcat - самый простой способ поднять HTTP listener без зависимостей. + +```bash +#!/bin/bash +set -e + +echo "Starting webhook listener on port 8080..." + +while true; do + # Принимаем HTTP запрос через netcat и сразу отвечаем 200 OK + echo -e "HTTP/1.1 200 OK\r\n\r\nWebhook received" | nc -l -p 8080 + + # Запускаем сборку синхронно (чтобы видеть логи в kubectl logs) + echo "$(date): Webhook triggered, starting build..." + /usr/local/bin/build.sh + echo "$(date): Build completed, waiting for next webhook..." +done +``` + +### Сборка и деплой образа + +```bash +# Собираем Docker образ +docker build -t hugo-builder:latest . + +# Сохраняем в tar файл +docker save hugo-builder:latest -o /tmp/hugo-builder.tar + +# Копируем на все K3s worker ноды +for ip in 210 211; do + scp /tmp/hugo-builder.tar k3s@192.168.11.$ip:/tmp/ + + # Импортируем образ в containerd K3s + ssh k3s@192.168.11.$ip "sudo k3s ctr images import /tmp/hugo-builder.tar && rm /tmp/hugo-builder.tar" +done +``` + +### Deployment и Service + +```yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hugo-builder-prod + namespace: blog +spec: + replicas: 1 + selector: + matchLabels: + app: hugo-builder-prod + template: + metadata: + labels: + app: hugo-builder-prod + spec: + containers: + - name: hugo-builder + image: hugo-builder:latest + imagePullPolicy: Never # Образ локальный, не тянуть из registry + env: + - name: BRANCH + value: "main" # Для prod используем main ветку + volumeMounts: + - name: public + mountPath: /mnt/blog-public # NFS хранилище + resources: + requests: + cpu: 200m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + volumes: + - name: public + persistentVolumeClaim: + claimName: blog-public-pvc + +--- +apiVersion: v1 +kind: Service +metadata: + name: hugo-builder-prod + namespace: blog +spec: + selector: + app: hugo-builder-prod + ports: + - port: 8080 + targetPort: 8080 + name: webhook +``` + +```bash +# Применяем манифест +kubectl apply -f 01-hugo-builder-prod.yaml + +# Проверяем что под запустился +kubectl get pods -n blog | grep hugo-builder + +# Смотрим логи - должна быть строка "Starting webhook listener" +kubectl logs -n blog deployment/hugo-builder-prod +``` + +--- + +## Шаг 4: Nginx с Prometheus exporter + +Nginx раздаёт статику с того же NFS где Hugo её собрал. Две реплики для минимальной доступности при обновлениях. + +Бонус: sidecar контейнер с nginx-prometheus-exporter для мониторинга через Grafana. + +```yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + namespace: blog +spec: + replicas: 2 # Две реплики для доступности + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + # Основной контейнер - Nginx + - name: nginx + image: nginx:1.25-alpine + ports: + - containerPort: 80 + volumeMounts: + - name: html + mountPath: /usr/share/nginx/html + readOnly: true # Nginx только читает, не пишет + - name: config + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + resources: + requests: + cpu: 50m + memory: 64Mi + + # Sidecar - экспортер метрик для Prometheus + - name: nginx-exporter + image: nginx/nginx-prometheus-exporter:1.1.0 + args: + - -nginx.scrape-uri=http://localhost/nginx_status + ports: + - containerPort: 9113 + name: metrics + resources: + requests: + cpu: 10m + memory: 16Mi + + volumes: + - name: html + persistentVolumeClaim: + claimName: blog-public-pvc # NFS хранилище + - name: config + configMap: + name: nginx-config + +--- +apiVersion: v1 +kind: Service +metadata: + name: nginx + namespace: blog +spec: + selector: + app: nginx + ports: + - port: 80 + targetPort: 80 + name: http + - port: 9113 + targetPort: 9113 + name: metrics # Для Prometheus +``` + +--- + +## Шаг 5: SSL сертификаты + +cert-manager автоматически получает сертификаты от Let's Encrypt через HTTP-01 challenge. + +**Важно:** Сначала настрой A-запись у DNS провайдера: +``` +your-blog.ru A 77.37.XXX.XXX (ваш внешний IP) +www.your-blog.ru A 77.37.XXX.XXX +``` + +Без этого Let's Encrypt не сможет проверить что домен принадлежит вам. + +```yaml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: blog-tls + namespace: blog +spec: + secretName: blog-tls + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + dnsNames: + - your-blog.ru + - www.your-blog.ru +``` + +```bash +# Применяем манифест +kubectl apply -f 04-certificate.yaml + +# Ждём 30-60 секунд пока cert-manager получит сертификат +kubectl get certificate -n blog + +# Должно быть READY=True +# NAME READY SECRET AGE +# blog-tls True blog-tls 45s +``` + +--- + +## Шаг 6: IngressRoute через Traefik + +Traefik маршрутизирует трафик на Nginx и делает SSL терминацию. + +```yaml +--- +# HTTP → HTTPS редирект (опционально) +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: blog-http + namespace: blog +spec: + entryPoints: + - web # Порт 80 + routes: + - match: Host(`your-blog.ru`) || Host(`www.your-blog.ru`) + kind: Rule + services: + - name: nginx + port: 80 + +--- +# HTTPS с SSL +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: blog-https + namespace: blog +spec: + entryPoints: + - websecure # Порт 443 + routes: + - match: Host(`your-blog.ru`) || Host(`www.your-blog.ru`) + kind: Rule + services: + - name: nginx + port: 80 + tls: + secretName: blog-tls # Сертификат от cert-manager +``` + +```bash +# Применяем манифест +kubectl apply -f 05-ingressroute.yaml + +# Проверяем что сайт доступен +curl -I https://your-blog.ru +# HTTP/2 200 +``` + +--- + +## Шаг 7: Webhook в Gitea + +Последний шаг - связать Gitea с Hugo Builder. + +Gitea → ваш репозиторий → Settings → Webhooks → Add Webhook → Gitea + +- **URL:** `http://hugo-builder-prod.blog.svc.cluster.local:8080` +- **HTTP Method:** POST +- **Content Type:** application/json +- **Trigger On:** Push events +- **Branch filter:** `main` + +Нажимаем "Test Delivery" - должен вернуть `200 OK`. + +Проверяем логи Hugo Builder: + +```bash +# Следим за логами в реальном времени +kubectl logs -n blog deployment/hugo-builder-prod -f + +# Должно появиться: +# Webhook triggered, starting build... +# Cloning repository... +# Initializing submodules... +# Building Hugo site... +# Build successful! +``` + +--- + +## Проверка работы + +```bash +# Меняем статью +cd ~/hugo-projects/blog +git checkout main +echo "## Тестовая правка" >> content/posts/hello-world/index.md + +# Коммитим и пушим +git add . +git commit -m "test: проверка автосборки" +git push origin main + +# Следим за логами Hugo Builder +kubectl logs -n blog deployment/hugo-builder-prod -f + +# Через 5-7 секунд сборка завершится +# Проверяем что изменение попало на сайт +curl -s https://your-blog.ru/posts/hello-world/ | grep "Тестовая правка" +``` + +Если видите "Тестовая правка" - всё работает. Каждый `git push` автоматически обновляет сайт. + +--- + +## Что дальше + +Production окружение развёрнуто. Но пока только для ветки `main`. + +В следующей части добавим development окружение с отдельным Hugo Builder, Nginx и защитой через Basic Auth. Два независимых пайплайна в одном namespace. + +--- + +**Стек этой части:** +- K3s 1.30 +- NFS на OpenMediaVault +- Hugo Builder (Alpine + Hugo v0.155.3) +- Nginx 1.25 + Prometheus exporter +- cert-manager + Let's Encrypt +- Traefik IngressRoute diff --git a/content/posts/blog-part-3-dev-environment/featured.png b/content/posts/blog-part-3-dev-environment/featured.png new file mode 100644 index 0000000..c923896 Binary files /dev/null and b/content/posts/blog-part-3-dev-environment/featured.png differ diff --git a/content/posts/blog-part-3-dev-environment/index.md b/content/posts/blog-part-3-dev-environment/index.md new file mode 100644 index 0000000..86fa102 --- /dev/null +++ b/content/posts/blog-part-3-dev-environment/index.md @@ -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) diff --git a/content/posts/blog-part-4-git-workflow/featured.png b/content/posts/blog-part-4-git-workflow/featured.png new file mode 100644 index 0000000..17e6599 Binary files /dev/null and b/content/posts/blog-part-4-git-workflow/featured.png differ diff --git a/content/posts/blog-part-4-git-workflow/index.md b/content/posts/blog-part-4-git-workflow/index.md new file mode 100644 index 0000000..c5d5436 --- /dev/null +++ b/content/posts/blog-part-4-git-workflow/index.md @@ -0,0 +1,417 @@ +--- +title: "Блог на Hugo в K3s: часть 4 - выбор Git workflow" +date: 2026-02-16 +draft: false +description: "Две папки или одна с переключением веток? Разбираем варианты организации работы с dev и production окружениями на практических примерах." +tags: ["git", "workflow", "devops", "hugo"] +categories: ["infrastructure"] +series: ["Блог на Hugo в K3s"] +series_order: 4 +--- + +В части 3 мы развернули два окружения - production и development. Один репозиторий, две ветки (`main` и `dev`), два пайплайна. + +Теперь встаёт вопрос: **как организовать работу локально?** + +--- + +## Проблема + +У нас есть: +- Репозиторий в Gitea +- Две ветки: `main` (production) и `dev` (development) +- Необходимость постоянно переключаться между ними + +Как это делать на локальной машине? Два варианта. + +--- + +## Вариант А: Две отдельные папки + +Клонируем репозиторий дважды - в разные папки: + +``` +~/projects/ +├── blog/ ← ветка main (production) +└── blog-dev/ ← ветка dev (development) +``` + +**Логика:** Хочу работать с dev - иду в `blog-dev`. Хочу что-то проверить в production - иду в `blog`. Без переключения веток. + +### Кажущиеся преимущества + +**Параллельная работа.** Можно держать открытыми два терминала - в одном `hugo server` для dev, в другом смотреть production код. + +**Изоляция.** Каждая папка - своя песочница. Изменения в одной не влияют на другую. + +**Простота навигации.** `cd blog-dev` вместо `git checkout dev`. Меньше команд. + +**Привычный паттерн.** Многие админы и разработчики держат несколько клонов для разных задач. + +### Реальные проблемы + +#### Проблема 1: Рассинхронизация локальных ветокin + +Работаю в `blog-dev` - пишу статьи, коммичу, пушу в `origin/dev`. Всё хорошо. + +Но локальная ветка `dev` в папке `blog` при этом **не обновляется**. Она отстаёт от `origin/dev`. + +Приходишь делать merge: + +```bash +cd blog +git checkout main +git merge dev +# Already up to date. ← НО есть НЮАНС! +``` + +Git говорит "всё актуально", имея в виду **локальную** ветку `dev`, которая отстала на три коммита. Статьи не попадают в production. + +Приходится помнить делать `git pull origin dev` перед каждым merge. Забыл - публикуешь устаревшую версию. + +#### Проблема 2: Конфликты при merge + +Редактируешь `config/params.toml` в обеих папках независимо: +- В `blog-dev` добавил Firebase конфиг +- В `blog` изменил название сайта + +При merge Git честно сообщает о конфликте: + +``` +CONFLICT (content): Merge conflict in config/params.toml +``` + +И это повторяется **каждый раз** когда трогаешь конфигурацию. Потому что две папки - это две независимые истории изменений одного файла. + +#### Проблема 3: Работа не в той папке + +Несколько раз ловил себя на том что редактирую статьи прямо в `blog` - папке production. Это нарушает весь смысл раздельных окружений. + +#### Проблема 4: Умственная нагрузка + +Постоянный вопрос "в какой папке я сейчас?" Для простого блога это лишняя когнитивная нагрузка. + +--- + +## Вариант Б: Одна папка с переключением веток + +Один клон репозитория, работа через `git checkout`: + +``` +~/projects/ +└── blog/ ← одна папка, две ветки: main и dev +``` + +### Как это работает + +**Пишу статью:** + +```bash +cd ~/projects/blog + +# Переключаюсь на dev +git checkout dev + +# Проверяю что dev актуален +git pull origin dev + +# Запускаю локальный сервер +hugo server -D --bind 0.0.0.0 + +# Создаю статью +hugo new content posts/название/index.md + +# Коммичу и пушу +git add . +git commit -m "feat: новая статья" +git push origin dev +``` + +**Публикую:** + +```bash +# Убеждаюсь что dev актуален +git checkout dev +git pull origin dev + +# Переключаюсь на main и мержу +git checkout main +git pull origin main +git merge dev +git push origin main +``` + +### Реальные преимущества + +**Никакой рассинхронизации.** Все ветки в одном репозитории. `git pull` обновляет всё что нужно. + +**Нет конфликтов из-за независимых изменений.** Когда работаешь в одной папке, `params.toml` существует в одном экземпляре. Все изменения делаются в `dev`, в `main` попадают только через merge. + +Конфликт возможен только если кто-то редактирует `main` напрямую - а это нарушение workflow. + +**Невозможно ошибиться с веткой.** `git branch` показывает где ты сейчас. Случайно отредактировать файлы в `main` - сложнее. + +**Меньше места на диске.** Один клон вместо двух. Один `.git` вместо двух. + +--- + +## Сравнение на практических примерах + +### Пример 1: Обновление темы Blowfish + +**Две папки:** +```bash +cd blog-dev +git submodule update --remote themes/blowfish +git add themes/blowfish +git commit -m "update: Blowfish theme" +git push origin dev + +# Проверяешь на dev.blog.ru +# Если всё ок - мержишь +cd ../blog +git checkout dev +git pull origin dev # ← ЛЕГКО ЗАБЫТЬ +git checkout main +git merge dev +git push origin main +``` + +**Одна папка:** +```bash +cd blog +git checkout dev +git pull origin dev +git submodule update --remote themes/blowfish +git add themes/blowfish +git commit -m "update: Blowfish theme" +git push origin dev + +# Проверяешь на dev.blog.ru +# Если всё ок - мержишь +git checkout main +git pull origin main +git merge dev +git push origin main +``` + +Меньше команд, меньше переходов между папками, меньше шансов забыть `git pull`. + +### Пример 2: Правка опечатки в production + +Нашёл опечатку на `blog.ru`. Нужно исправить быстро. + +**Две папки:** + +Опасность: хочется исправить прямо в `blog` (ветка `main`). Это нарушает workflow - все изменения должны идти через `dev`. + +Правильно: +```bash +cd blog-dev +git checkout dev +# Исправляешь +git commit -m "fix: опечатка" +git push origin dev +cd ../blog +git checkout main +git pull origin dev # ← опять легко забыть +git merge dev +git push origin main +``` + +**Одна папка:** + +```bash +cd blog +git checkout dev +git pull origin dev +# Исправляешь +git commit -m "fix: опечатка" +git push origin dev +git checkout main +git merge dev +git push origin main +``` + +Проще, меньше команд, понятнее. + +### Пример 3: Долгая работа над статьёй + +Пишешь большую статью несколько дней. Между сеансами работы кто-то (или ты сам) запушил другие изменения в `dev`. + +**Две папки:** + +```bash +cd blog-dev +# День 1: пишешь +git add . +git commit -m "wip: статья" + +# День 2: продолжаешь +git pull origin dev # Подтягиваешь чужие изменения +# Пишешь дальше +git add . +git commit -m "feat: закончил статью" +git push origin dev +``` + +Всё так же как и с одной папкой. Разницы нет. + +**Одна папка:** + +```bash +cd blog +git checkout dev + +# День 1: пишешь +git add . +git commit -m "wip: статья" + +# День 2: продолжаешь +git pull origin dev # Подтягиваешь чужие изменения +# Пишешь дальше +git add . +git commit -m "feat: закончил статью" +git push origin dev +``` + +Идентично. Этот пример работает одинаково в обоих вариантах. + +--- + +## Что выбрать? + +**Если ты только начинаешь - сразу делай одну папку.** + +Две папки кажутся удобными, но создают проблемы которые регулярно прерывают работу: +- Рассинхронизация веток +- Конфликты при merge +- Когнитивная нагрузка + +Одна папка с переключением веток - стандартный Git workflow, проверенный миллионами разработчиков. Требует чуть больше дисциплины (`git checkout dev` вместо `cd blog-dev`), но избавляет от всех проблем выше. + +**Золотое правило:** Никогда не редактировать файлы находясь на ветке `main`. Все изменения - через `dev`. Всегда. + +--- + +## Миграция: если начал с двух папок + +Если уже работаешь в двух папках - переход простой. + +### Шаг 1: Убеждаемся что всё запушено + +```bash +# Проверяем обе папки +cd ~/projects/blog-dev +git status +git push origin dev + +cd ~/projects/blog +git status +git push origin main +``` + +### Шаг 2: Синхронизируем ветку dev в основной папке + +```bash +cd ~/projects/blog + +# Обновляем локальную ветку dev из remote +git checkout dev +git pull origin dev + +# Проверяем что всё актуально +git log --oneline -5 + +# Возвращаемся на main +git checkout main +``` + +### Шаг 3: Удаляем вторую папку + +```bash +# Убеждаемся что в blog-dev нет несохранённых изменений +cd ~/projects/blog-dev +git status +# Должно быть: nothing to commit, working tree clean + +# Удаляем папку +cd ~/projects +rm -rf blog-dev +``` + +### Шаг 4: Проверяем что всё работает + +```bash +cd ~/projects/blog + +# Переключаемся на dev и запускаем сервер +git checkout dev +hugo server -D --bind 0.0.0.0 + +# Открываем http://localhost:1313/ +# Видим dev версию сайта +``` + +--- + +## Новый workflow: шпаргалка + +### Создаю статью + +```bash +cd ~/projects/blog + +git checkout dev +hugo new content posts/название/index.md + +# Пишу, сохраняю, проверяю в hugo server +git add . +git commit -m "feat: название статьи" +git push origin dev # → dev.blog.ru +``` + +### Публикую статью + +```bash +git checkout dev +git pull origin dev # Убеждаюсь что dev актуален + +git checkout main +git pull origin main # Убеждаюсь что main актуален +git merge dev +git push origin main # → blog.ru +``` + +### Меняю конфигурацию + +```bash +git checkout dev # ВСЕ изменения только через dev! +nano config/params.toml +git add . +git commit -m "feat: изменил конфиг" +git push origin dev # Проверяю на dev.blog.ru + +# Если всё ок +git checkout main +git merge dev +git push origin main +``` + +--- + +## Что дальше + +Workflow выбран, окружения работают. Можно писать статьи. + +Но есть ещё одна тема которую стоит разобрать - что делать когда что-то сломалось. Как диагностировать проблемы когда сайт вдруг начал отдавать 503, или SSL перестал работать, или webhook не срабатывает. + +В следующей части покажу процесс диагностики на реальном примере - как я чинил `blog.ru` когда он внезапно стал недоступен из интернета. + +--- + +**Рекомендация этой части:** +- Одна папка `~/projects/blog` +- Переключение веток через `git checkout` +- Все изменения через `dev` → merge в `main` +- Никогда не редактировать находясь на `main` diff --git a/content/posts/blog-part-5-debugging/featured.png b/content/posts/blog-part-5-debugging/featured.png new file mode 100644 index 0000000..0231c89 Binary files /dev/null and b/content/posts/blog-part-5-debugging/featured.png differ diff --git a/content/posts/blog-part-5-debugging/index.md b/content/posts/blog-part-5-debugging/index.md new file mode 100644 index 0000000..2291782 --- /dev/null +++ b/content/posts/blog-part-5-debugging/index.md @@ -0,0 +1,634 @@ +--- +title: "Блог на Hugo в K3s: часть 5 - что делать когда всё внезапно сломалось" +date: 2026-02-17 +draft: false +description: "Сайт работал вчера, а сегодня 503. Алгоритм диагностики Kubernetes проблем за 5 минут - от DNS до пода, без паники и танцев с бубном." +tags: ["kubernetes", "k3s", "traefik", "debugging", "nginx", "troubleshooting"] +categories: ["infrastructure"] +series: ["Блог на Hugo в K3s"] +series_order: 5 +--- + +В части 4 мы разобрались с Git workflow. Всё работает: пушишь в `dev` - видишь на тестовом окружении, мержишь в `main` - публикуется на production. + +А потом в один прекрасный день открываешь свой сайт и видишь `503 Service Temporarily Unavailable`. + +Вчера же все работало! Ты ничего не менял. Что произошло? + +Добро пожаловать в мир эксплуатации Kubernetes, где проблемы тоже возникают и требуют системного подхода без паники. + +Эта статья - алгоритм диагностики от DNS до пода. Проходишь по шагам сверху вниз, находишь проблему за 5 минут. Не гадаешь, не тыкаешь наугад - работаешь по системе. + +--- + +## Анатомия HTTP запроса в K3s + +Прежде чем искать проблему, нужно понять путь запроса от браузера до nginx: + +``` +Браузер + ↓ DNS запрос +DNS сервер (провайдер или Cloudflare) + ↓ Возвращает IP адрес +Роутер/Файрвол (OPNsense, MikroTik) + ↓ Port Forward 443 → K3s node +MetalLB LoadBalancer + ↓ External IP +Traefik Ingress Controller + ↓ IngressRoute matching +Kubernetes Service + ↓ Endpoint selection +Pod (Nginx контейнер) + ↓ Volume mount +NFS хранилище +``` + +Проблема может быть на любом из этих уровней. Секрет эффективной диагностики - проверять снаружи внутрь, последовательно исключая рабочие компоненты. + +Когда тыкаешь наугад, проверяя сначала поды, потом DNS, потом снова поды - тратишь время. Когда идёшь по алгоритму - находишь проблему за минуты. + +--- + +## Шаг 1: DNS - доходит ли домен до твоего IP + +Первым делом проверяем что домен резолвится в правильный IP. Без этого дальше проверять бессмысленно - браузер просто не знает куда направлять запрос. + +### Проверка + +```bash +# Проверяем DNS резолв (используй свой домен) +dig blog.example.com +short + +# Альтернатива если dig не установлен +nslookup blog.example.com +``` + +### Ожидаемый результат + +``` +77.37.XXX.XXX +``` + +Должен вернуться твой **публичный IP адрес** (тот который прописан в A-записи у DNS провайдера). + +### Что может пойти не так + +| Симптом | Причина | Как проверить | Решение | +|---------|---------|---------------|---------| +| Возвращается старый IP | DNS кеш не обновился | `dig blog.example.com @8.8.8.8` | Подожди TTL (обычно 300-3600 сек) | +| `NXDOMAIN` ошибка | Домен не делегирован | Проверь NS записи у регистратора | Настрой DNS правильно | +| Возвращается `127.0.0.1` | Локальный override | `cat /etc/hosts \| grep blog` | Удали строку из /etc/hosts | +| Возвращается несколько IP | Round-robin DNS | Проверь все ли IP твои | Удали лишние A-записи | + +Если DNS правильный - идём дальше. + +--- + +## Шаг 2: Внешний доступ - доходит ли запрос до сервера + +Теперь проверяем что запрос физически доходит до сервера. DNS может быть правильным, но файрвол может блокировать трафик. + +### Проверка + +```bash +# Пробуем подключиться извне (важно - НЕ из локальной сети!) +curl -v https://blog.example.com 2>&1 | head -30 +``` + +**Важно:** Запускай эту команду с **внешнего** сервера или используй мобильный интернет. Тест из локальной сети ничего не докажет - можешь обходить файрвол. + +### Ситуация А: Connection refused или timeout + +``` +curl: (7) Failed to connect to blog.example.com port 443: Connection refused +``` + +Запрос вообще не дошёл до сервера. Проблема на сетевом уровне. + +**Возможные причины:** + +**1. Порт 443 закрыт на файрволе/роутере** + +Проверь Port Forward правила на OPNsense/MikroTik. Должно быть: +``` +WAN:443 → 192.168.X.X:443 (IP любой K3s ноды) +``` + +**2. MetalLB не назначил External IP для Traefik** + +```bash +# Проверяем MetalLB +kubectl get svc -n traefik traefik + +# Ожидаемый результат +NAME TYPE EXTERNAL-IP PORT(S) +traefik LoadBalancer 192.168.X.X 80:30080/TCP,443:30443/TCP +``` + +Если `EXTERNAL-IP` показывает `` - MetalLB не работает или пул IP адресов не настроен. + +**3. Traefik под не запущен** + +```bash +# Проверяем что Traefik работает +kubectl get pods -n traefik + +# Должны быть все Running +NAME READY STATUS +traefik-xxxxxxxxxx-xxxxx 1/1 Running +``` + +### Ситуация Б: TLS handshake прошёл, но 503 + +``` +< HTTP/2 503 +< content-type: text/plain; charset=utf-8 +< content-length: 20 +no available server +``` + +Отлично - наша ситуация! Traefik работает, SSL сертификат отдаёт, но дальше запрос упирается в стену. + +Сообщение `no available server` означает что Traefik **нашёл роутер**, но **не нашёл живой бэкенд** за ним. + +Проблема внутри кластера. Идём глубже. + +### Ситуация В: SSL certificate problem + +``` +curl: (60) SSL certificate problem: unable to get local issuer certificate +``` + +Сертификат невалидный или не выпущен. + +```bash +# Проверяем Certificate объект (используй свой namespace) +kubectl get certificate -n blog + +# Должно быть READY=True +NAME READY SECRET AGE +blog-tls True blog-tls 2d +``` + +Если `READY=False` - cert-manager не смог выпустить сертификат. + +```bash +# Смотрим что пошло не так +kubectl describe certificate blog-tls -n blog + +# Ищем секцию Events внизу вывода - там описание проблемы +``` + +### Главное: Запрос доходит до Traefik + +```bash +# Проверка (с ВНЕШНЕГО сервера!) +curl -I https://blog.example.com + +# Ожидаемый результат (любой из двух) +HTTP/2 200 # Всё работает +HTTP/2 503 # Traefik работает, но бэкенд недоступен + +# Если connection refused/timeout - проблема в сети (см. выше) +``` + +--- + +## Шаг 3: Traefik - правильно ли маршрутизируется трафик + +Traefik получил запрос на твой домен. Что он с ним делает? Смотрим логи. + +### Проверка логов Traefik + +```bash +# Смотрим последние 50 строк логов Traefik +kubectl logs -n traefik deployment/traefik --tail=50 + +# Фильтруем только свой домен (убираем шум от других сервисов) +kubectl logs -n traefik deployment/traefik --tail=100 | grep blog.example +``` + +### Что искать в логах + +**Нормальный запрос:** + +```json +{ + "request": "GET / HTTP/2.0", + "status": 200, + "size": 8994, + "router": "blog-blog-https-xxxxx@kubernetescrd", + "service": "blog-nginx-blog@kubernetescrd", + "backend": "http://10.42.2.40:80", + "duration": 12 +} +``` + +Ключевые поля: +- **router:** Traefik нашёл нужный IngressRoute (`blog-blog-https`) +- **backend:** IP пода nginx куда проксируется запрос (`10.42.2.40:80`) +- **status:** HTTP код ответа от nginx (`200` = всё хорошо) + +**Проблемный запрос:** + +```json +{ + "request": "GET / HTTP/2.0", + "status": 503, + "router": "blog-blog-https-xxxxx@kubernetescrd", + "error": "no available server" +} +``` + +Traefik нашёл роутер, но поле `backend` отсутствует - под недоступен или не существует. + +### Проверяем список IngressRoute + +```bash +# Смотрим все IngressRoute в кластере +kubectl get ingressroute -A + +# Фильтруем только свой домен +kubectl get ingressroute -A | grep blog.example +``` + +**Важный момент:** Если один и тот же домен прописан в **двух разных IngressRoute** из разных namespace - Traefik будет балансировать между ними. + +Например: + +```bash +NAMESPACE NAME AGE +blog blog-https 10d # СТАРЫЙ namespace +blog-new blog-https 2d # НОВЫЙ namespace +``` + +Оба IngressRoute имеют `match: Host('blog.example.com')`. Traefik видит оба, честно балансирует трафик 50/50. + +Если один из бэкендов мёртв - половина запросов уходит в пустоту. 503 через раз. + +**Решение:** Удалить старый IngressRoute: + +```bash +# Удаляем дубль из старого namespace +kubectl delete ingressroute blog-https blog-http -n blog +``` + +### Проверяем синтаксис match + +Traefik очень требователен к синтаксису. Частая ошибка - забыть backticks или скобки. + +**Неправильно:** +```yaml +match: Host(blog.example.com) # Нет backticks +match: Host `blog.example.com` # Нет скобок вокруг Host +match: Host("blog.example.com") # Двойные кавычки вместо backticks +``` + +**Правильно:** +```yaml +match: Host(`blog.example.com`) +``` + +Проверяем: + +```bash +# Смотрим манифест IngressRoute +kubectl get ingressroute blog-https -n blog -o yaml | grep match: + +# Должно быть со скобками и backticks +match: Host(`blog.example.com`) +``` + +### Главное: Traefik нашёл роутер + +```bash +# Проверка +kubectl logs -n traefik deployment/traefik --tail=50 | grep blog.example + +# Ожидаемый результат - есть строки с "router": "blog-blog-https" +# Если router не найден - проблема в IngressRoute match синтаксисе +``` + +--- + +## Шаг 4: Service - видит ли он поды + +Traefik нашёл роутер, проксирует трафик на Service. Но Service может не видеть поды если selector неправильный. + +### Проверка endpoints + +```bash +# Смотрим endpoints для Service (используй своё имя Service) +kubectl get endpoints nginx -n blog + +# Ожидаемый результат - НЕ пустой список IP +NAME ENDPOINTS +nginx 10.42.0.44:80,10.42.2.40:80 +``` + +Если видишь `` - Service не нашёл ни одного пода. Две возможные причины. + +### Причина 1: Selector не совпадает с labels + +```bash +# Смотрим selector у Service +kubectl get svc nginx -n blog -o yaml | grep -A3 "selector:" + +# Вывод +selector: + app: nginx + +# Смотрим labels у подов +kubectl get pods -n blog --show-labels | grep nginx + +# Вывод +nginx-xxxxxxxxxx-xxxxx 1/1 Running app=nginx-old +``` + +Видишь проблему? Service ищет `app: nginx`, а под помечен `app: nginx-old`. Не совпадает. + +**Решение:** Исправить Deployment или Service чтобы labels совпадали. + +### Причина 2: Поды не Running + +```bash +# Смотрим статус подов +kubectl get pods -n blog + +# Видим +NAME READY STATUS +nginx-xxxxxxxxxx-xxxxx 0/1 CreateContainerError +``` + +Под существует, но не работает. Service правильно не включает его в endpoints. Идём в следующий шаг - разбираемся почему под не запускается. + +### Главное: Service видит поды + +```bash +# Проверка +kubectl get endpoints nginx -n blog + +# Ожидаемый результат - НЕ пустой +nginx 10.42.0.44:80,10.42.2.40:80 + +# Если - проблема в селекторах или поды не Running +``` + +--- + +## Шаг 5: Pod - что происходит внутри контейнера + +Самый глубокий уровень. Под не запускается или падает в цикле перезапусков. + +### Проверка статуса подов + +```bash +# Смотрим все поды в namespace +kubectl get pods -n blog + +# Фильтруем только nginx +kubectl get pods -n blog | grep nginx +``` + +**Возможные статусы проблем:** + +### CreateContainerError + +Контейнер вообще не может стартануть. Обычно проблема с volumes или образом. + +```bash +# Смотрим детали пода (используй своё имя пода) +kubectl describe pod nginx-xxxxxxxxxx-xxxxx -n blog | tail -30 +``` + +Ищем секцию `Events` внизу вывода. Там будет описание проблемы: + +**Пример 1: PVC не примонтировался** + +``` +Events: + Warning FailedMount MountVolume.SetUp failed for volume "blog-public-pvc": + mount failed: mount.nfs: Connection timed out +``` + +NFS хранилище недоступно. Возможные причины: +- NFS сервер выключен или перезагружается +- Неправильный IP или путь в PersistentVolume +- Файрвол блокирует NFS трафик (порт 2049) + +**Пример 2: Образ не скачался** + +``` +Events: + Warning Failed Failed to pull image "nginx:latest": rpc error: code = Unknown +``` + +Контейнер не может скачать образ. Обычно это означает что `imagePullPolicy: Never`, а образ не импортирован на ноду. + +```bash +# Проверяем что образ есть на ноде (используй IP своей worker ноды) +ssh user@192.168.X.X "sudo k3s crictl images | grep nginx" +``` + +Если образа нет - импортируй его через `k3s ctr images import`. + +**Пример 3: ConfigMap не найден** + +``` +Events: + Warning FailedMount ConfigMap "nginx-config" not found +``` + +Deployment ссылается на несуществующий ConfigMap. + +```bash +# Проверяем что ConfigMap существует +kubectl get configmap -n blog | grep nginx-config +``` + +Если нет - создай или исправь имя в Deployment. + +### CrashLoopBackOff + +Контейнер запускается, но сразу падает. Смотрим логи **предыдущего** запуска: + +```bash +# Логи последнего упавшего контейнера +kubectl logs nginx-xxxxxxxxxx-xxxxx -n blog --previous +``` + +**Пример: Nginx падает из-за неправильного конфига** + +``` +nginx: [emerg] unexpected "}" in /etc/nginx/nginx.conf:15 +nginx: configuration file /etc/nginx/nginx.conf test failed +``` + +Синтаксическая ошибка в `nginx.conf`. Проверяем ConfigMap: + +```bash +# Смотрим содержимое конфига +kubectl get configmap nginx-config -n blog -o yaml +``` + +Находим ошибку, исправляем, применяем. Под перезапустится автоматически. + +### Главное: Под работает + +```bash +# Проверка +kubectl get pods -n blog | grep nginx + +# Ожидаемый результат - все Running +nginx-xxxxxxxxxx-xxxxx 1/1 Running 0 2d + +# Если не Running - смотри troubleshooting выше +``` + +--- + +## Шаг 6: Контент - есть ли файлы для отдачи + +Под работает, Service видит его, Traefik проксирует трафик. Но сайт отдаёт `404 Not Found` или пустую страницу. + +Проблема: Hugo Builder не записал файлы на NFS или записал не туда. + +### Проверка + +```bash +# Заходим в под nginx (используй своё имя пода) +kubectl exec -it nginx-xxxxxxxxxx-xxxxx -n blog -- sh + +# Внутри пода смотрим что примонтировалось +ls -la /usr/share/nginx/html/ + +# Должен быть index.html и папки posts, tags, etc +``` + +**Если директория пустая** - Hugo Builder не сработал. Проверяем его логи: + +```bash +# Логи Hugo Builder +kubectl logs -n blog deployment/hugo-builder-prod --tail=50 +``` + +Ищем строку `Build successful!` и список созданных файлов. Если её нет: + +1. **Webhook не сработал** - проверь настройки webhook в Gitea +2. **Hugo упал с ошибкой** - читай логи выше, смотри на что ругается +3. **Собрал в другую директорию** - проверь переменную `OUTPUT_DIR` в build.sh + +### Главное: Контент на месте + +```bash +# Проверка (используй своё имя пода) +kubectl exec -it nginx-xxxxxxxxxx-xxxxx -n blog -- ls /usr/share/nginx/html/ | head -5 + +# Ожидаемый результат +index.html +posts/ +tags/ +categories/ + +# Если пусто - Hugo Builder не отработал (см. выше) +``` + +--- + +## Быстрый чеклист для любой проблемы + +Сохрани эту последовательность - она работает для 95% проблем: + +``` +[ ] DNS: dig домен → правильный IP? +[ ] Сеть: curl -v https://домен → доходит до Traefik? +[ ] MetalLB: kubectl get svc -n traefik → External IP назначен? +[ ] Traefik: kubectl get pods -n traefik → Running? +[ ] IngressRoute: kubectl get ingressroute -A | grep домен → нет дублей? +[ ] Match синтаксис: Host(`домен`) со скобками и backticks? +[ ] Endpoints: kubectl get endpoints -n namespace → не пустые? +[ ] Selector: labels подов совпадают с selector Service? +[ ] Pods: kubectl get pods -n namespace → все Running? +[ ] PVC: kubectl get pvc -n namespace → все Bound? +[ ] Контент: kubectl exec ls /usr/share/nginx/html → файлы есть? +``` + +Проходишь по списку сверху вниз. Останавливаешься на первом `[ ]` где что-то не так. Чинишь. Проверяешь снова. + +Не прыгай хаотично между уровнями. Алгоритм экономит время. + +--- + +## Реальный пример: 503 через раз + +Мой сайт отдавал `503` примерно в 50% запросов. Половина запросов работала, половина нет. + +Прошёл по алгоритму: + +1. ✅ DNS - правильный IP +2. ✅ Сеть - Traefik отвечает +3. ✅ MetalLB - External IP назначен +4. ✅ Traefik - поды Running +5. ❌ IngressRoute - **два роутера на один домен** + +```bash +kubectl get ingressroute -A | grep oakazanin + +NAMESPACE NAME +blog blog-https # СТАРЫЙ, бэкенд в CreateContainerError +oakazanin blog-https # НОВЫЙ, работает +``` + +Traefik видел два роутера, честно балансировал трафик 50/50. Каждый второй запрос улетал в мёртвый `blog/nginx`. + +Диагноз поставлен за 3 минуты. Лечение - одна команда: + +```bash +# Удаляем дубль из старого namespace +kubectl delete ingressroute blog-https blog-http -n blog +``` + +Мораль: **всегда чисти за собой**. Старые namespace с нерабочими сервисами - источник неочевидных проблем. + +--- + +## Откат и cleanup + +Если в процессе диагностики что-то сломал ещё больше - откатываемся: + +```bash +# Восстанавливаем предыдущую версию манифеста +kubectl apply -f nginx-deployment.yaml + +# Перезапускаем поды принудительно +kubectl rollout restart deployment/nginx -n blog + +# Смотрим что изменения применились +kubectl rollout status deployment/nginx -n blog +``` + +**Золотое правило:** Перед экспериментами делай бэкапы манифестов: + +```bash +# Экспортируем текущее состояние с датой +kubectl get deployment,service,ingressroute -n blog -o yaml > backup-$(date +%Y%m%d).yaml +``` + +--- + +## Что дальше + +Ты умеешь диагностировать проблемы. Но лучше их вообще не создавать. + +В следующей части покажу как правильно мигрировать сервисы между namespace - без даунтайма, дублей IngressRoute и других сюрпризов которые приводят к 503. + +Разберём реальный пример: переносим Gitea между namespace с NFS данными, получаем новый SSL за 32 секунды, и удаляем старый namespace навсегда. + +--- + +**Стек этой части:** +- Traefik 2.11 IngressRoute +- Kubernetes 1.30 (K3s) +- kubectl CLI +- curl для внешних проверок +- dig для DNS диагностики diff --git a/content/posts/k3s-architecture/featured.png b/content/posts/k3s-architecture/featured.png deleted file mode 100644 index 71196ef..0000000 Binary files a/content/posts/k3s-architecture/featured.png and /dev/null differ diff --git a/content/posts/k3s-infrastructure/featured.png b/content/posts/k3s-infrastructure/featured.png deleted file mode 100644 index 48e6a49..0000000 Binary files a/content/posts/k3s-infrastructure/featured.png and /dev/null differ diff --git a/content/posts/k3s-installation/featured.png b/content/posts/k3s-installation/featured.png deleted file mode 100644 index be48e8b..0000000 Binary files a/content/posts/k3s-installation/featured.png and /dev/null differ diff --git a/content/posts/k3s-architecture/etcd.svg b/content/posts/k3s-part1-architecture/etcd.svg similarity index 100% rename from content/posts/k3s-architecture/etcd.svg rename to content/posts/k3s-part1-architecture/etcd.svg diff --git a/content/posts/k3s-part1-architecture/featured.png b/content/posts/k3s-part1-architecture/featured.png new file mode 100644 index 0000000..9afcdfb Binary files /dev/null and b/content/posts/k3s-part1-architecture/featured.png differ diff --git a/content/posts/k3s-architecture/final_arch.svg b/content/posts/k3s-part1-architecture/final_arch.svg similarity index 100% rename from content/posts/k3s-architecture/final_arch.svg rename to content/posts/k3s-part1-architecture/final_arch.svg diff --git a/content/posts/k3s-architecture/index.md b/content/posts/k3s-part1-architecture/index.md similarity index 97% rename from content/posts/k3s-architecture/index.md rename to content/posts/k3s-part1-architecture/index.md index d9bdc06..5f9bd02 100644 --- a/content/posts/k3s-architecture/index.md +++ b/content/posts/k3s-part1-architecture/index.md @@ -3,13 +3,9 @@ title: "K3s HA для homelab: архитектура без боли" date: 2025-10-14 draft: false description: "Полноценный Kubernetes в бинарнике на 50MB вместо 1.5GB зависимостей. Разбираем архитектуру K3s HA кластера: почему именно 3 master ноды, зачем embedded etcd и сколько ресурсов закладывать." -summary: "Kubernetes слишком тяжёлый, Docker Swarm мёртв, а хочется нормальный кластер для экспериментов. K3s решает эту проблему - полноценный Kubernetes в 50MB. Разберём архитектуру HA кластера без боли." tags: ["kubernetes", "k3s", "homelab", "proxmox", "architecture", "ha", "devops"] series: ["K3s HA кластер для homelab"] series_order: 1 -# seriesOpened: false -# showTableOfContents: true -showAuthor: true --- Kubernetes слишком тяжёлый, Docker Swarm мёртв, а хочется нормальный кластер для экспериментов. Знакомо? K3s решает эту проблему - полноценный Kubernetes в бинарнике на 50MB вместо 1.5GB зависимостей. Но без правильного планирования вы получите нестабильную конструкцию, которая падает в самый неподходящий момент. diff --git a/content/posts/k3s-architecture/k3s-architecture.svg b/content/posts/k3s-part1-architecture/k3s-architecture.svg similarity index 100% rename from content/posts/k3s-architecture/k3s-architecture.svg rename to content/posts/k3s-part1-architecture/k3s-architecture.svg diff --git a/content/posts/k3s-architecture/k3s-ha.svg b/content/posts/k3s-part1-architecture/k3s-ha.svg similarity index 100% rename from content/posts/k3s-architecture/k3s-ha.svg rename to content/posts/k3s-part1-architecture/k3s-ha.svg diff --git a/content/posts/k3s-architecture/k3s_network.svg b/content/posts/k3s-part1-architecture/k3s_network.svg similarity index 100% rename from content/posts/k3s-architecture/k3s_network.svg rename to content/posts/k3s-part1-architecture/k3s_network.svg diff --git a/content/posts/k3s-architecture/k8s-architecture.svg b/content/posts/k3s-part1-architecture/k8s-architecture.svg similarity index 100% rename from content/posts/k3s-architecture/k8s-architecture.svg rename to content/posts/k3s-part1-architecture/k8s-architecture.svg diff --git a/content/posts/k3s-part2-infrastructure/featured.png b/content/posts/k3s-part2-infrastructure/featured.png new file mode 100644 index 0000000..c147fb5 Binary files /dev/null and b/content/posts/k3s-part2-infrastructure/featured.png differ diff --git a/content/posts/k3s-infrastructure/index.md b/content/posts/k3s-part2-infrastructure/index.md similarity index 98% rename from content/posts/k3s-infrastructure/index.md rename to content/posts/k3s-part2-infrastructure/index.md index b014367..f1d7fea 100644 --- a/content/posts/k3s-infrastructure/index.md +++ b/content/posts/k3s-part2-infrastructure/index.md @@ -1,14 +1,11 @@ --- -title: "Подготовить инфраструктуру для K3s в Proxmox" +title: "K3s HA для homelab: Готовим инфраструктуру в Proxmox" date: 2025-10-21 draft: false description: "Создаём 5 VM в Proxmox с Debian 12, настраиваем сеть, отключаем swap и готовим систему для K3s. Пошаговая инструкция без сюрпризов." -summary: "Архитектура спланирована, ресурсы посчитаны - пора создавать виртуальные машины. Подготовим 5 VM в Proxmox и настроим ОС так, чтобы K3s установился без сюрпризов." tags: ["kubernetes", "k3s", "homelab", "proxmox", "infrastructure", "debian", "devops"] series: ["K3s HA кластер для homelab"] series_order: 2 -# seriesOpened: true -showTableOfContents: true --- Архитектура спланирована, ресурсы посчитаны - пора создавать виртуальные машины. В этой статье подготовим 5 VM в Proxmox и настроим ОС так, чтобы K3s установился без сюрпризов. diff --git a/content/posts/k3s-infrastructure/mermaid-diagram-4x.png b/content/posts/k3s-part2-infrastructure/mermaid-diagram-4x.png similarity index 100% rename from content/posts/k3s-infrastructure/mermaid-diagram-4x.png rename to content/posts/k3s-part2-infrastructure/mermaid-diagram-4x.png diff --git a/content/posts/k3s-infrastructure/mermaid-diagram-5x.png b/content/posts/k3s-part2-infrastructure/mermaid-diagram-5x.png similarity index 100% rename from content/posts/k3s-infrastructure/mermaid-diagram-5x.png rename to content/posts/k3s-part2-infrastructure/mermaid-diagram-5x.png diff --git a/content/posts/k3s-infrastructure/mermaid-diagram.svg b/content/posts/k3s-part2-infrastructure/mermaid-diagram.svg similarity index 100% rename from content/posts/k3s-infrastructure/mermaid-diagram.svg rename to content/posts/k3s-part2-infrastructure/mermaid-diagram.svg diff --git a/content/posts/k3s-part3-installation/featured.png b/content/posts/k3s-part3-installation/featured.png new file mode 100644 index 0000000..0ef9b9c Binary files /dev/null and b/content/posts/k3s-part3-installation/featured.png differ diff --git a/content/posts/k3s-installation/index.md b/content/posts/k3s-part3-installation/index.md similarity index 98% rename from content/posts/k3s-installation/index.md rename to content/posts/k3s-part3-installation/index.md index 8c95126..dabf936 100644 --- a/content/posts/k3s-installation/index.md +++ b/content/posts/k3s-part3-installation/index.md @@ -1,14 +1,11 @@ --- -title: "Установить K3s HA кластер" +title: "K3s HA для homelab: Ставим K3s HA кластер" date: 2025-11-02 draft: false description: "Один curl-скрипт на каждую ноду - и через 15 минут у вас работающий HA-кластер. Устанавливаем K3s на 3 master и 2 worker ноды, настраиваем kubectl." -summary: "Инфраструктура готова: 5 VM работают, ОС настроена, порты открыты. Пора устанавливать K3s. Один curl-скрипт на каждую ноду - и через 15 минут у вас работающий HA-кластер." tags: ["kubernetes", "k3s", "homelab", "installation", "ha", "etcd", "devops"] series: ["K3s HA кластер для homelab"] series_order: 3 -# seriesOpened: true -showTableOfContents: true --- Инфраструктура готова: 5 VM работают, ОС настроена, порты открыты. Пора устанавливать K3s. Один curl-скрипт на каждую ноду - и через 15 минут у вас работающий HA-кластер. diff --git a/content/posts/k3s-installation/mermaid-diagram.svg b/content/posts/k3s-part3-installation/mermaid-diagram.svg similarity index 100% rename from content/posts/k3s-installation/mermaid-diagram.svg rename to content/posts/k3s-part3-installation/mermaid-diagram.svg diff --git a/i18n/ru.yaml b/i18n/ru.yaml index b52ef20..8f2e4a3 100644 --- a/i18n/ru.yaml +++ b/i18n/ru.yaml @@ -23,7 +23,7 @@ article: other: "{{ .Count }} нравится" part_of_series: "Эта статья — часть серии." part: "Часть" - this_article: "Читаешь сейчас" + this_article: "Ты уже здесь" related_articles: "Статьи по теме" reply_by_email: "Ответить по электронной почте" diff --git a/public/404.html b/public/404.html index 0bd10e5..814e3d3 100644 --- a/public/404.html +++ b/public/404.html @@ -24,7 +24,7 @@ - + @@ -64,7 +64,7 @@ - + @@ -145,8 +145,6 @@ - - @@ -156,8 +154,8 @@ defer type="text/javascript" id="script-bundle" - src="/js/main.bundle.min.bdda7dece6cbaf08deef7d254f7f842f3261c2524d247905127c9a20decc03f1011a2950048464c79272c1ce0705a49a41147f39f2b95163bb71d404b33263ef.js" - integrity="sha512-vdp97ObLrwje730lT3+ELzJhwlJNJHkFEnyaIN7MA/EBGilQBIRkx5Jywc4HBaSaQRR/OfK5UWO7cdQEszJj7w==" + src="/js/main.bundle.min.858f7f82734cfae08d59fcf8d0eb186f01706a84e2f7d20d39edfd7bc8bed6166e02d5c65ecce1de82b1ac52d1e01d77bd1a82d19186fdae5fe6e12d867fcf68.js" + integrity="sha512-hY9/gnNM+uCNWfz40OsYbwFwaoTi99INOe39e8i+1hZuAtXGXszh3oKxrFLR4B13vRqC0ZGG/a5f5uEthn/PaA==" data-copy="Копировать" data-copied="Скопировано"> @@ -204,6 +202,13 @@ + + + + + + + @@ -228,7 +233,7 @@ "headline": "404 Page not found", "inLanguage": "ru", - "url" : "http://192.168.11.190:1313/404.html", + "url" : "http://localhost:1313/404.html", "author" : { "@type": "Person", "name": "Олег Казанин" @@ -543,7 +548,7 @@