Ingress rules: host, path, TLS, annotations
Теперь — как именно Ingress маршрутизирует. Концептуально просто: пришёл HTTP-запрос с Host: app.example.com и path /api/v1/users, controller матчит правило, форвардит в указанный Service. На практике три ловушки: pathType ведёт себя неинтуитивно, TLS требует Secret правильного формата, и annotations — это vendor lock-in, который рушит миграцию между controllers.
Разберём каждый аспект.
L4 vs L7 load balancing: transport против application
Структура rules
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /v1
pathType: Prefix
backend:
service:
name: api-v1
port:
number: 80
- path: /v2
pathType: Prefix
backend:
service:
name: api-v2
port:
number: 80
- host: www.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
Иерархия: rules[] → каждое правило с host + http.paths[] → каждый path с pathType + backend.service.
pathType: три варианта и их подвох
С v1.18 поле pathType стало обязательным. Три значения:
Exact
Точное совпадение path. Никаких префиксов:
- path: /healthz
pathType: Exact
backend:
service:
name: health
port:
number: 80
/healthz→ match/healthz/→ НЕ match (trailing slash считается)/healthz/v1→ НЕ match/healthzz→ НЕ match
Prefix
Префикс path, разделённый по /:
- path: /api
pathType: Prefix
/api→ match/api/→ match/api/v1/users→ match/api/v1/users?x=1→ match (query string игнорируется)/apifoo→ НЕ match — это другой path element
Это самый коварный момент. Многие думают, что Prefix /foo матчит ВСЁ, что начинается с /foo, как regex ^/foo.*. Это НЕ так. Prefix матчит по segments, разделённым /. Поэтому /foo матчит /foo, /foo/, /foo/bar, но НЕ /foobar. Это поведение по спецификации, и controllers обязаны его соблюдать.
Корректнее всего думать о Prefix как о regex ^/foo($|/.*).
ImplementationSpecific
Поведение зависит от controller. Например, ingress-nginx интерпретирует path: /api(/.*)? как regex с capture group для rewrite. На других controllers этот path вообще не сработает.
- path: /api(/.*)?
pathType: ImplementationSpecific
Используется когда нужна функциональность за рамками стандарта (regex paths, rewrite). Не переносится между controllers.
Алгоритм матчинга путей
Когда есть несколько paths под одним host, controller выбирает самый длинный matching prefix (longest-match wins):
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port: { number: 80 }
- path: /api
pathType: Prefix
backend:
service:
name: api
port: { number: 80 }
- path: /api/v2
pathType: Prefix
backend:
service:
name: api-v2
port: { number: 80 }
Запрос /api/v2/orders → matching paths: /, /api, /api/v2. Самый длинный — /api/v2. Запрос идёт в api-v2.
Запрос /api/v1/users → matching: /, /api. Самый длинный — /api. Запрос идёт в api.
Запрос /about → matching: /. Запрос идёт в web.
Exact имеет приоритет над Prefix той же длины.
host: virtual host routing
rules:
- host: api.example.com
http: { ... }
- host: web.example.com
http: { ... }
- host: "*.example.com" # wildcard, только один уровень
http: { ... }
- Без host — catch-all для любого Host-заголовка (используется когда у вас один IP без DNS)
- Конкретный host — точное совпадение Host-заголовка
- Wildcard
*.example.com— матчит ровно один уровень:foo.example.comда,foo.bar.example.comнет
Wildcards в Ingress — не глобальные. *.example.com НЕ матчит сам example.com (нужно отдельное правило) и не матчит a.b.example.com (это два уровня subdomain).
defaultBackend: catch-all
Если ни одно правило не подошло, можно отдать запрос в defaultBackend:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
spec:
ingressClassName: nginx
defaultBackend:
service:
name: error-page
port:
number: 80
rules:
- host: app.example.com
http:
paths: [ ... ]
Без defaultBackend controller вернёт 404. С defaultBackend — запрос пойдёт на custom error-page service. Часто используется для красивых 404-страниц или fallback на статический сайт.
TLS: HTTPS termination
Ingress умеет терминировать TLS — controller сам делает SSL handshake и отдаёт backend трафик уже plain HTTP:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
spec.tls[] — список TLS-конфигураций. Каждая привязывает SNI hosts к Secret с certificate.
Secret типа kubernetes.io/tls
Secret должен быть именно типа kubernetes.io/tls, с ключами tls.crt и tls.key:
apiVersion: v1
kind: Secret
metadata:
name: app-tls
type: kubernetes.io/tls
data:
tls.crt: <base64-encoded PEM certificate>
tls.key: <base64-encoded PEM private key>
Создать через kubectl:
kubectl create secret tls app-tls \
--cert=fullchain.pem \
--key=privkey.pem
Secret должен быть в том же namespace, что и Ingress. Cross-namespace ссылки на TLS Secret из Ingress спецификации не поддерживаются (это решено в Gateway API через ReferenceGrant — увидим в lessons 4-5).
Несколько TLS-сертификатов на одном Ingress
tls:
- hosts: [api.example.com]
secretName: api-tls
- hosts: [web.example.com]
secretName: web-tls
Controller использует SNI (Server Name Indication) — клиент в TLS handshake передаёт hostname, controller выбирает правильный сертификат.
cert-manager: автоматизация Let’s Encrypt
В production выпускать сертификаты руками раз в 90 дней — мука. Решение — cert-manager, controller, который автоматически выпускает и обновляет сертификаты через Let’s Encrypt.
Подход: добавить annotation на Ingress, cert-manager увидит и создаст Secret сам:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-tls # cert-manager создаст этот Secret сам
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
Renewal происходит автоматически за 30 дней до истечения. Это де-факто стандарт в production Kubernetes.
Annotations: vendor lock-in
Vanilla Ingress API очень минималистичный. Реальные production-фичи (rewrite, rate-limit, auth, body size, timeouts, websocket support) задаются через annotations на Ingress, и эти annotations — специфичны для каждого controller.
Примеры ingress-nginx annotations
metadata:
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/auth-url: "https://auth.example.com/validate"
nginx.ingress.kubernetes.io/auth-signin: "https://auth.example.com/login"
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/limit-rps: "10"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8,192.168.0.0/16"
Примеры Traefik annotations
metadata:
annotations:
traefik.ingress.kubernetes.io/router.middlewares: default-redirect@kubernetescrd
traefik.ingress.kubernetes.io/router.tls: "true"
Примеры AWS ALB annotations
metadata:
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:eu-west-1:...
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
Annotations не переносятся между controllers. Если вы написали Ingress с nginx.ingress.kubernetes.io/rewrite-target и потом решили переехать на Traefik или AWS ALB — все эти annotations нужно переписать или переделать через CRD. Это главная мотивация для Gateway API, которая native решает эти задачи через типизированные объекты (увидим в lessons 4-5).
Rewrite: классический пример
Очень частая задача: серверу удобно, чтобы path был /orders, но снаружи он смотрится через /api/v2/orders. Нужно “снять” /api/v2 префикс при проксировании в backend.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /api/v2(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: api-v2
port:
number: 80
pathиспользует regex (ImplementationSpecific) с capture group(.*)rewrite-target: /$2подставляет вторую capture group в backend path
Запрос /api/v2/orders → backend получит /orders. На Traefik для этого был бы Middleware ReplacePathRegex — совсем другая конфигурация.
Полный пример: production-like Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
nginx.ingress.kubernetes.io/rate-limit: "1000"
spec:
ingressClassName: nginx
tls:
- hosts:
- shop.example.com
- api.shop.example.com
secretName: shop-tls
rules:
- host: shop.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
- path: /static
pathType: Prefix
backend:
service:
name: cdn-proxy
port:
number: 80
- host: api.shop.example.com
http:
paths:
- path: /v1
pathType: Prefix
backend:
service:
name: api-v1
port:
number: 80
- path: /v2
pathType: Prefix
backend:
service:
name: api-v2
port:
number: 80
defaultBackend:
service:
name: error-page
port:
number: 80
Что здесь:
- Два host:
shop.example.comиapi.shop.example.com - Внутри
shopдва path:/и/static(longest match выберет/staticдля статики) - Внутри
apiдва path:/v1и/v2 - TLS на оба хоста через один Secret
shop-tls(cert-manager выпустит multi-SAN сертификат) - defaultBackend на error-page для всего, что не подошло
- Annotations: ssl-redirect, body size, rate limit
kubectl describe ingress
kubectl describe ingress shop
# Name: shop
# Namespace: default
# Address: 1.2.3.4
# Ingress Class: nginx
# Default backend: error-page:80
# TLS:
# shop-tls terminates shop.example.com, api.shop.example.com
# Rules:
# Host Path Backends
# ---- ---- --------
# shop.example.com
# / web:80
# /static cdn-proxy:80
# api.shop.example.com
# /v1 api-v1:80
# /v2 api-v2:80
# Annotations:
# cert-manager.io/cluster-issuer: letsencrypt-prod
# nginx.ingress.kubernetes.io/ssl-redirect: true
# ...
# Events:
# Type Reason Age From Message
# ---- ------ ---- ---- -------
# Normal Sync 5m nginx-ingress-controller Scheduled for sync
Events показывают, обработал ли controller Ingress. Если Events: <none> — controller не watch-ит этот Ingress (часто из-за неправильной ingressClassName).
CKAD-сценарий: создать Ingress
Типовая задача с экзамена:
Создайте Ingress
webв namespacedefault, ingressClassNamenginx. Маршрут/api(Prefix) → serviceapiport 80. Маршрут/web(Prefix) → servicewebport 80. Host оставьте пустым.
# Imperative (kubectl create ingress появилось в v1.19)
kubectl create ingress web \
--class=nginx \
--rule='/api*=api:80' \
--rule='/web*=web:80'
* в --rule означает Prefix; без * — Exact. Это бесценный шорткат на экзамене, когда нет времени писать YAML.
Declarative:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 80
- path: /web
pathType: Prefix
backend:
service:
name: web
port:
number: 80
Проверка:
kubectl describe ingress web
kubectl get ingress web -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
# С узла кластера (host пустой — нужен IP напрямую):
curl http://<INGRESS_IP>/api/health
curl http://<INGRESS_IP>/web/