DataLife Engine / Настраиваем Ingress Controller Traefik и Let's Encrypt в кластере Kubernetes на Ubuntu 22.04

Настраиваем Ingress Controller Traefik и Let's Encrypt в кластере Kubernetes на Ubuntu 22.04


На уже развёрнутом kubeadm-кластере поднимем Ingress Controller Traefik, дадим ему принимать входящий трафик на портах 80/443, настроим persistent-хранилище для сертификатов и получим рабочий HTTPS через Let's Encrypt встроенным ACME-клиентом Traefik.

Статья продолжает «Разворачиваем кластер Kubernetes (kubeadm) на Ubuntu 22.04» — здесь предполагается, что кластер уже поднят и все ноды в Ready.


Важно: проект ingress-nginx официально архивирован 24 марта 2026 года и больше не развивается — использовать его для новых кластеров не стоит. В качестве альтернативы взят Traefik, который активно поддерживается.


Требования




1. Установка Helm


curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4
chmod 700 get_helm.sh
./get_helm.sh
helm version


2. Установка Traefik через Helm


Добавляем репозиторий:
helm repo add traefik https://traefik.github.io/charts
helm repo update


Ставим с hostNetwork: true — под будет напрямую биндиться на порты ноды, без облачного LoadBalancer (актуально для self-managed VPS, где его просто нет):
helm install traefik traefik/traefik \
  --namespace traefik \
  --create-namespace \
  --set hostNetwork=true \
  --set service.enabled=false \
  --set nodeSelector."kubernetes\.io/hostname"=<hostname_worker_ноды>


nodeSelector жёстко закрепляет Traefik за конкретной нодой — именно за той, на чей IP указывает ваш домен.

3. Меняем порты на 80/443


По умолчанию чарт слушает 8000/8443 (непривилегированные порты), а не 80/443. Меняем явно:
helm upgrade traefik traefik/traefik \
  --namespace traefik \
  --reuse-values \
  --set ports.web.port=80 \
  --set ports.websecure.port=443 \
  --set securityContext.runAsUser=0 \
  --set securityContext.runAsNonRoot=false \
  --set podSecurityContext.runAsUser=0 \
  --set podSecurityContext.runAsNonRoot=false \
  --set securityContext.capabilities.add[0]=NET_BIND_SERVICE


Root здесь не украшение, а вынужденная мера: связка hostNetwork: true и capability NET_BIND_SERVICE (штатный способ разрешить непривилегированному процессу биндиться на порт меньше 1024) в текущей версии чарта не срабатывает сама по себе — под падает с bind: permission denied. Root — задокументированный обходной путь именно для этого сценария.


Важно: runAsUser=0 один, без capabilities.add[0]=NET_BIND_SERVICE, — недостаточен. По умолчанию чарт выставляет capabilities.drop: [ALL], и в современном ядре Linux даже root не может биндиться на привилегированный порт, если capability явно обнулена — это capability-based проверка, а не привилегия по UID. Нужны оба флага одновременно.


Проверяем, что порты реально слушаются на нужной ноде:
ss -tlpn | grep traefik


Ожидаем *:80 и *:443.

4. Настраиваем DNS для hostNetwork-пода


Под с hostNetwork: true по умолчанию использует DNS-настройки самой ноды (/etc/resolv.conf хоста), а не кластерный CoreDNS — из-за этого под не сможет резолвить имена вида *.svc.cluster.local других сервисов кластера. Для Ingress-роутинга это не критично (Traefik получает адреса подов напрямую из Kubernetes API, а не через DNS), но пригодится для любых будущих проверок и интеграций изнутри самого Traefik:
helm upgrade traefik traefik/traefik \
  --namespace traefik \
  --reuse-values \
  --set deployment.dnsPolicy=ClusterFirstWithHostNet


ClusterFirstWithHostNet — официальное значение Kubernetes специально для hostNetwork-подов, которым всё равно нужен доступ к кластерному DNS.

5. Открываем порты в облачном firewall


Отдельно от firewall самой ОС: если используете, например, Hetzner Cloud Firewall, добавьте inbound-правила для TCP 80 и 443 на эту ноду. Проверить снаружи:
telnet <публичный_IP> 80


6. Устанавливаем StorageClass (local-path-provisioner)


Без него любой PersistentVolumeClaim в кластере зависнет в Pending навсегда — в self-managed kubeadm-кластере, в отличие от managed (EKS/GKE), StorageClass по умолчанию не существует.
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.36/deploy/local-path-storage.yaml
kubectl get pods -n local-path-storage


Делаем его default-классом кластера:
kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'


7. Включаем persistence для сертификатов


Без этого шага сертификат будет теряться при каждом перезапуске пода, а частые повторные запросы упрутся в rate limit Let's Encrypt — не больше 5 дубликатов в неделю на домен.
helm upgrade traefik traefik/traefik \
  --namespace traefik \
  --reuse-values \
  --set persistence.enabled=true \
  --set persistence.size=128Mi \
  --set persistence.storageClass=local-path


8. Настраиваем ACME (Let's Encrypt)


helm upgrade traefik traefik/traefik \
  --namespace traefik \
  --reuse-values \
  --set certificatesResolvers.le.acme.email=<ваш_email> \
  --set certificatesResolvers.le.acme.storage=/data/acme.json \
  --set certificatesResolvers.le.acme.httpChallenge.entryPoint=web


9. Добавляем init-container для прав на acme.json


Traefik сам предупреждает об этом при первом упоминании persistence и ACME: без явного init-container права на файл могут теряться при перезапуске.
cat <<EOF > traefik-acme-init.yaml
deployment:
  initContainers:
    - name: volume-permissions
      image: busybox:latest
      command: ["sh", "-c", "touch /data/acme.json; chmod -v 600 /data/acme.json"]
      volumeMounts:
        - mountPath: /data
          name: data
EOF

helm upgrade traefik traefik/traefik \
  --namespace traefik \
  --reuse-values \
  -f traefik-acme-init.yaml


Проверяем логи — должна появиться строка о старте ACME-провайдера:
kubectl logs -n traefik -l app.kubernetes.io/name=traefik --tail=30 | grep -i acme


10. Деплоим тестовое приложение: HTTP и HTTPS независимо, двумя отдельными Ingress


kubectl create deployment demo --image=traefik/whoami --replicas=1
kubectl expose deployment demo --port=80 --target-port=80


Важно: Traefik создаёт один роутер на один Ingress-объект. Если добавить аннотацию router.tls.certresolver в общий Ingress, TLS включается для всего роутера целиком — даже если в router.entrypoints явно перечислить и web, и websecure. В результате обычный HTTP-запрос на порт 80 перестаёт обслуживаться (роутер требует TLS-подключение) и Traefik отдаёт свой дефолтный 404 page not found, при этом HTTPS на том же домене продолжает работать нормально — асимметрия, которую легко принять за проблему с DNS или сетью, хотя причина чисто в конфигурации роутинга.


Если нужен редирект HTTP в HTTPS (частый случай) — достаточно одного Ingress с TLS-секцией, роутер сам обработает оба направления. Но если протоколы должны работать независимо, без редиректа — нужны два отдельных Ingress, каждый со своим entrypoints и backend:
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-http
  annotations:
    kubernetes.io/ingress.class: traefik
    traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
  rules:
  - host: <ваш_домен>
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: demo
            port:
              number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-https
  annotations:
    kubernetes.io/ingress.class: traefik
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls.certresolver: le
spec:
  tls:
  - hosts:
    - <ваш_домен>
  rules:
  - host: <ваш_домен>
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: demo
            port:
              number: 80
EOF


Traefik автоматически инициирует HTTP-01 challenge (через роутер demo-http на порту 80) и получит сертификат для роутера demo-https — проверяем:
kubectl logs -n traefik -l app.kubernetes.io/name=traefik --tail=30 | grep -i acme


Проверяем оба протокола отдельно:
curl -I http://<ваш_домен>/
curl -I https://<ваш_домен>/


Оба должны вернуть 200 OK.

Возможные проблемы


PVC зависает в Pending: unbound immediate PersistentVolumeClaims

Причина — не указан storageClass явно, а в кластере нет default StorageClass. Решение — шаги 6 и 7 выше: поставить local-path-provisioner, пометить его как default либо явно указать persistence.storageClass=local-path.

Если PVC уже создан пустым, без storageClass, — storageClassName неизменяемое поле, апгрейд Helm его не поправит. Нужно удалить и пересоздать:
kubectl delete pvc traefik -n traefik
kubectl delete pod -n traefik -l app.kubernetes.io/name=traefik


bind: permission denied даже с runAsUser=0

Root сам по себе не спасает от capability-based проверки ядра. Чарт по умолчанию выставляет securityContext.capabilities.drop: [ALL] — обнуляет всё, включая право биндиться на порт меньше 1024. Добавьте capability явно, вместе с root:
--set securityContext.capabilities.add[0]=NET_BIND_SERVICE


HTTP отдаёт 404, хотя HTTPS на том же домене работает нормально

Причина — общий Ingress с аннотацией router.tls.certresolver, которая делает единственный роутер TLS-only для всех перечисленных entrypoints (см. шаг 10 выше). Разделите на два отдельных Ingress-объекта: один чисто HTTP, другой чисто HTTPS.

При каждом helm upgrade новый под зависает в Pending с ошибкой didn't have free ports

Классика для hostNetwork: true — два пода с одинаковыми портами физически не могут одновременно жить на одной ноде. Rolling update не может завершиться сам. Перед апгрейдами, меняющими сетевую конфигурацию, проще сразу остановить старый под:
kubectl scale deployment traefik -n traefik --replicas=0
# helm upgrade ...
kubectl scale deployment traefik -n traefik --replicas=1


Traefik слушает 8000/8443 вместо 80/443, хотя ports.web.port=80 указан

Проверьте фактические слушающие порты прямо на ноде:
ss -tlpn | grep traefik


Если видите старые порты — под не пересоздался после апгрейда (сверьте PID процесса до и после). Удалите под вручную, Deployment пересоздаст его с актуальным конфигом.
Сегодня, 18:47
Вернуться назад