--- title: "Блог на Hugo в K3s: часть 5 - что делать когда всё внезапно сломалось" date: 2026-02-17 draft: false description: "Алгоритм диагностики Hugo блога в K3s за 5 минут: от DNS до пода. Сайт отдаёт 503 — находим причину без паники." tags: ["kubernetes", "k3s", "traefik", "debugging", "nginx", "troubleshooting"] categories: ["Kubernetes", "DevOps практики"] 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 диагностики