Перейти к основному содержимому

Сеть в K8s

Презентация​

Ссылка на презентацию
Последняя редакция: 18.09.26

Теория​

Probes​

Probes - механизмы, которыми Kubernetes отслеживает состояние пода изнутри, а не просто "жив процесс или нет"

  • Liveness - когда перезапускать контейнер (завис, но процесс формально жив)
  • Readiness - готово ли приложение принимать трафик прямо сейчас
  • Startup - запустилось ли приложение вообще (для медленно стартующих сервисов)
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: liveness-exec
spec:
containers:
- name: liveness
image: registry.k8s.io/busybox:1.27.2
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -f /tmp/healthy; sleep 600
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 5
periodSeconds: 5

readinessProbe и startupProbe устроены точно так же - те же exec/httpGet/tcpSocket и те же поля таймингов (initialDelaySeconds, periodSeconds и так далее), отличается только имя ключа верхнего уровня

readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 10

Пока startupProbe не пройдёт хотя бы один раз успешно, livenessProbe и readinessProbe вообще не запускаются - это специально для медленно стартующих приложений, чтобы их не перезапустили и не убрали из трафика ещё до того, как они успели подняться

Readiness - не просто диагностика, а прямое условие для сети: именно от неё зависит, попадёт ли под в Endpoints Service'а (см. ниже) и получит ли он вообще трафик снаружи

Endpoints и EndpointSlice​

Ingress и Gateway API (дальше в этой статье) в итоге всегда указывают на Service, а не напрямую на поды
Service сам по себе - только стабильный адрес (имя, порт, тип), реальный список живых подов за ним хранится в отдельном объекте - Endpoints (или его более новой версии, EndpointSlice)

Как Service находит свой Endpoints - через одинаковое имя: kubectl get endpoints my-svc всегда возвращает объект с тем же metadata.name, что и у Service my-svc
У EndpointSlice имя другое (слайсов на один Service может быть несколько), поэтому связь идёт через лейбл kubernetes.io/service-name: my-svc
kube-proxy на каждой ноде следит за этими объектами и на их основе программирует реальные правила перенаправления трафика

Зачем Endpoints вообще нужен как отдельный объект, если у Service уже есть selector - дело в том, что selector у Service необязателен, Service можно создать вообще без него, и тогда Kubernetes не будет сам вычислять его Endpoints
Типичный случай - Service указывает не на поды в кластере, а на внешний backend за его пределами, например на базу данных

apiVersion: v1
kind: Service
metadata:
name: external-db
spec:
ports:
- port: 5432
---
apiVersion: v1
kind: Endpoints
metadata:
name: external-db
subsets:
- addresses:
- ip: 192.0.2.42
ports:
- port: 5432

Здесь Endpoints создаётся и поддерживается руками (или отдельным контроллером), а не автоматически по selector - именно поэтому Endpoints не поле Service, а отдельный ресурс

Ещё один довод в пользу отдельного объекта - масштабирование: если пользоваться старым Endpoints, все адреса живут в одном объекте, который целиком рассылается на каждую ноду при любом изменении
EndpointSlice по умолчанию ограничивает один слайс 100 адресами (--max-endpoints-per-slice на kube-controller-manager, можно поднять до 1000) и режет большой список на несколько слайсов - при изменении одного пода kube-proxy на нодах получает маленький дифф, а не пересылку всего списка целиком

Как это связано с Probes: если под не проходит readiness-проверку, control plane убирает его из Endpoints/EndpointSlice - трафик через Service (а значит, и через Ingress/Gateway API) на такой под просто не пойдёт, при этом сам под продолжает работать и не перезапускается
Именно так K8s не даёт "недоготовым" репликам приложения попадать под реальную нагрузку во время деплоя или медленного старта

Ingress​

Прямой доступ снаружи к каждому Service по отдельности неудобно масштабировать - под каждый сервис пришлось бы городить свой NodePort или LoadBalancer
Ingress - единая точка входа для HTTP(S)-трафика снаружи, которая распределяет запросы по разным Service внутри кластера по правилам маршрутизации

client -> Ingress-managed load balancer -> Ingress -> Service -> Pod

Правила маршрутизации бывают двух видов

  • По пути (path) - /foo уходит на один Service, /bar - на другой, на одном и том же хосте
  • По хосту (host) - foo.bar.com и bar.foo.com уходят на разные Service, хотя оба приходят на один и тот же Ingress

