Лето 2024. Пятница, 18:30. Canary в проде не работает.
Задача простая: 10% трафика на новую версию API. Nginx Ingress, пример из документации:
# То, что я думал будет работать
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
rules:
- host: api.company.com
http:
paths:
- path: /v2
backend:
service:
name: api-v2
port:
number: 8080Применил. Сто процентов на новую версию. Поменял weight на 90. Снова сто.
Четыре часа: документацию семь раз, поиск «nginx ingress canary not working», версия контроллера 0.21 вместо нужных 0.22+, обновление сломало ещё два Ingress, отдельный ресурс с тем же host, path оказался regex, не prefix.
В 23:00 завелось. 47 строк YAML и 12 annotations. Половину смысла я не понимал.
В понедельник: «давайте ещё JWT на этот эндпоинт».
Тот же canary месяцем позже
На dev-кластере попробовал Envoy Gateway скептически.
# То, что работает с первого раза
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-route
spec:
parentRefs:
- name: my-gateway
hostnames:
- api.company.com
rules:
- matches:
- path:
type: PathPrefix
value: /v2
backendRefs:
- name: api-v1 # 90% трафика
port: 8080
weight: 90
- name: api-v2 # 10% трафика (canary)
port: 8080
weight: 1015 строк. Ноль annotations. С первого раза.
Через неделю попросили JWT. С Nginx это ещё annotations, внешний auth и вечер на токены. Здесь:
# Добавил 12 строк, JWT работает
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
name: jwt-policy
spec:
targetRef:
kind: HTTPRoute
name: api-route
jwt:
providers:
- name: auth0
issuer: https://company.auth0.com/
audiences:
- api://myapp
remoteJWKS:
uri: https://company.auth0.com/.well-known/jwks.jsonПрименил. Заработало. К Nginx Ingress annotations я больше не возвращался.
Из чего это состоит
Envoy Proxy от Lyft. На нём Istio. HTTP/1.1, HTTP/2, HTTP/3, gRPC, WebSocket. Least request и consistent hashing. Circuit breaking, retry, timeout без плагинов. Rate limit и JWT нативно. Метрики в Prometheus, трейсы в Jaeger.
Gateway API: официальный API Kubernetes для входящего трафика. Ingress старше и держится на строках в annotations. У каждого контроллера свой диалект. Переезд означает переписывание этих строк. Здесь поля типизированы, ошибка видна до применения, смена реализации это смена gatewayClassName.
Control plane читает Gateway и HTTPRoute, переводит в xDS, крутит поды Envoy, обновляет конфиг без рестарта, дружит с cert-manager, external-dns, Prometheus.
JWT и лимит без Redis
Сто запросов в минуту на пользователя. В Nginx для лимита часто нужен Redis и Lua. Здесь два манифеста.
# JWT: 12 строк
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
name: jwt-policy
spec:
targetRef:
kind: HTTPRoute
name: api-route
jwt:
providers:
- name: auth0
issuer: https://mycompany.auth0.com/
audiences:
- api://myapp
remoteJWKS:
uri: https://mycompany.auth0.com/.well-known/jwks.json
---
# Rate Limiting: 15 строк
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: rate-limit
spec:
targetRef:
kind: HTTPRoute
name: api-route
rateLimit:
type: Local
local:
rules:
- clientSelectors:
- headers:
- name: x-user-id
type: Distinct
limit:
requests: 100
unit: MinuteJWKS кешируется сам. Счётчики в памяти. Метрики сколько отсекли, сколько пустили.
gRPC
Nginx gRPC умеет. Health check и метрики по методам приходится достраивать. Envoy родился в Lyft вокруг gRPC.
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
name: user-service
spec:
parentRefs:
- name: my-gateway
hostnames:
- grpc.example.com
rules:
- matches:
- method:
service: myapp.v1.UserService
method: GetUser
backendRefs:
- name: user-service
port: 9090Latency GetUser и ошибки CreateUser видны отдельно. Health check, retry, circuit breaking рядом.
Nginx добавил gRPC позже. У Envoy это родная среда.
Не берите Envoy Gateway, если сервисов один-два, кластер старше 1.25, команда не готова переписывать Ingress, нужен mTLS между всеми сервисами (это Istio) или хочется один Ingress на весь кластер и забыть.
Берите, если нужны canary, JWT, лимиты, переносимость манифестов и метрики из коробки.
Как устроено внутри
┌─────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
├─────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────┐ │
│ │ Control Plane (envoy-gateway) │ │
│ │ │ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ Gateway API Controller │ │ │
│ │ │ (watches Gateway, Route) │ │ │
│ │ └────────┬─────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ xDS Translator │ │ │
│ │ │ (Gateway API → Envoy cfg)│ │ │
│ │ └────────┬─────────────────┘ │ │
│ │ │ xDS (gRPC) │ │
│ └───────────┼─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ Data Plane (envoy-proxy pods) │ │
│ │ │ │
│ │ ┌────────┐ ┌────────┐ │ │
│ │ │ Envoy │ │ Envoy │ ... │ │
│ │ │ Pod 1 │ │ Pod 2 │ │ │
│ │ └───┬────┘ └───┬────┘ │ │
│ │ │ │ │ │
│ └──────┼───────────┼──────────────┘ │
│ │ │ │
│ │ Ingress Traffic (HTTP/gRPC) │
│ ▼ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ Backend Services (Pods) │ │
│ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘
Один под control plane в envoy-gateway-system. N подов Envoy принимают трафик. Data plane масштабируется отдельно. Конфиг горячий. Формат xDS общий: контроллер можно сменить.
Nginx Ingress читает YAML сам. Здесь YAML читает control plane и по gRPC говорит прокси, что делать.
От нуля до canary
Нужны Kubernetes 1.25+ и kubectl.
# Gateway API CRDs
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yaml
# Envoy Gateway
kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/latest/install.yaml
# Проверяем
kubectl get pods -n envoy-gateway-system
# Должен быть pod: envoy-gateway-xxxxx (STATUS: Running)После установки появится GatewayClass envoy-gateway.
# Создаём файл echo-app.yaml
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Namespace
metadata:
name: demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo-v1
namespace: demo
spec:
replicas: 2
selector:
matchLabels:
app: echo
version: v1
template:
metadata:
labels:
app: echo
version: v1
spec:
containers:
- name: echo
image: hashicorp/http-echo:latest
args: ["-text=Hello from v1"]
ports:
- containerPort: 5678
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo-v2
namespace: demo
spec:
replicas: 2
selector:
matchLabels:
app: echo
version: v2
template:
metadata:
labels:
app: echo
version: v2
spec:
containers:
- name: echo
image: hashicorp/http-echo:latest
args: ["-text=Hello from v2 (canary!)"]
ports:
- containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
name: echo-v1
namespace: demo
spec:
selector:
app: echo
version: v1
ports:
- port: 80
targetPort: 5678
---
apiVersion: v1
kind: Service
metadata:
name: echo-v2
namespace: demo
spec:
selector:
app: echo
version: v2
ports:
- port: 80
targetPort: 5678
EOF
# Проверяем
kubectl get pods -n demo
# Должно быть 4 пода: echo-v1-xxx (2 шт), echo-v2-xxx (2 шт)cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: demo-gateway
namespace: demo
spec:
gatewayClassName: envoy-gateway
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Same
EOF
# Ждём, пока Gateway станет готовым
kubectl wait --for=condition=Programmed gateway/demo-gateway -n demo --timeout=300s
# Получаем IP/Hostname
export GATEWAY_IP=$(kubectl get gateway demo-gateway -n demo -o jsonpath='{.status.addresses[0].value}')
echo "Gateway IP: $GATEWAY_IP"В GKE, EKS, AKS Gateway получит внешний IP. В minikube и kind нужен kubectl port-forward.
cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: echo-route
namespace: demo
spec:
parentRefs:
- name: demo-gateway
hostnames:
- echo.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: echo-v1
port: 80
weight: 90 # 90% трафика на v1
- name: echo-v2
port: 80
weight: 10 # 10% трафика на v2 (canary)
EOF
# Проверяем статус
kubectl get httproute -n demo
# STATUS: Accepted# Если у вас внешний IP:
for i in {1..20}; do
curl -H "Host: echo.example.com" http://$GATEWAY_IP/
done
# Если используете port-forward (локальный кластер):
kubectl port-forward -n demo svc/demo-gateway-envoy-gateway 8080:80 &
for i in {1..20}; do
curl -H "Host: echo.example.com" http://localhost:8080/
done
# Результат (примерно):
# Hello from v1 (18 раз из 20 ≈ 90%)
# Hello from v2 (canary!) (2 раза из 20 ≈ 10%)Лимит пять запросов в минуту на x-user-id:
cat <<EOF | kubectl apply -f -
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: rate-limit
namespace: demo
spec:
targetRef:
group: gateway.networking.k8s.io
kind: HTTPRoute
name: echo-route
rateLimit:
type: Local
local:
rules:
- clientSelectors:
- headers:
- name: x-user-id
type: Distinct
limit:
requests: 5
unit: Minute
EOF
# Тестируем (делаем 10 запросов с одним user_id)
for i in {1..10}; do
curl -H "Host: echo.example.com" -H "x-user-id: user123" http://$GATEWAY_IP/
done
# Первые 5 запросов: HTTP 200 OK
# Следующие 5: HTTP 429 Too Many RequestsБез Redis. Счётчики в памяти, между репликами синхронизируются. В проде для нескольких Gateway можно включить Global и Redis.
Грабли
Gateway API живой. Gateway, HTTPRoute, GRPCRoute стабильны как v1. SecurityPolicy и BackendTrafficPolicy экспериментальные, сверяйте матрицу. Версию Envoy Gateway в проде фиксируйте.
Сертификаты руками каждые 90 дней не нужны. cert-manager:
# Установка cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml
# ClusterIssuer для Let's Encrypt
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
gatewayHTTPRoute:
parentRefs:
- name: demo-gateway
namespace: demo
EOF
# Gateway с TLS
cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: demo-gateway
namespace: demo
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
gatewayClassName: envoy-gateway
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: "*.example.com"
tls:
mode: Terminate
certificateRefs:
- name: example-com-tls
EOFМетрики Envoy отдаёт сам: envoy_http_downstream_rq_total, envoy_http_downstream_rq_xx, envoy_http_downstream_rq_time, envoy_cluster_upstream_rq_retry. Дашборд: Envoy Gateway Overview.
В проде минимум две реплики Envoy, лимиты CPU и памяти, TLS, лимит запросов, circuit breaking, preStop, манифесты в Git.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: production-gateway
namespace: production
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
gatewayClassName: envoy-gateway
listeners:
# HTTP → HTTPS redirect
- name: http
protocol: HTTP
port: 80
hostname: "*.example.com"
# HTTPS с автоматическими сертификатами
- name: https
protocol: HTTPS
port: 443
hostname: "*.example.com"
tls:
mode: Terminate
certificateRefs:
- name: wildcard-tls
---
# Автоматический редирект на HTTPS
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: https-redirect
namespace: production
spec:
parentRefs:
- name: production-gateway
sectionName: http
hostnames:
- "*.example.com"
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301Миграция с Nginx
Тридцать Ingress в проде в один день не перевожу. Ставлю Envoy рядом, отдельный LoadBalancer, один некритичный сервис, неделя метрик. Потом DNS 10/90, 25, 50, 75, 100. Откат: переключить DNS. Nginx держу ещё две недели.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
rules:
- host: api.example.com
http:
paths:
- path: /api(/|$)(.*)
backend:
service:
name: api-service
port:
number: 8080apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-route
spec:
parentRefs:
- name: production-gateway
hostnames:
- api.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /api
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: api-service
port: 8080
---
# Rate limiting через отдельную политику
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: api-rate-limit
spec:
targetRef:
kind: HTTPRoute
name: api-route
rateLimit:
type: Local
local:
rules:
- limit:
requests: 100
unit: MinuteAnnotations стали полями. Regex стал PathPrefix. Лимит уехал в политику, которую можно повесить на другой маршрут.
Я не учу «annotations Nginx Ingress». Учу Gateway API. Это же знание работает с Istio Gateway, Cilium, Kong.
Локальный кластер и пятнадцать минут на пример выше обычно хватает, чтобы решить, стоит ли тащить это в прод.
См. также:

