Local Kubernetes Dev — Part 11: Networking — reaching your service and Ingress
Как поды находят друг друга по имени, какими способами пробросить трафик с хоста внутрь кластера и как настроить prod-like Ingress с человеческими доменами вроде myapp.localhost.
К этому моменту у вас уже есть собранный образ myapp, манифесты (Deployment и Service) и поднятый кластер dev на k3d. Сервис, скорее всего, уже крутится в namespace myapp. Но возникает закономерный вопрос: «А как до него достучаться?» Изнутри кластера один под видит другой, но из браузера на вашем ноутбуке http://localhost:8080 ведёт в пустоту — внутри кластера своя сеть, и она по умолчанию изолирована от хоста.
В этой главе разберёмся с тремя слоями сети: как поды находят друг друга по имени (DNS), какими способами пробросить трафик снаружи внутрь, и как настроить «как в проде» — через Ingress с человеческими доменами вроде myapp.localhost.
DNS внутри кластера: сервисы по имени
Первое, что нужно понять: внутри кластера к сервисам обращаются не по IP, а по имени. Kubernetes запускает встроенный DNS-сервер (по умолчанию это CoreDNS) и автоматически заводит DNS-запись для каждого Service. Это очень удобно: IP пода или сервиса может меняться при пересоздании, а имя остаётся стабильным.
Полное (FQDN) имя обычного сервиса строится по шаблону:
1<имя-сервиса>.<namespace>.svc.<домен-кластера>Домен кластера по умолчанию — cluster.local. То есть наш Service с именем myapp в namespace myapp будет доступен по адресу:
1myapp.myapp.svc.cluster.localДа, выглядит как «масло масляное» (имя сервиса совпало с именем namespace), но это нормально. Такое имя резолвится в ClusterIP — стабильный виртуальный IP сервиса, за которым стоит балансировка по подам.
Хорошая новость: внутри одного namespace писать FQDN целиком не нужно. Если PostgreSQL живёт в том же namespace myapp под сервисом postgres, то из кода myapp достаточно указать хост postgres — короткое имя резолвится за счёт search-доменов, прописанных в /etc/resolv.conf каждого пода. А вот из другого namespace короткое имя уже не сработает — придётся писать postgres.myapp или полный FQDN. Это частая ловушка: «у меня же работало», а сервис переехал в соседний namespace.
Проверить резолв проще всего изнутри временного пода:
1# Полное имя из любого namespace
2kubectl run -it --rm dnstest --image=busybox --restart=Never -- \
3 nslookup myapp.myapp.svc.cluster.local
4
5# Короткое имя — только из своего namespace
6kubectl exec -it -n myapp deploy/myapp -- nslookup postgresПара деталей, которые пригодятся позже (см. главу про зависимости и главу про отладку):
- A/AAAA-записи обычного (non-headless) сервиса указывают на один ClusterIP. У headless-сервиса (
clusterIP: None) запись возвращает IP всех подов сразу — это сюрприз для клиента, который ждёт один VIP. Headless обычно нужен для StatefulSet, например для кластера БД, где важно адресовать конкретные экземпляры. - Для именованных портов CoreDNS заводит SRV-записи вида
_<имя-порта>._<протокол>.<сервис>.<namespace>.svc.cluster.local.
Подробности формата имён описаны в официальной документации DNS for Services and Pods.
Способы доступа снаружи: port-forward, NodePort, Ingress
Теперь — как пробросить трафик с хоста внутрь кластера. Есть три основных способа, и у каждого своя роль.
port-forward — туннель для отладки
kubectl port-forward открывает временный туннель с вашего localhost на один под (или на под, выбранный за сервисом). Идеально для «быстро глянуть» или дёрнуть приложение, которое вы не собираетесь публиковать.
1# Пробросить порт сервиса (можно по имени порта, например https)
2kubectl port-forward -n myapp service/myapp 8080:8080
3
4# Или напрямую на конкретный под
5kubectl port-forward -n myapp pod/myapp-xxxx 8888:8080После этого curl http://localhost:8080/ идёт в myapp. Не путайте эти два адреса: localhost:8080 — это прямой port-forward-туннель на под (так мы открывали сервис в главе про Tilt и будем дёргать его в главе про отладку), а myapp.localhost:8081 ниже — путь через Ingress/Traefik. Это два разных механизма доступа, а не противоречие. По умолчанию туннель биндится только на loopback (127.0.0.1 и ::1); чтобы открыть его для других машин в сети, добавьте --address 0.0.0.0:
1kubectl port-forward -n myapp --address 0.0.0.0 deployment/myapp 8080:8080Важные ограничения: туннель ведёт на один под. Когда под пересоздаётся (выкатка новой версии, масштабирование), сессия обрывается — это не балансировка и не «постоянный» доступ, а инструмент для отладки. Синтаксис и поведение описаны в справке kubectl port-forward.
NodePort — порт на ноде
Service типа NodePort открывает один и тот же порт на всех нодах кластера в диапазоне 30000–32767. Трафик на этот порт перенаправляется в сервис. Способ простой, но порты нестандартные (никакого :80), и на прод это не похоже совсем.
Ingress — HTTP-маршрутизация как в проде
Ingress — это ресурс, который описывает правила HTTP(S)-маршрутизации: какой хост и путь ведут в какой сервис, где TLS. Именно так трафик обычно ходит в проде. Но есть критичный нюанс, на котором спотыкаются почти все новички. Цитата из документации:
You must have an Ingress controller to satisfy an Ingress. Only creating an Ingress resource has no effect.
То есть сам по себе манифест Ingress — это просто декларация правил. Чтобы они заработали, в кластере должен быть запущен Ingress-контроллер (Traefik, NGINX и т.п.), который эти правила читает и реально принимает трафик. Какой именно контроллер обслуживает данный Ingress, задаётся полем ingressClassName.
К счастью, в k3d контроллер уже есть из коробки — об этом ниже. Подробности про Ingress — в официальном Ingress | Kubernetes. Там же упоминается, что Kubernetes развивает более новый Gateway API, но классический Ingress стабилен и заморожен в текущем виде — для локалки его более чем достаточно.
Ingress-контроллер в k3d (Traefik из коробки) и свои хосты
k3d собран на базе k3s, а k3s по умолчанию деплоит Traefik как Ingress-контроллер на портах 80/443, плюс встроенный ServiceLB (Klipper) — благодаря ему сервисы типа LoadBalancer не висят вечно в статусе pending, как было бы в кластере без облачного провайдера.
Но один важный шаг легко пропустить. Traefik слушает внутри кластера, а чтобы он был доступен с хоста, нужно при создании кластера пробросить порт на специальную loadbalancer-ноду (serverlb), которая стоит перед серверными нодами:
1k3d cluster create dev --api-port 6550 -p "8081:80@loadbalancer" --agents 2Здесь -p "8081:80@loadbalancer" означает: порт 8081 на хосте → порт 80 Traefik. Запомните главное: порт-маппинг задаётся только при создании кластера. Добавить его к уже запущенному кластеру нельзя — придётся пересоздавать. Это второй по популярности источник боли после «забыл про контроллер».
Если вы поднимали кластер в главе про k3d без этого флага — сейчас удобный момент пересоздать его с -p "8081:80@loadbalancer".
Теперь опишем Ingress для myapp. Предполагаем, что у вас уже есть Service с именем myapp на порту 80 (он, в свою очередь, ведёт на контейнерный порт 8080):
1apiVersion: networking.k8s.io/v1
2kind: Ingress
3metadata:
4 name: myapp
5 namespace: myapp
6spec:
7 rules:
8 - host: myapp.localhost
9 http:
10 paths:
11 - path: /
12 pathType: Prefix
13 backend:
14 service:
15 name: myapp
16 port:
17 number: 80Применяем и проверяем:
1kubectl apply -f ingress.yaml
2curl http://myapp.localhost:8081/Цепочка получается такая: curl → порт 8081 хоста → Traefik (порт 80 в кластере) → по правилу host: myapp.localhost → Service myapp → под. Это ровно та схема маршрутизации, что и в проде, только вместо реального домена — *.localhost, а вместо облачного балансировщика — k3d. Рекомендованный способ публикации в k3d описан в Exposing Services - k3d.
Хочу свой Ingress-контроллер
Если по какой-то причине нужен не Traefik, а, например, ingress-nginx, встроенный Traefik можно отключить при создании кластера и пробросить «голые» 80/443:
1k3d cluster create traefikless \
2 --k3s-arg "--disable=traefik@server:0" \
3 -p "80:80@loadbalancer" -p "443:443@loadbalancer"Для повседневной локалки этого обычно не требуется — Traefik из коробки закрывает задачу.
Локальные домены (*.localhost, правка /etc/hosts)
Откуда вообще взялось, что myapp.localhost указывает на ваш компьютер? Это не магия k3d. TLD .localhost зарезервирован стандартом RFC 6761 под loopback: резолверы должны возвращать для него адрес обратной петли (127.0.0.1 / ::1) и не отправлять такие запросы в обычный DNS.
На практике это означает: Chrome и Firefox сами резолвят любой *.localhost (myapp.localhost, api.localhost, что угодно) в loopback без всякой настройки — это подтверждают и эксперименты сообщества. Открыли http://myapp.localhost:8081 в Chrome — и всё работает.
А вот дальше начинается то, на чём теряют время. Не все клиенты так умеют. По документации Microsoft:
- Safari на macOS не поддерживает авторезолв
*.localhost— он пойдёт в DNS-стек ОС и, скорее всего, ничего не найдёт. - Не-браузерные инструменты —
curl, многие CLI, HTTP-клиенты в коде — обрабатывают*.localhostкак обычный домен и тоже идут в DNS-стек операционной системы. Если он не резолвит имя, вы получитеcould not resolve host.
Поэтому, если вы тестируете через curl, скрипты или Safari (а ещё — если используете кастомные домены не на .localhost), добавьте запись в /etc/hosts:
1echo '127.0.0.1 myapp.localhost' | sudo tee -a /etc/hostsНа Windows файл лежит по пути C:\Windows\System32\drivers\etc\hosts (правится с правами администратора). После этого curl http://myapp.localhost:8081/ будет работать так же, как в Chrome.
Итого правило простое: в Chrome/Firefox *.localhost работает сразу; для curl, Safari и любых кастомных доменов — пропишите их в /etc/hosts.
Когда какой способ выбрать в локальной разработке
Сведём всё вместе. Три способа — это не «лучше/хуже», а разные инструменты под разные задачи.
| Способ | Когда применять | Минусы |
|---|---|---|
| port-forward | Разовая отладка одного сервиса, быстро дёрнуть API, доступ к БД с хоста | Один под, обрывается при пересоздании, не воспроизводит prod-маршрутизацию |
| Ingress (Traefik + *.localhost) | Повседневная работа, основной prod-like способ | Нужен контроллер и проброс порта при создании кластера |
| NodePort | Запасной вариант, когда не хочется возиться с Ingress | Нестандартные порты 30000–32767, наименее похож на прод |
Практическая рекомендация для нашего `myapp`:
- Основной режим — Ingress. Он повторяет ровно тот путь трафика, что будет в проде (хост/путь/TLS через контроллер), даёт человеческие домены и работает с несколькими репликами. В связке с Tilt (см. главу про Tilt) Ingress настраивается один раз и дальше просто живёт.
- port-forward — для точечной отладки. Залезть в конкретный под, проверить метрики, подключиться к PostgreSQL клиентом с ноутбука. Не для постоянного доступа.
- NodePort — если совсем лень настраивать Ingress в каком-то одноразовом эксперименте. Учтите предупреждение из документации k3d: проброс широкого диапазона NodePort-портов ресурсоёмок и может подвесить систему из-за обработки правил iptables в Docker — пробрасывайте только конкретные нужные порты.
Чем ближе локальный доступ к продовому, тем меньше сюрпризов при выкатке. Поэтому по умолчанию выбираем Ingress, а port-forward держим в кармане для отладки. Как разглядывать, что именно происходит с трафиком и подами, разберём в следующей главе про отладку и наблюдаемость.
Источники
- DNS for Services and Pods | Kubernetes
- Debugging DNS Resolution | Kubernetes
- kubectl port-forward | Kubernetes
- Ingress | Kubernetes
- Exposing Services - k3d
- Networking Services | K3s
- RFC 6761: Special-Use Domain Names
- Support for the .localhost top-level domain | Microsoft Learn
- Firefox and Chrome resolve any localhost domain (*.localhost) to loopback - DEV Community