К этому моменту у вас уже есть собранный образ myapp, манифесты (Deployment и Service) и поднятый кластер dev на k3d. Сервис, скорее всего, уже крутится в namespace myapp. Но возникает закономерный вопрос: «А как до него достучаться?» Изнутри кластера один под видит другой, но из браузера на вашем ноутбуке http://localhost:8080 ведёт в пустоту — внутри кластера своя сеть, и она по умолчанию изолирована от хоста.

В этой главе разберёмся с тремя слоями сети: как поды находят друг друга по имени (DNS), какими способами пробросить трафик снаружи внутрь, и как настроить «как в проде» — через Ingress с человеческими доменами вроде myapp.localhost.

DNS внутри кластера: сервисы по имени

Первое, что нужно понять: внутри кластера к сервисам обращаются не по IP, а по имени. Kubernetes запускает встроенный DNS-сервер (по умолчанию это CoreDNS) и автоматически заводит DNS-запись для каждого Service. Это очень удобно: IP пода или сервиса может меняться при пересоздании, а имя остаётся стабильным.

Полное (FQDN) имя обычного сервиса строится по шаблону:

text
1<имя-сервиса>.<namespace>.svc.<домен-кластера>

Домен кластера по умолчанию — cluster.local. То есть наш Service с именем myapp в namespace myapp будет доступен по адресу:

text
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.

Проверить резолв проще всего изнутри временного пода:

bash
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 на один под (или на под, выбранный за сервисом). Идеально для «быстро глянуть» или дёрнуть приложение, которое вы не собираетесь публиковать.

bash
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:

bash
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), которая стоит перед серверными нодами:

bash
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):

yaml
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

Применяем и проверяем:

bash
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:

bash
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:

bash
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 держим в кармане для отладки. Как разглядывать, что именно происходит с трафиком и подами, разберём в следующей главе про отладку и наблюдаемость.

Источники

Хотите prod-like сеть в локальном Kubernetes?
Нужна помощь с настройкой DNS, Ingress и prod-like локальных доменов для ваших сервисов в Kubernetes? Помогу настроить чистую production-like сеть в локальном кластере.