Минимальный манифест - без fan-out и виртуальных хостов, весь трафик на один Service

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-service
port:
number: 80

ingressClassName - какой именно Ingress Controller должен разбирать этот манифест (см. ниже) - без него, если в кластере несколько контроллеров одновременно, непонятно, кто из них должен подхватить правило

Ещё одно полезное поле - defaultBackend: куда слать запрос, если ни одно правило из rules не подошло (без него балансировщик на несовпавший путь просто ответит своим дефолтным 404)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test-ingress
spec:
defaultBackend:
service:
name: test
port:
number: 80

Ingress также умеет терминировать TLS - подробнее про сертификаты для этого ниже, в разделе про HTTPS

Ingress Controller​

Сам ресурс Ingress - это только манифест с правилами, кто-то должен их реально исполнять - этим занимается Ingress Controller, отдельный компонент, который нужно установить отдельно

Реализаций несколько, и они не взаимозаменяемы один в один

  • nginx - самая распространённая
  • traefik
  • istio
  • облачные (AWS, GCE и другие) - управляются самим облаком

Важно про nginx: проект ingress-nginx объявлен retired - с марта 2026 у него больше нет ни релизов, ни исправлений безопасности, хотя уже развёрнутые кластеры продолжают работать как раньше
Сам ресурс Ingress от этого не deprecated и никуда не делся, deprecated только конкретно эта реализация контроллера - официальная рекомендация - переходить на Gateway API (см. ниже), либо использовать другой Ingress Controller (traefik, облачный и так далее)

На minikube - через встроенный addon (он ставит именно ingress-nginx из примечания выше)

minikube addons enable ingress

В YCloud Ingress Controller ставится отдельно, вместе с менеджером сертификатов Let's Encrypt® (см. "Дополнительно")

CRD (Custom Resources)​

Ingress - штатный ресурс Kubernetes API, но большая часть более сложных вещей (включая Gateway API дальше) реализована через CRD (Custom Resource Definition) - механизм, который позволяет добавлять в API k8s свои собственные типы объектов

  • Свои типы объектов, не встроенные в Kubernetes изначально
  • Доступ к ним - через тот же kubectl/API, что и к штатным ресурсам (Pod, Service и так далее)
  • Есть стандарт описания самого CRD и валидация манифестов - k8s отклонит объект, если он не соответствует схеме

Gateway API целиком построен на CRD - Gateway, HTTPRoute, GatewayClass не входят в apps/v1/networking.k8s.io/v1, это отдельные CRD, которые ставятся в кластер до того, как ими можно будет пользоваться

Gateway API​

Gateway API - более новая замена Ingress, тоже отвечает за вход трафика в кластер, но устроена иначе

  • Разделена на несколько ролей вместо одного плоского манифеста - GatewayClass (реализация, обычно её ставит облако/платформа), Gateway (точка входа - слушает конкретный порт/протокол), HTTPRoute (правила маршрутизации, привязанные к конкретному Gateway)
  • Разные команды могут владеть разными частями - платформенная команда управляет Gateway, разработчики - своими HTTPRoute, не трогая инфраструктурную часть
  • Поддерживает больше протоколов и более гибкие правила маршрутизации, чем классический Ingress

Как и с Ingress, сам ресурс - это только манифест, кто-то должен реализовывать эти правила
Установка одних CRD Gateway API (kubectl apply --server-side -f .../standard-install.yaml) трафик обрабатывать не заставит - нужен отдельно установленный Gateway API controller, точно так же как Ingress Controller для Ingress

Готового addon для minikube, как у Ingress, здесь нет
Один из вариантов без Helm (в курсе Helm пока не разбирали)

kubectl apply -f https://projectcontour.io/quickstart/contour-gateway-provisioner.yaml
kubectl get pods -n projectcontour

Этот же манифест ставит и CRD, и сам контроллер (Contour), а связывает их GatewayClass

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: contour
spec:
controllerName: projectcontour.io/gateway-controller

GatewayClass в большинстве случаев уже создан за вас платформой (в YCloud - при установке Gateway API для managed-кластера, см. "Дополнительно") - дальше нужны только Gateway и HTTPRoute, которые ссылаются на него по имени через gatewayClassName

