Неделя 7: Envoy gateway, ext_authz, NotificationService с Telegram
Сервисы есть. Аутентификация есть. Не хватает единой точки входа в систему и нормальных уведомлений пользователю. Эту неделю собираем gateway на Envoy и полноценный NotificationService с интеграцией Telegram.
Цели недели
- Поднять Envoy как gateway, маршрутизировать
/order,/auth, etc. в нужные сервисы. - Настроить ext_authz, чтобы все запросы (кроме
/auth/loginи/auth/register) проверялись через AuthService.Whoami. - Создать NotificationService с архитектурой как у других сервисов.
- Подписать NotificationService на
order-paidиship-assembled. - Реализовать Telegram-клиента, отправлять уведомления конечному пользователю.
Зачем Envoy
До этой недели клиенты ходили напрямую в OrderService. На пять сервисов это уже неудобно:
- надо помнить разные порты,
- нет единого места для авторизации,
- нет общего rate limiting и observability,
- TLS terminate приходится делать в каждом сервисе.
Решение, API gateway. На рынке есть варианты: nginx, Envoy, Kong, Traefik. В курсе Envoy потому что:
- C++-движок, очень быстрый,
- поддерживает HTTP/1.1, HTTP/2, HTTP/3, gRPC, gRPC-Web,
- встроенный ext_authz для делегирования авторизации,
- xDS-протокол для динамической конфигурации (k8s service mesh),
- богатая observability (статистика, tracing).
Конкуренты: nginx проще, но без gRPC и ext_authz “из коробки”. Traefik удобен в Docker. Kong популярен с плагинами на Lua.
Маршруты Envoy
# infra/envoy/envoy.yaml
static_resources:
listeners:
- name: edge
address: { socket_address: { address: 0.0.0.0, port_value: 8080 } }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: edge
http_filters:
- name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
grpc_service:
envoy_grpc:
cluster_name: ext_authz
timeout: 0.5s
include_peer_certificate: false
transport_api_version: V3
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
route_config:
name: edge_routes
virtual_hosts:
- name: edge
domains: ["*"]
routes:
- match: { prefix: /auth/login }
route: { cluster: auth_service }
typed_per_filter_config:
envoy.filters.http.ext_authz:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
disabled: true
- match: { prefix: /auth/register }
route: { cluster: auth_service }
typed_per_filter_config:
envoy.filters.http.ext_authz:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
disabled: true
- match: { prefix: /auth }
route: { cluster: auth_service }
- match: { prefix: /order }
route: { cluster: order_service }
- match: { prefix: /payment }
route: { cluster: payment_service }
- match: { prefix: /analytics }
route: { cluster: analytics_service }
- match: { prefix: /inventory }
route: { cluster: inventory_service }
clusters:
- name: auth_service
connect_timeout: 1s
type: STRICT_DNS
load_assignment:
cluster_name: auth_service
endpoints:
- lb_endpoints:
- endpoint:
address: { socket_address: { address: auth, port_value: 50051 } }
- name: ext_authz
connect_timeout: 0.5s
type: STRICT_DNS
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http2_protocol_options: {}
load_assignment:
cluster_name: ext_authz
endpoints:
- lb_endpoints:
- endpoint:
address: { socket_address: { address: ext-authz, port_value: 5050 } }
# ... order, payment, inventory clusters
Ключевой момент, disabled: true на роутах /auth/login и /auth/register. Без этого запрос на login заходит в ext_authz, тот делает Whoami, получает 401, и пользователь никогда не залогинится.
ext_authz: три варианта реализации
Вариант 1, отдельный gRPC-сервис (рекомендую)
Envoy общается с тобой по Authorization gRPC API. Ты пишешь маленький Go-сервис, который:
- принимает CheckRequest (с заголовками и метаданными),
- достаёт
X-Session-Token, - вызывает AuthService.Whoami,
- возвращает CheckResponse с OK или DENIED.
type AuthZ struct {
auth authv1.AuthServiceClient
}
func (a *AuthZ) Check(ctx context.Context, req *envoy_auth.CheckRequest) (*envoy_auth.CheckResponse, error) {
headers := req.GetAttributes().GetRequest().GetHttp().GetHeaders()
token := headers["x-session-token"]
if token == "" {
return denied(codes.Unauthenticated, "no session"), nil
}
resp, err := a.auth.Whoami(ctx, &authv1.WhoamiRequest{SessionToken: token})
if err != nil {
return denied(codes.Unauthenticated, "invalid session"), nil
}
return ok(resp.UserUuid), nil
}
func ok(userUUID string) *envoy_auth.CheckResponse {
return &envoy_auth.CheckResponse{
Status: &status.Status{Code: int32(codes.OK)},
HttpResponse: &envoy_auth.CheckResponse_OkResponse{
OkResponse: &envoy_auth.OkHttpResponse{
Headers: []*core.HeaderValueOption{
{Header: &core.HeaderValue{Key: "x-user-uuid", Value: userUUID}},
},
},
},
}
}
Преимущество, прокидывает user_uuid в downstream-сервисы через заголовок x-user-uuid, и им уже не нужно ходить в Auth.
Вариант 2, ext_authz через HTTP
Тот же протокол, но HTTP. Проще, но дороже по latency (HTTP/1.1 без multiplexing).
Вариант 3, Lua-скрипт
Без отдельного сервиса. Lua-скрипт прямо в Envoy:
function envoy_on_request(request_handle)
local token = request_handle:headers():get("x-session-token")
if not token then
request_handle:respond({[":status"] = "401"}, "")
return
end
local resp = request_handle:httpCall("auth_service",
{[":method"]="POST", [":path"]="/whoami", [":authority"]="auth"},
'{"session_token":"'..token..'"}', 1000)
if resp[":status"] ~= "200" then
request_handle:respond({[":status"] = "401"}, "")
end
end
Подходит для очень простых случаев. Сложнее тестировать, без типизации.
В курсе берём gRPC ext_authz.
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.