Gateway - точка входа, слушает конкретный порт и протокол

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: example-gateway
spec:
gatewayClassName: example-class
listeners:
- name: http
protocol: HTTP
port: 80

HTTPRoute - правила маршрутизации, привязанные к конкретному Gateway через parentRefsУмеет то же, что и path/host-based роутинг у Ingress выше - hostnames отвечает за хост, matches внутри rules - за путь

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: example-httproute
spec:
parentRefs:
- name: example-gateway
hostnames:
- "www.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /login
backendRefs:
- name: example-svc
port: 8080

Полный рабочий пример, эквивалентный тому же fan-out Ingress'у выше - ниже, в разделе "Примеры"

Доменные имена и DNS​

DNS в контексте этой темы - не внутрикластерный DNS подов и Service, а обычный интернет-DNS, без которого к приложению нельзя будет зайти по человеческому имени, а не IP

  • ICANN координирует TLD (Top-Level Domain - .ru, .com и подобные)
  • Операторы реестров управляют конкретным TLD (для .RU/.РФ - Координационный центр доменов, а также РКН как реестр доменов)
  • Регистраторы аккредитованы реестром и продают домены конечным пользователям - именно здесь домен покупают

Доменное имя само по себе бесполезно без сопоставления с IP-адресом - за это отвечают DNS-серверы
Их адрес обычно не выбирают руками - он приходит от интернет-провайдера, зашит в роутер или на устройстве по умолчанию, но его всегда можно переопределить явно (например, использовать DNS-серверы Яндекса)

DNS-записи​

Один домен может хранить в DNS не только IP - разные типы записей отвечают за разные вещи

  • A - доменное имя -> IPv4-адрес
  • AAAA - доменное имя -> IPv6-адрес
  • CNAME - домен/поддомен -> другое доменное имя (алиас)
  • MX - куда слать почту для этого домена
  • TXT - произвольная текстовая информация (подтверждение владения доменом, SPF и так далее)
  • NS - какие серверы отвечают за DNS этого домена

Локальный DNS и свой домен​

Локально, ещё до всякого DNS-сервера, есть файл /etc/hosts - он сопоставляет имя и IP только для собственной машины, в обход настоящего DNS

127.0.0.1 {{your_domain}}

Удобно, чтобы проверить Ingress по имени хоста ещё до того, как домен настроен по-настоящему (см. "Практика")

Чтобы заходить в приложение с реального домена

  • Купить домен у регистратора
  • Прописать у регистратора адреса DNS-серверов Яндекса
  • В Yandex DNS создать нужные записи (A на IP балансировщика, CNAME для поддоменов)

Шифрование и HTTPS​

Чтобы понять, что такое SSL-сертификат и зачем он нужен Ingress, стоит начать с самого шифрования

Шифр - сообщение превращается в нечитаемый вид и обратно с помощью ключа и алгоритма
Простой пример - шифр Цезаря со сдвигом 5 (ROT5): hello world -> mjqqt btwqi
Проблема любого такого шифра одна - если один и тот же ключ нужен и чтобы зашифровать, и чтобы расшифровать, что делать, если он попадёт не в те руки?

Отсюда - асимметричное шифрование

  • Создаётся пара связанных ключей - публичный и приватный
  • Публичным ключом данные шифруют, приватным - расшифровывают
  • Публичный ключ можно раздавать всем, приватный не покидает сервер

Остаётся один вопрос - как убедиться, что публичный ключ, который вам прислали, действительно принадлежит тому серверу, а не подделан по пути
Отвечает на него сертификат: авторитетная третья сторона ("арбитр", центр сертификации) подписывает публичный ключ сервера, подтверждая, что он принадлежит именно ему
Для HTTPS-трафика такой подписанный сертификат обязателен - именно его получает и подключает Ingress из TLS-примера выше
Let's Encrypt - один из таких арбитров, причём бесплатный, поэтому и упоминается в инструкции по установке Ingress Controller в YCloud

Сертификаты и cert-manager​

Сертификаты не бессрочны и требуют регулярного обновления - делать это руками на каждый домен и каждый Ingress не масштабируется

cert-manager - контроллер, который автоматизирует всю эту рутину

  • Получение сертификатов у выбранного удостоверяющего центра (issuer)
  • Автоматическое обновление до истечения срока
  • Поддержка нескольких "арбитров" одновременно (Let's Encrypt и другие)

Установка - один манифест, без Helm

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.21.2/cert-manager.yaml
kubectl get pods --namespace cert-manager

Дальше нужен ClusterIssuer - он описывает, у какого арбитра и как получать сертификаты

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: user@example.com
privateKeySecretRef:
name: letsencrypt-staging-key
solvers:
- http01:
ingress:
ingressClassName: nginx

Staging-сервер (как в примере) - для тестов, у production-сервера Let's Encrypt (acme-v02...) отдельные жёсткие лимиты на количество запросов, их легко исчерпать во время отладки

Имея ClusterIssuer, вручную создавать Secret с сертификатом (как в TLS-примере из "Примеры") уже не нужно - достаточно добавить на Ingress аннотацию cert-manager.io/cluster-issuer и тот же spec.tls, а cert-manager сам создаст нужный Secret и будет следить за его продлением

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-example-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
spec:
tls:
- hosts:
- https-example.foo.com
secretName: testsecret-tls
rules:
- host: https-example.foo.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service1
port:
number: 80

Под капотом при этом создаётся цепочка объектов - Certificate (запрос на сертификат) -> CertificateRequest (приватный ключ и CSR) -> Order (запрос к ACME-серверу) -> Challenge (одно на каждый домен, доказательство владения им)
Для HTTP01 (как в примере выше) доказательство - файл, который cert-manager сам выкладывает по адресу http://<домен>/.well-known/acme-challenge/<токен> через тот же Ingress, а ACME-сервер сам его проверяет

Второй способ - DNS01: доказательство не файл на сайте, а TXT-запись вида _acme-challenge.<домен> прямо в DNS-зоне - её должен туда добавить сам cert-manager, а для этого ему нужен доступ к API DNS-провайдера, а не к Ingress
DNS01 не требует, чтобы порт 80 был открыт наружу - подходит и для доменов без публичного входа, и для wildcard-сертификатов (*.example.com), которые HTTP01 получить в принципе не может

Всё это работает только если ACME-сервер может достучаться до вас (HTTP01) или cert-manager может достучаться до DNS-провайдера (DNS01) - HTTP01 требует, чтобы порт 80 вашего Ingress был доступен из интернета
На minikube (без публичного IP и без настоящего DNS) не подходит ни один из двух способов - ClusterIssuer с ACME просто зависнет на Order/Challenge и упадёт по таймауту, сколько ни жди
Для локального теста без реального IP есть отдельный тип Issuer/ClusterIssuer - selfSigned

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned
spec:
selfSigned: {}

Проверки владения доменом здесь нет вообще - сертификат подписывает сам себя, без внешнего арбитра, поэтому не требует ни интернета, ни реального IP, ни DNS
Обратная сторона - браузер такому сертификату по умолчанию не доверяет, признанного центра сертификации за ним нет
Для minikube это нормальный вариант - показывает именно автоматизацию cert-manager (Certificate/Secret создаются и живут сами), а не настоящий доверенный HTTPS

Настоящий, доверенный браузером сертификат через ACME имеет смысл получать там, где есть настоящий внешний IP - в облаке, а не на minikube
Развёртывание кластера в настоящем облаке - отдельная тема, она разобрана в 06-K8s-cloud-cluster, а получение настоящего сертификата через cert-manager и Cloud DNS - в 07-K8s-cloud-ingress

Это не единственный способ получить настоящий доверенный сертификат без публичного IP - у Tailscale есть собственный выход на Let's Encrypt, минуя cert-manager, разобран в разделе про Tailscale чуть ниже

Tailscale: доступ к локальному кластеру снаружи​

minikube tunnel из "Практики" выше решает только половину задачи - делает Service типа LoadBalancer доступным с той же машины, где крутится сам minikube
С другого компьютера до него всё равно не достучаться - та же причина, по которой в 06-K8s-cloud-cluster ради внешнего доступа приходится переезжать в облако: у локального кластера нет и не может быть собственного публичного IP

Tailscale - оверлейная сеть поверх WireGuard: каждое устройство, зашедшее в один и тот же "tailnet", получает собственный стабильный приватный IP и может достучаться до любого другого устройства той же сети напрямую, через NAT и без публичного IP с обеих сторон
Это не замена облаку (масштабирование, отказоустойчивость, настоящий LoadBalancer с публичным IP остаются только у облака), но для самого частого учебного/личного сценария - дать кому-то ещё заглянуть в то, что крутится локально - разворачивать ради этого целый кластер в Yandex Cloud избыточно

Headscale - открытый self-hosted control-plane, совместимый с протоколом Tailscale, без зависимости от инфраструктуры самого Tailscale
Но у оператора ниже жёсткая зависимость именно от SaaS Tailscale - аутентификация идёт через OAuth, которого Headscale не реализует (это прямо зафиксировано как неподдерживаемое в его собственном трекере задач), так что с Headscale этот оператор не взлетит - там пришлось бы ставить обычный tailscale-клиент с флагом --login-server вручную, без операторского удобства

Официальный Tailscale Kubernetes Operator ставится одним манифестом, без Helm (в курсе Helm пока не разбирали) - предварительно в консоли Tailscale нужно завести OAuth-клиента, на странице Trust credentials (admin console -> Settings -> Trust credentials -> Credential -> OAuth)
Клиенту нужен write-доступ к трём областям, каждая - с тэгом tag:k8s-operator: General/Services, Devices/Core, Keys/Auth Keys:

curl -o operator.yaml https://raw.githubusercontent.com/tailscale/tailscale/main/cmd/k8s-operator/deploy/manifests/operator.yaml
# в operator.yaml заменить плейсхолдеры на свои значения, в кавычках (секрет
# регистрозависим):
# # SET CLIENT ID HERE -> client_id: "<OAuth client ID>"
# # SET CLIENT SECRET HERE -> client_secret: "<OAuth client secret>"
kubectl apply -f operator.yaml
kubectl get pods -n tailscale

OAuth client ID/secret после правки остаются в operator.yaml открытым текстом - этот файл с уже вписанными значениями нельзя коммитить в git

После установки любой существующий Service достаточно переключить на loadBalancerClass: tailscale вместо обычного LoadBalancer, без переписывания остального манифеста:

apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
type: LoadBalancer
loadBalancerClass: tailscale
selector:
app: nginx
ports:
- port: 80

Оператор сам поднимает отдельный под-прокси, заводит его в tailnet и выдаёт Service DNS-имя вида nginx.<имя-tailnet>.ts.net - по нему любое устройство, вошедшее в тот же tailnet (tailscale up на ноутбуке, телефоне, другом сервере), достучится до него напрямую, без minikube tunnel и без localhost
Для голого Service это L4-проксирование - трафик уже зашифрован WireGuard'ом, но отдельного HTTPS-сертификата для самого приложения здесь не возникает

Ingress с ingressClassName: tailscale вместо ingress-nginx даёт не только маршрутизацию по пути или хосту - на первом подключении оператор сам запрашивает у Let's Encrypt настоящий, доверенный браузером сертификат на MagicDNS-имя, без cert-manager и без ручного ClusterIssuer:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress
spec:
ingressClassName: tailscale
tls:
- hosts:
- nginx
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80

Для этого на tailnet должны быть включены HTTPS Certificates и MagicDNS (один раз, в консоли Tailscale), дальше сертификат на nginx.<имя-tailnet>.ts.net выпускается и продлевается сам - лимит 50 новых сертификатов в неделю на tailnet, для частой пересборки при отладке есть staging-сервер Let's Encrypt через ProxyClass
Ограничение про внешний IP, то есть про облако, из раздела про HTTPS к этому случаю не относится - MagicDNS-имя доверенное даже на minikube без единого публичного IP

На каждый Ingress оператор поднимает ровно один прокси-под и одно MagicDNS-имя
Фронтенд и бэкенд с разными хостами в одном Ingress (несколько rules/tls.hosts с разными host, как привычно для ingress-nginx) не дадут двух машин в tailnet - поднимется только первый хост, второй останется без прокси
Чтобы у каждого сервиса была своя машина, нужен отдельный Ingress-объект на каждый хост:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress
spec:
ingressClassName: tailscale
tls:
- hosts:
- nginx
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
spec:
ingressClassName: tailscale
tls:
- hosts:
- api
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 8080

Каждый получит своё MagicDNS-имя (nginx.<tailnet>.ts.net и api.<tailnet>.ts.net) и свой прокси-под - в отличие от ingress-nginx/Gateway API, где ровно такой фан-аут по хостам живёт в одном объекте, у Tailscale он требует несколько объектов

Публиковать сервис в открытый интернет (а не только на свой tailnet) можно отдельно - но именно через Ingress, а не через голый Service: аннотация tailscale.com/funnel: "true" ставится на Ingress с ingressClassName: tailscale
Это уже осознанный шаг, а не поведение по умолчанию - заранее нужно разрешить funnel в ACL-политике tailnet, причём для тэга прокси-подов tag:k8s (не tag:k8s-operator - это тэг самого оператора, не прокси)

Если на паре соединение Tailscale не поднимается - до разбора манифеста стоит сначала проверить, не режет ли WireGuard-трафик провайдер: в части регионов это встречается независимо от того, Tailscale это или Headscale, потому что фильтруется сам протокол, а не конкретный сервис

Примеры​

Ingress с маршрутизацией по пути (fan-out) - /foo и /bar на одном хосте уходят на разные Service

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: fanout-ingress
spec:
rules:
- http:
paths:
- path: /foo
pathType: Prefix
backend:
service:
name: service1
port:
number: 4200
- path: /bar
pathType: Prefix
backend:
service:
name: service2
port:
number: 8080

Тот же принцип, но маршрутизация по хосту (name-based virtual hosting) - foo.bar.com и bar.foo.com через один и тот же Ingress уходят на разные Service

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: name-virtual-host-ingress
spec:
rules:
- host: foo.bar.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service1
port:
number: 80
- host: bar.foo.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service2
port:
number: 80

Тот же Service, но через Ingress с TLS - секрет с сертификатом и ключом, на который ссылается Ingress

apiVersion: v1
kind: Secret
metadata:
name: testsecret-tls
namespace: default
data:
tls.crt: <base64-encoded cert>
tls.key: <base64-encoded key>
type: kubernetes.io/tls
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-example-ingress
spec:
tls:
- hosts:
- https-example.foo.com
secretName: testsecret-tls
rules:
- host: https-example.foo.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service1
port:
number: 80

Тот же смысл, но через Gateway API - Gateway слушает порт, HTTPRoute привязывает к нему правила (GatewayClass в примере ниже уже установлен платформой заранее)

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: prod-web
spec:
gatewayClassName: example
listeners:
- name: prod-web-gw
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Same
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: foo
spec:
parentRefs:
- name: prod-web
hostnames:
- "foo.bar.com"
rules:
- matches:
- path:
type: PathPrefix
value: /foo
backendRefs:
- name: foo-svc
port: 8080

hostnames и matches.path вместе повторяют то же самое разделение по хосту и по пути, что и у Ingress-примеров выше - только host/path у Ingress лежат в одном rules[].host/.path, а у HTTPRoute это hostnames (список на весь маршрут) и matches[].path (отдельно на каждое правило)

Проверь себя​

1. Какая из трёх проб (Liveness/Readiness/Startup) отвечает за перезапуск контейнера, а какая - за получение трафика?

Liveness отвечает за перезапуск (под завис, но процесс формально жив)
Readiness отвечает за трафик - если она не проходит, под убирают из Endpoints, и Service перестаёт направлять на него запросы

2. Как Service технически "находит" свой Endpoints и EndpointSlice?

Service и его Endpoints носят одно и то же имя (metadata.name) - это и есть связь
У EndpointSlice имя другое, потому что слайсов на один Service может быть несколько, поэтому связь идёт через лейбл kubernetes.io/service-name

3. Зачем нужен отдельный объект Endpoints, если у Service и так есть selector с лейблами?

selector у Service необязателен - Service можно создать и без него, и тогда Kubernetes не будет сам вычислять список подов
Типичный случай - Service указывает на внешний backend вне кластера (например, базу данных), а Endpoints для него заводят и поддерживают руками, именно поэтому это отдельный объект, а не поле Service

4. EndpointSlice по умолчанию ограничивает один слайс 100 адресами - зачем, если можно было бы хранить их все в одном объекте, как раньше делал Endpoints?

kube-proxy на каждой ноде следит за этими объектами, и любое изменение рассылается на все ноды
Если бы все адреса жили в одном большом объекте, любое изменение одного пода означало бы пересылку всего списка целиком - разбиение на слайсы уменьшает размер каждого такого изменения

5. В чём разница между маршрутизацией Ingress по пути и по хосту?

По пути - на одном и том же домене /foo и /bar ведут на разные Service
По хосту - разные домены (foo.bar.com, bar.foo.com), приходящие на один и тот же Ingress, ведут на разные Service

6. Ingress создан и выглядит корректно, но трафик никуда не идёт - что забыли установить?

Ingress Controller - сам ресурс Ingress лишь описывает правила, применяет их отдельный компонент (nginx, traefik и другие), который нужно ставить отдельно от манифеста

7. Как CRD связан с Gateway API?

Gateway API не входит в стандартный Kubernetes API - его ресурсы (Gateway, HTTPRoute, GatewayClass) реализованы как CRD и должны быть установлены в кластер до использования

8. Чем роль GatewayClass отличается от роли HTTPRoute в Gateway API?

GatewayClass описывает конкретную реализацию (обычно её ставит платформа/облако)
HTTPRoute - это уже правила маршрутизации конкретного приложения, которые настраивает разработчик, не трогая инфраструктурную часть

9. Достаточно ли установить только CRD Gateway API, чтобы трафик начал ходить?

Нет - CRD описывают только формат ресурсов, обрабатывать по ним трафик должен отдельно установленный Gateway API controller (например, Contour), ровно так же как для Ingress нужен Ingress Controller

10. /etc/hosts с 127.0.0.1 myapp.local - это то же самое, что настроить DNS?

Нет - /etc/hosts подменяет резолвинг только на локальной машине, ни один другой компьютер об этом не узнает
Для доступа для всех нужны настоящие DNS-записи у регистратора/в Yandex DNS

11. Почему для HTTPS недостаточно просто сгенерировать пару публичный/приватный ключ самому?

Нужно доказать, что публичный ключ действительно принадлежит вашему серверу, а не подделан - для этого его подписывает авторитетная третья сторона (сертификат от центра сертификации вроде Let's Encrypt), а не сам владелец ключа

12. Зачем нужен cert-manager, если сертификат можно один раз получить и подключить в Ingress вручную?

Сертификаты не бессрочны - cert-manager автоматически продлевает их до истечения срока и умеет работать сразу с несколькими центрами сертификации, а не только с одним

13. В примере с TLS Ingress сертификат кладут в Secret вручную, через base64 - при установленном cert-manager это по-прежнему обязательно делать так?

Нет - если есть ClusterIssuer, достаточно добавить на Ingress аннотацию cert-manager.io/cluster-issuer и spec.tls, cert-manager сам создаст нужный Secret и будет следить за его продлением

14. ClusterIssuer с ACME (Let's Encrypt) на minikube зависает на Order/Challenge и падает по таймауту - почему, и что использовать вместо него для локального теста?

HTTP01-проверка требует, чтобы ACME-сервер сам достучался до вашего Ingress по порту 80 снаружи

  • у minikube нет публичного IP, достучаться неоткуда
    Для локального теста без реального IP есть Issuer/ClusterIssuer с selfSigned - работает полностью офлайн, но сертификату не доверяют браузеры, потому что нет признанного центра сертификации за ним
15. Чем доступ через Tailscale отличается от minikube tunnel?

minikube tunnel делает Service типа LoadBalancer доступным только с той же машины, где запущен minikube
Tailscale заводит устройство в приватную сеть (tailnet) со стабильным IP - достучаться до Service можно с любого другого устройства, вошедшего в тот же tailnet, без публичного IP и без переезда в облако

16. Почему официальный Tailscale Kubernetes Operator нельзя так же просто направить на Headscale вместо SaaS Tailscale?

Оператор аутентифицируется через OAuth, которого Headscale не реализует - это открыто зафиксировано как неподдерживаемое в трекере задач самого проекта
С Headscale пришлось бы ставить обычный tailscale-клиент с флагом --login-server вручную, без операторского удобства

17. Почему Ingress с ingressClassName: tailscale даёт настоящий сертификат, а Service с loadBalancerClass: tailscale - нет?

Service - это L4-проксирование, трафик зашифрован WireGuard'ом, но отдельного HTTPS-сертификата для приложения не возникает
Ingress - L7, и оператор сам, на первом подключении, запрашивает у Let's Encrypt настоящий сертификат на MagicDNS-имя, если на tailnet включены HTTPS Certificates и MagicDNS

Практика​

  1. Настрой liveness и readiness probe для backend'а
  2. Установи Ingress Controller (minikube addons enable ingress или руководство для YCloud) и создай Ingress для фронтенда и бэкенда как для внешних сервисов
  3. В отдельном терминале запусти minikube tunnel - без него снаружи до Ingress на minikube не достучаться, у ingress-managed load balancer не будет внешнего адреса
  4. Пропиши тестовый домен в /etc/hosts и убедись, что сервисы доступны снаружи по имени, а не только по IP
  5. Установи Gateway API - CRD и отдельно контроллер (например, Contour) - создай Gateway и HTTPRoute для того же бэкенда, и сравни с Ingress-версией
  6. Если хочется настоящий домен - купи домен, настрой DNS-записи (Yandex DNS или другой регистратор), проверь доступность сервисов уже по нему, а не по /etc/hosts
  7. Установи cert-manager на minikube, создай ClusterIssuer с selfSigned и подключи сертификат к своему Ingress через аннотацию, вместо ручного Secret из "Примеры"
  8. На облачном кластере из 06-K8s-cloud-cluster (там есть настоящий внешний IP) - то же самое, но с ACME ClusterIssuer на staging-сервере Let's Encrypt (установка cert-manager с DNS01 - в 07-K8s-cloud-ingress), и сравни, чем отличается результат от self-signed варианта
  9. Установи Tailscale Kubernetes Operator (бесплатного personal-аккаунта Tailscale достаточно), переключи Service фронтенда на loadBalancerClass: tailscale и зайди на него с другого устройства (телефон, другой компьютер), вошедшего в тот же tailnet - без minikube tunnel, без домена и без переезда в облако
  10. Замени тот же Service на Ingress с ingressClassName: tailscale - сертификат на *.ts.net появится сам, настоящий и доверенный браузером, без единого шага через cert-manager

Итог / чек-лист​

  • Probes: Liveness - когда перезапускать, Readiness - когда давать трафик, Startup - запустилось ли приложение вообще, все три используют одни и те же поля, и пока startupProbe не пройдёт, livenessProbe/readinessProbe не запускаются
  • Readiness-проба напрямую управляет содержимым Endpoints/EndpointSlice - именно оттуда Service и, через него, Ingress/Gateway API узнают, какие поды готовы принимать трафик
  • Service и Endpoints связаны одинаковым именем (EndpointSlice - лейблом kubernetes.io/service-name), а сам Endpoints - отдельный объект именно потому, что selector у Service необязателен (пример - Service на внешний backend вне кластера)
  • Ingress - единая точка входа HTTP(S)-трафика с маршрутизацией по пути или по хосту, для работы ему всегда нужен отдельно установленный Ingress Controller - ingress-nginx, самая распространённая реализация, с марта 2026 в статусе retired
  • CRD - механизм для добавления своих типов объектов в API k8s, на нём целиком построен Gateway API
  • Gateway API разносит роли по разным ресурсам - GatewayClass (реализация), Gateway (точка входа), HTTPRoute (правила) - в отличие от одного плоского манифеста Ingress, и точно так же требует отдельно установленного контроллера
  • DNS в этой теме - настоящий интернет-DNS (ICANN/TLD/регистраторы, записи A/AAAA/CNAME/MX/TXT/NS), а не внутрикластерный
  • HTTPS требует не просто пары ключей, а подписанного сертификата от доверенного центра (Let's Encrypt и подобные)
  • cert-manager автоматизирует получение и продление сертификатов через ClusterIssuer и аннотацию на Ingress вместо ручного Secret - selfSigned работает без всякой внешней доступности (годится для minikube, но без доверия браузера), ACME/Let's Encrypt даёт настоящий доверенный сертификат, но требует реального внешнего IP, то есть облака, а не minikube
  • Tailscale Kubernetes Operator даёт внешний доступ к Service/Ingress на minikube без публичного IP и без облака - loadBalancerClass/ingressClassName: tailscale вместо обычных значений, устройство получает DNS-имя в приватной сети (tailnet), а не IP
    Для Ingress это DNS-имя получает и настоящий сертификат от Let's Encrypt, минуя cert-manager - ограничение про внешний IP из раздела про HTTPS сюда не применяется
    Headscale - open-source замена SaaS control-plane Tailscale, но с этим оператором не работает (нужен OAuth, которого там нет) - только с обычным tailscale-клиентом и --login-server
  • Развёртывание в настоящем облаке (а не minikube) - тема 06-K8s-cloud-cluster и 07-K8s-cloud-ingress

Дополнительно​